Skip to content

Commit 86da59b

Browse files
authored
Merge pull request #1030 from alebcay/fat-arch-alignment
Validate FatArch alignment and reject truncated Fat architecture records
2 parents f47772d + 899b5e2 commit 86da59b

5 files changed

Lines changed: 124 additions & 1 deletion

File tree

‎lib/macho/exceptions.rb‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,15 @@ def initialize(offset)
238238
end
239239
end
240240

241+
# Raised when a fat architecture record specifies an invalid alignment value.
242+
# @param align [Integer] the invalid alignment exponent
243+
class FatArchAlignmentError < NotAMachOError
244+
# @param align [Integer] the invalid alignment exponent
245+
def initialize(align)
246+
super("Invalid fat architecture alignment: 2**#{align} exceeds maximum supported value")
247+
end
248+
end
249+
241250
# Raised when attempting to parse a compressed Mach-O without explicitly
242251
# requesting decompression.
243252
class CompressedMachOError < MachOError

‎lib/macho/fat_file.rb‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@ def populate_fat_header
371371

372372
# Obtain an array of fat architectures from raw file data.
373373
# @return [Array<Headers::FatArch>] an array of fat architectures
374+
# @raise [TruncatedFileError] if the file is too small to contain all fat architectures
374375
# @api private
375376
def populate_fat_archs
376377
archs = []
@@ -380,7 +381,14 @@ def populate_fat_archs
380381
fa_len = fa_klass.bytesize
381382

382383
header.nfat_arch.times do |i|
383-
archs << fa_klass.new_from_bin(:big, @raw_data[fa_off + (fa_len * i), fa_len])
384+
arch_off = fa_off + (fa_len * i)
385+
arch_bin = @raw_data[arch_off, fa_len]
386+
raise TruncatedFileError if arch_bin.nil? || arch_bin.bytesize < fa_len
387+
388+
arch = fa_klass.new_from_bin(:big, arch_bin)
389+
raise FatArchAlignmentError, arch.align if arch.align > Headers::MAX_FAT_ARCH_ALIGN
390+
391+
archs << arch
384392
end
385393

386394
archs

‎lib/macho/headers.rb‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,12 @@ def to_h
537537
end
538538
end
539539

540+
# Maximum supported fat architecture alignment exponent.
541+
# The fat architecture `align` field stores an exponent N where the actual
542+
# alignment is 2**N. This limit matches MAXSECTALIGN from cctools and
543+
# prevents excessive padding allocation during Fat file reconstruction.
544+
MAX_FAT_ARCH_ALIGN = 15
545+
540546
# 32-bit fat binary header architecture structure. A 32-bit fat Mach-O has one or more of
541547
# these, indicating one or more internal Mach-O blobs.
542548
# @note "32-bit" indicates the fact that this structure stores 32-bit offsets, not that the

‎test/test_code_signing.rb‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,31 @@ def test_rejects_duplicate_signature_commands
248248
end
249249
end
250250

251+
def test_rejects_invalid_fat_alignment_before_signing
252+
# Build a valid Fat file and then corrupt the alignment field to exceed MAX_FAT_ARCH_ALIGN
253+
machos = SINGLE_ARCHES.map { |a| MachO::MachOFile.new(fixture(a, "hello.bin")) }
254+
fat = MachO::FatFile.new_from_machos(*machos)
255+
fat_bin = fat.serialize
256+
257+
# FAT_MAGIC (32-bit) FatArch structure: align at offset 8 + 16 = 24
258+
fat_header_size = MachO::Headers::FatHeader.bytesize
259+
align_offset = fat_header_size + 16
260+
261+
# Set alignment to MAX_FAT_ARCH_ALIGN + 1 (16)
262+
fat_bin[align_offset, 4] = [MachO::Headers::MAX_FAT_ARCH_ALIGN + 1].pack("N")
263+
264+
tempfile_with_data("hello", fat_bin) do |file|
265+
original = File.binread(file.path)
266+
267+
assert_raises MachO::CodeSigningError do
268+
MachO.codesign!(file.path)
269+
end
270+
271+
# File should not be modified when parsing fails
272+
assert_equal original, File.binread(file.path)
273+
end
274+
end
275+
251276
private
252277

253278
def entitlement_blob

‎test/test_fat.rb‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,81 @@ def test_zero_arch_file
5656
end
5757
end
5858

59+
def test_invalid_fat_arch_alignment_32
60+
# Build a valid Fat file and then corrupt the alignment field to exceed MAX_FAT_ARCH_ALIGN
61+
machos = SINGLE_ARCHES.map { |a| MachO::MachOFile.new(fixture(a, "hello.bin")) }
62+
fat = MachO::FatFile.new_from_machos(*machos)
63+
fat_bin = fat.serialize
64+
65+
# FAT_MAGIC (32-bit) FatArch structure: cputype(4) + cpusubtype(4) + offset(4) + size(4) + align(4) = 20 bytes
66+
# First FatArch starts at offset 8 (FatHeader is 8 bytes)
67+
# align is at offset 8 + 16 = 24
68+
fat_header_size = MachO::Headers::FatHeader.bytesize
69+
align_offset = fat_header_size + 16 # cputype(4) + cpusubtype(4) + offset(4) + size(4)
70+
71+
# Set alignment to MAX_FAT_ARCH_ALIGN + 1 (16)
72+
fat_bin[align_offset, 4] = [MachO::Headers::MAX_FAT_ARCH_ALIGN + 1].pack("N")
73+
74+
assert_raises MachO::FatArchAlignmentError do
75+
MachO::FatFile.new_from_bin(fat_bin)
76+
end
77+
end
78+
79+
def test_invalid_fat_arch_alignment_64
80+
# Build a valid Fat64 file and then corrupt the alignment field to exceed MAX_FAT_ARCH_ALIGN
81+
machos = SINGLE_ARCHES.map { |a| MachO::MachOFile.new(fixture(a, "hello.bin")) }
82+
fat = MachO::FatFile.new_from_machos(*machos, :fat64 => true)
83+
fat_bin = fat.serialize
84+
85+
# FAT_MAGIC_64 (64-bit) FatArch64 structure: cputype(4) + cpusubtype(4) + offset(8) + size(8) + align(4) + reserved(4) = 32 bytes
86+
# First FatArch64 starts at offset 8 (FatHeader is 8 bytes)
87+
# align is at offset 8 + 24 = 32
88+
fat_header_size = MachO::Headers::FatHeader.bytesize
89+
align_offset = fat_header_size + 24 # cputype(4) + cpusubtype(4) + offset(8) + size(8)
90+
91+
# Set alignment to MAX_FAT_ARCH_ALIGN + 1 (16)
92+
fat_bin[align_offset, 4] = [MachO::Headers::MAX_FAT_ARCH_ALIGN + 1].pack("N")
93+
94+
assert_raises MachO::FatArchAlignmentError do
95+
MachO::FatFile.new_from_bin(fat_bin)
96+
end
97+
end
98+
99+
def test_max_valid_fat_arch_alignment_accepted
100+
# Build a valid Fat file and set alignment to exactly MAX_FAT_ARCH_ALIGN
101+
machos = SINGLE_ARCHES.map { |a| MachO::MachOFile.new(fixture(a, "hello.bin")) }
102+
fat = MachO::FatFile.new_from_machos(*machos)
103+
fat_bin = fat.serialize
104+
105+
fat_header_size = MachO::Headers::FatHeader.bytesize
106+
align_offset = fat_header_size + 16
107+
108+
# Set alignment to MAX_FAT_ARCH_ALIGN (15) - should be accepted
109+
fat_bin[align_offset, 4] = [MachO::Headers::MAX_FAT_ARCH_ALIGN].pack("N")
110+
111+
# Should parse without raising
112+
parsed = MachO::FatFile.new_from_bin(fat_bin)
113+
assert_equal MachO::Headers::MAX_FAT_ARCH_ALIGN, parsed.fat_archs.first.align
114+
end
115+
116+
def test_truncated_fat_arch_32
117+
# FAT_MAGIC + nfat_arch=1 + FatArch with only 16 bytes (cputype, cpusubtype, offset, size) missing align (4 bytes)
118+
# Total = 8 + 16 = 24 bytes instead of 28 bytes
119+
bin = [MachO::Headers::FAT_MAGIC, 1, MachO::Headers::CPU_TYPE_I386, 3, 24, 0].pack("N6")
120+
assert_raises MachO::TruncatedFileError do
121+
MachO::FatFile.new_from_bin(bin)
122+
end
123+
end
124+
125+
def test_truncated_fat_arch_64
126+
# FAT_MAGIC_64 + nfat_arch=1 + FatArch64 with 24 bytes (cputype, cpusubtype, offset(8), size(8)) missing align(4) + reserved(4)
127+
# Total = 8 + 24 = 32 bytes instead of 40 bytes
128+
bin = [MachO::Headers::FAT_MAGIC_64, 1].pack("N2") + [MachO::Headers::CPU_TYPE_I386, 3, 24, 0].pack("N2Q>2")
129+
assert_raises MachO::TruncatedFileError do
130+
MachO::FatFile.new_from_bin(bin)
131+
end
132+
end
133+
59134
def test_mismatch_cpu_arch_file
60135
assert_raises MachO::CPUTypeMismatchError do
61136
MachO::FatFile.new("test/bin/llvm/macho-invalid-fat_cputype")

0 commit comments

Comments
 (0)