forked from chromium/chromium
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
mac: Pad the unsigned official branded x86_64 main executable to ~257kB
Seriously. When the main executable is updated on disk while the application is running, and the offset of the Mach-O image at the main executable’s path changes from the offset that was determined when the executable was loaded, SecCode ceases to be able to work with the executable. This may be triggered when the product is updated on disk but the application has not yet relaunched. This affects SecCodeCopySelf and SecCodeCopyGuestWithAttributes. Bugs are evident even when validation (SecCodeCheckValidity) is not attempted. Practically, this is only a concern for fat (universal) files, because the offset of a Mach-O image in a thin (single-architecture) file is always 0. The branded product always ships a fat executable, and because some uses of SecCode are in OS code beyond Chrome’s control, an effort is made to freeze the geometry of the branded (is_chrome_branded) for-public-release (is_official_build) main executable. The fat file is produced by installer/mac/universalizer.py. The x86_64 slice always precedes the arm64 slice: lipo, as used by universalizer.py, always places the arm64 slice last. See Xcode 12.0 https://github.com/apple-oss-distributions/cctools/blob/cctools-973.0.1/misc/lipo.c#L2672 cmp_qsort, used by create_fat at #L962. universalizer.py ensures that the first slice in the file is located at a constant offset (16kB since 98.0.4758.80), but if the first slice’s size changes, it can affect the offset of the second slice, the arm64 one, triggering SecCode-related bugs for arm64 users across updates. As quite a hack of a workaround, the offset of the arm64 slice within the fat main executable is fixed at a constant value by introducing padding to the x86_64 slice that precedes it. The arm64 slice needs to remain at offset 304kB (since 98.0.4758.80), so enough padding is added to the x86_64 slice to ensure that the arm64 slice lands where it needs to be when universalized. This padding needs to be added to the thin form of the x86_64 image before being fed to universalizer.py. To make things extra tricky, the final size of the x86_64 image is not known when it is linked, because code signing will contribute more data to the file. Typically, official code signing adds a non-constant 22-23kB. The non-constancy makes a precise computation of the required padding at this stage of the build impossible. Luckily, the size of the x86_64 image doesn’t need to be so precise. The arm64 slice that follows it in the fat file will be 16kB-aligned, so it’s enough to ensure that the x86_64 slice ends at an offset in the range (288kB, 304kB], or, since the x86_64 slice itself begins at offset 16kB, its total size once signed must be in the range (272kB, 288kB]. Targeting size 280kB, right in the middle of that range, and assuming an expected 23200-byte contribution from code signing, the unsigned x86_64 image should be padded to 263520 bytes. Code signing may then add any amount in the range (15008 bytes, 31392 bytes] and the result, when universalized, will have its arm64 slice at the required 304kB offset. This introduces //build/toolchain/apple/pad_linkedit.py to insert the padding, which can be used by passing -Wcrl,pad_linkedit to linker_driver.py, and uses it when linking the x86_64 main executable in branded official builds. pad_linkedit.py can only increase the size of the image. It will raise an error on an attempt to decrease the size. Fortunately, the x86_64 main executable in this build configuration is currently smaller than this size, and is expected to shrink even further in the future. Bug: 1300598, 1295098 Change-Id: Ib1b92c45ecae3d38752eb56103e436032e818c03 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3488454 Reviewed-by: Avi Drissman <avi@chromium.org> Reviewed-by: Robert Sesek <rsesek@chromium.org> Commit-Queue: Mark Mentovai <mark@chromium.org> Cr-Commit-Position: refs/heads/main@{#974951}
- Loading branch information
1 parent
361540a
commit d70a026
Showing
3 changed files
with
268 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
#!/usr/bin/env python3 | ||
# coding: utf-8 | ||
|
||
# Copyright 2022 The Chromium Authors. All rights reserved. | ||
# Use of this source code is governed by a BSD-style license that can be | ||
# found in the LICENSE file. | ||
|
||
# Increases the size of a Mach-O image by adding padding to its __LINKEDIT | ||
# segment. | ||
|
||
import argparse | ||
import os | ||
import struct | ||
import sys | ||
|
||
# Constants from <mach-o/loader.h>. | ||
_MH_MAGIC = 0xfeedface | ||
_MH_MAGIC_64 = 0xfeedfacf | ||
_LC_SEGMENT = 0x1 | ||
_LC_SEGMENT_64 = 0x19 | ||
_LC_CODE_SIGNATURE = 0x1d | ||
_SEG_LINKEDIT = b'__LINKEDIT'.ljust(16, b'\x00') | ||
|
||
|
||
class PadLinkeditError(Exception): | ||
pass | ||
|
||
|
||
def _struct_read_unpack(file, format_or_struct): | ||
"""Reads bytes from |file|, unpacking them via the struct module. This | ||
function is provided for convenience: the number of bytes to read from | ||
|file| is determined based on the size of data that the struct unpack | ||
operation will consume. | ||
Args: | ||
file: The file object to read from. | ||
format_or_struct: A string suitable for struct.unpack’s |format| | ||
argument, or a struct.Struct object. This will be used to determine | ||
the number of bytes to read and to perform the unpack. | ||
Returns: | ||
A tuple of unpacked items. | ||
""" | ||
|
||
if isinstance(format_or_struct, struct.Struct): | ||
struc = format_or_struct | ||
return struc.unpack(file.read(struc.size)) | ||
|
||
format = format_or_struct | ||
return struct.unpack(format, file.read(struct.calcsize(format))) | ||
|
||
|
||
def PadLinkedit(file, size): | ||
"""Takes |file|, a single-architecture (thin) Mach-O image, and increases | ||
its size to |size| by adding padding (NUL bytes) to its __LINKEDIT segment. | ||
If |file| is not a thin Mach-O image, if its structure is unexpected, if it | ||
is already larger than |size|, or if it is already code-signed, raises | ||
PadLinkeditError. | ||
The image must already have a __LINKEDIT segment, the load command for the | ||
__LINKEDIT segment must be the last segment load command in the image, and | ||
the __LINKEDIT segment contents must be at the end of the file. | ||
Args: | ||
file: The file object to read from and modify. | ||
size: The desired size of the file. | ||
Returns: | ||
None | ||
Raises: | ||
PadLinkeditError if |file| is not suitable for the operation. | ||
""" | ||
|
||
file.seek(0, os.SEEK_END) | ||
current_size = file.tell() | ||
file.seek(0, os.SEEK_SET) | ||
|
||
magic, = _struct_read_unpack(file, '<I') | ||
if magic == _MH_MAGIC_64: | ||
bits, endian = 64, '<' | ||
elif magic == _MH_MAGIC: | ||
bits, endian = 32, '<' | ||
elif magic == struct.unpack('>I', struct.pack('<I', _MH_MAGIC_64)): | ||
bits, endian = 64, '>' | ||
elif magic == struct.unpack('>I', struct.pack('<I', _MH_MAGIC)): | ||
bits, endian = 32, '>' | ||
else: | ||
raise PadLinkeditError('unrecognized magic', magic) | ||
|
||
if bits == 64: | ||
lc_segment = _LC_SEGMENT_64 | ||
segment_command_struct = struct.Struct(endian + '16s4Q4I') | ||
else: | ||
lc_segment = _LC_SEGMENT | ||
segment_command_struct = struct.Struct(endian + '16s8I') | ||
|
||
grow = size - current_size | ||
if grow < 0: | ||
raise PadLinkeditError('file would need to shrink', grow) | ||
|
||
(cputype, cpusubtype, filetype, ncmds, sizeofcmds, | ||
flags) = _struct_read_unpack(file, | ||
endian + '6I' + ('4x' if bits == 64 else '')) | ||
|
||
load_command_struct = struct.Struct(endian + '2I') | ||
found_linkedit = False | ||
segment_max_offset = 0 | ||
|
||
# Iterate through the load commands. It’s possible to consider |sizeofcmds|, | ||
# but since the file is being edited in-place, that would just be a sanity | ||
# check. | ||
for load_command_index in range(ncmds): | ||
cmd, cmdsize = _struct_read_unpack(file, load_command_struct) | ||
consumed = load_command_struct.size | ||
if cmd == lc_segment: | ||
if found_linkedit: | ||
raise PadLinkeditError('__LINKEDIT segment not last') | ||
|
||
(segname, vmaddr, vmsize, fileoff, filesize, maxprot, initprot, | ||
nsects, flags) = _struct_read_unpack(file, segment_command_struct) | ||
consumed += segment_command_struct.size | ||
|
||
if segname == _SEG_LINKEDIT: | ||
found_linkedit = True | ||
|
||
if fileoff < segment_max_offset: | ||
raise PadLinkeditError('__LINKEDIT data not last') | ||
if fileoff + filesize != current_size: | ||
raise PadLinkeditError('__LINKEDIT data not at EOF') | ||
|
||
vmsize += grow | ||
filesize += grow | ||
file.seek(-segment_command_struct.size, os.SEEK_CUR) | ||
file.write( | ||
segment_command_struct.pack(segname, vmaddr, vmsize, | ||
fileoff, filesize, maxprot, | ||
initprot, nsects, flags)) | ||
|
||
segment_max_offset = max(segment_max_offset, fileoff + filesize) | ||
elif cmd == _LC_CODE_SIGNATURE: | ||
raise PadLinkeditError( | ||
'modifying an already-signed image would render it unusable') | ||
|
||
# Aside from the above, load commands aren’t being interpreted, or even | ||
# read, so skip ahead to the next one. | ||
file.seek(cmdsize - consumed, os.SEEK_CUR) | ||
|
||
if not found_linkedit: | ||
raise PadLinkeditError('no __LINKEDIT') | ||
|
||
# Add the padding to the __LINKEDIT segment data. | ||
file.seek(grow, os.SEEK_END) | ||
file.truncate() | ||
|
||
|
||
def _main(args): | ||
parser = argparse.ArgumentParser( | ||
description= | ||
'Increase the size of a Mach-O image by adding padding to its ' + | ||
'__LINKEDIT segment.') | ||
parser.add_argument('file', help='The Mach-O file to modify') | ||
parser.add_argument('size', | ||
type=int, | ||
help='The desired final size of the file, in bytes') | ||
parsed = parser.parse_args() | ||
|
||
with open(parsed.file, 'r+b') as file: | ||
PadLinkedit(file, parsed.size) | ||
|
||
|
||
if __name__ == '__main__': | ||
sys.exit(_main(sys.argv[1:])) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters