From 719eb3b8966ed3d755c7ce704ce5303159c2c94e Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Fri, 10 Jul 2026 19:45:45 +0100 Subject: [PATCH] Add YAML Mach-O parser fixtures - Cover malformed command sizes, bounds and string offsets. - Exercise byte order, build metadata and runtime paths. --- lib/macho/exceptions.rb | 8 +++ lib/macho/load_commands.rb | 2 + lib/macho/macho_file.rb | 12 ++++ test/bin/yaml2obj/Makefile | 8 +++ test/bin/yaml2obj/big-endian.macho | Bin 0 -> 28 bytes test/bin/yaml2obj/big-endian.yaml | 11 +++ test/bin/yaml2obj/build-version.macho | Bin 0 -> 64 bytes test/bin/yaml2obj/build-version.yaml | 22 ++++++ test/bin/yaml2obj/invalid-lcstr-offset.macho | Bin 0 -> 56 bytes test/bin/yaml2obj/invalid-lcstr-offset.yaml | 18 +++++ .../bin/yaml2obj/load-cmdsize-too-large.macho | Bin 0 -> 48 bytes test/bin/yaml2obj/load-cmdsize-too-large.yaml | 15 ++++ .../bin/yaml2obj/load-cmdsize-too-small.macho | Bin 0 -> 104 bytes test/bin/yaml2obj/load-cmdsize-too-small.yaml | 24 +++++++ test/bin/yaml2obj/minimal.macho | Bin 0 -> 32 bytes test/bin/yaml2obj/minimal.yaml | 12 ++++ test/bin/yaml2obj/rpath.macho | Bin 0 -> 56 bytes test/bin/yaml2obj/rpath.yaml | 18 +++++ .../truncated-load-command-region.macho | Bin 0 -> 32 bytes .../truncated-load-command-region.yaml | 12 ++++ .../yaml2obj/truncated-load-commands.macho | Bin 0 -> 32 bytes .../bin/yaml2obj/truncated-load-commands.yaml | 12 ++++ test/bin/yaml2obj/unaligned-cmdsize.macho | Bin 0 -> 57 bytes test/bin/yaml2obj/unaligned-cmdsize.yaml | 17 +++++ test/test_macho.rb | 65 ++++++++++++++++++ 25 files changed, 256 insertions(+) create mode 100644 test/bin/yaml2obj/Makefile create mode 100644 test/bin/yaml2obj/big-endian.macho create mode 100644 test/bin/yaml2obj/big-endian.yaml create mode 100644 test/bin/yaml2obj/build-version.macho create mode 100644 test/bin/yaml2obj/build-version.yaml create mode 100644 test/bin/yaml2obj/invalid-lcstr-offset.macho create mode 100644 test/bin/yaml2obj/invalid-lcstr-offset.yaml create mode 100644 test/bin/yaml2obj/load-cmdsize-too-large.macho create mode 100644 test/bin/yaml2obj/load-cmdsize-too-large.yaml create mode 100644 test/bin/yaml2obj/load-cmdsize-too-small.macho create mode 100644 test/bin/yaml2obj/load-cmdsize-too-small.yaml create mode 100644 test/bin/yaml2obj/minimal.macho create mode 100644 test/bin/yaml2obj/minimal.yaml create mode 100644 test/bin/yaml2obj/rpath.macho create mode 100644 test/bin/yaml2obj/rpath.yaml create mode 100644 test/bin/yaml2obj/truncated-load-command-region.macho create mode 100644 test/bin/yaml2obj/truncated-load-command-region.yaml create mode 100644 test/bin/yaml2obj/truncated-load-commands.macho create mode 100644 test/bin/yaml2obj/truncated-load-commands.yaml create mode 100644 test/bin/yaml2obj/unaligned-cmdsize.macho create mode 100644 test/bin/yaml2obj/unaligned-cmdsize.yaml diff --git a/lib/macho/exceptions.rb b/lib/macho/exceptions.rb index 89bfc15ab..11c0f82b7 100644 --- a/lib/macho/exceptions.rb +++ b/lib/macho/exceptions.rb @@ -126,6 +126,14 @@ def initialize(num) end end + # Raised when a load command has an invalid size. + class LoadCommandSizeError < NotAMachOError + # @param size [Integer] the invalid size + def initialize(size) + super("Invalid Mach-O load command size: #{size}") + end + end + # Raised when a load command can't be created manually. class LoadCommandNotCreatableError < MachOError # @param cmd_sym [Symbol] the uncreatable load command's symbol diff --git a/lib/macho/load_commands.rb b/lib/macho/load_commands.rb index 8d3dbae3a..edae2fc94 100644 --- a/lib/macho/load_commands.rb +++ b/lib/macho/load_commands.rb @@ -332,6 +332,8 @@ def initialize(lc, lc_str) view = lc.view if view + raise LCStrMalformedError, lc if lc_str < lc.class.bytesize || lc_str >= lc.cmdsize + lc_str_abs = view.offset + lc_str lc_end = view.offset + lc.cmdsize - 1 raw_string = view.raw_data.slice(lc_str_abs..lc_end) diff --git a/lib/macho/macho_file.rb b/lib/macho/macho_file.rb index 6b50f7c02..e0fd6b3c3 100644 --- a/lib/macho/macho_file.rb +++ b/lib/macho/macho_file.rb @@ -579,17 +579,27 @@ def check_filetype(filetype) # All load commands in the file. # @return [Array] an array of load commands + # @raise [TruncatedFileError] if the declared load command data is incomplete # @raise [LoadCommandError] if an unknown load command is encountered + # @raise [LoadCommandSizeError] if a load command's size is invalid # @api private def populate_load_commands permissive = options.fetch(:permissive, false) offset = header.class.bytesize + load_commands_end = offset + sizeofcmds + raise TruncatedFileError if load_commands_end > @raw_data.bytesize + load_commands = [] @load_commands_by_type = Hash.new { |h, k| h[k] = [] } header.ncmds.times do + raise TruncatedFileError if offset + LoadCommands::LoadCommand.bytesize > load_commands_end + fmt = Utils.specialize_format("L=", endianness) cmd = @raw_data.slice(offset, 4).unpack1(fmt) + cmdsize = @raw_data.slice(offset + 4, 4).unpack1(fmt) + raise LoadCommandSizeError, cmdsize if cmdsize % 4 != 0 || offset + cmdsize > load_commands_end + cmd_sym = LoadCommands::LOAD_COMMANDS[cmd] raise LoadCommandError, cmd unless cmd_sym || permissive @@ -602,6 +612,8 @@ def populate_load_commands LoadCommands::LoadCommand end + raise LoadCommandSizeError, cmdsize if cmdsize < klass.bytesize + view = MachOView.new(self, @raw_data, endianness, offset) command = klass.new_from_bin(view) diff --git a/test/bin/yaml2obj/Makefile b/test/bin/yaml2obj/Makefile new file mode 100644 index 000000000..cae0086ee --- /dev/null +++ b/test/bin/yaml2obj/Makefile @@ -0,0 +1,8 @@ +YAMLS := $(wildcard *.yaml) +MACHOS := $(YAMLS:.yaml=.macho) + +.PHONY: all +all: $(MACHOS) + +%.macho: %.yaml + yaml2obj $< -o $@ diff --git a/test/bin/yaml2obj/big-endian.macho b/test/bin/yaml2obj/big-endian.macho new file mode 100644 index 0000000000000000000000000000000000000000..6cabf9415f30845bc9b9424ed17c6b43f9e620c2 GIT binary patch literal 28 XcmezO_SZQE1_pK@W(HzLAcg|~V>ko9 literal 0 HcmV?d00001 diff --git a/test/bin/yaml2obj/big-endian.yaml b/test/bin/yaml2obj/big-endian.yaml new file mode 100644 index 000000000..f2c229732 --- /dev/null +++ b/test/bin/yaml2obj/big-endian.yaml @@ -0,0 +1,11 @@ +--- !mach-o +IsLittleEndian: false +FileHeader: + magic: 0xFEEDFACE + cputype: 0x00000007 + cpusubtype: 0x00000003 + filetype: 0x00000001 + ncmds: 0 + sizeofcmds: 0 + flags: 0x00000000 +... diff --git a/test/bin/yaml2obj/build-version.macho b/test/bin/yaml2obj/build-version.macho new file mode 100644 index 0000000000000000000000000000000000000000..154e540abbd8348504b765f759018f1969f1b2aa GIT binary patch literal 64 ucmX^A>+L^w1_nlE1_lNuAZ7$&1t5k2BOpToh(U4;3>-kr304DA2Lb^5Ap}YQ literal 0 HcmV?d00001 diff --git a/test/bin/yaml2obj/build-version.yaml b/test/bin/yaml2obj/build-version.yaml new file mode 100644 index 000000000..c2c2106e8 --- /dev/null +++ b/test/bin/yaml2obj/build-version.yaml @@ -0,0 +1,22 @@ +--- !mach-o +IsLittleEndian: true +FileHeader: + magic: 0xFEEDFACF + cputype: 0x01000007 + cpusubtype: 0x00000003 + filetype: 0x00000002 + ncmds: 1 + sizeofcmds: 32 + flags: 0x00000000 + reserved: 0x00000000 +LoadCommands: + - cmd: LC_BUILD_VERSION + cmdsize: 32 + platform: 2 + minos: 0x00080000 + sdk: 0x00090000 + ntools: 1 + Tools: + - tool: 1 + version: 0 +... diff --git a/test/bin/yaml2obj/invalid-lcstr-offset.macho b/test/bin/yaml2obj/invalid-lcstr-offset.macho new file mode 100644 index 0000000000000000000000000000000000000000..b264e1659ccc43667a9c055985fe607d28ecfb63 GIT binary patch literal 56 ncmX^A>+L^w1_nlE1_lNuAZ7$&2_S|6J|F|eFG(#ffr$eE+L^w1_nlE1|R{&96*{u0Yoq`yx+I>o&baiCIR7z2QmNv literal 0 HcmV?d00001 diff --git a/test/bin/yaml2obj/load-cmdsize-too-large.yaml b/test/bin/yaml2obj/load-cmdsize-too-large.yaml new file mode 100644 index 000000000..b03895a96 --- /dev/null +++ b/test/bin/yaml2obj/load-cmdsize-too-large.yaml @@ -0,0 +1,15 @@ +--- !mach-o +IsLittleEndian: true +FileHeader: + magic: 0xFEEDFACF + cputype: 0x01000007 + cpusubtype: 0x00000003 + filetype: 0x00000001 + ncmds: 1 + sizeofcmds: 8 + flags: 0x00002000 + reserved: 0x00000000 +LoadCommands: + - cmd: 0xDEADBEEF + cmdsize: 16 +... diff --git a/test/bin/yaml2obj/load-cmdsize-too-small.macho b/test/bin/yaml2obj/load-cmdsize-too-small.macho new file mode 100644 index 0000000000000000000000000000000000000000..200df8ae98f851963113ff9fb14e1d247178a1cd GIT binary patch literal 104 zcmX^A>+L^w1_nlE1|R{&7C@Ro0Yoq`NCGj4A0Hp$8W92#fdT;t4W(eBa2jYP11p@z GzyJU=^9Gs# literal 0 HcmV?d00001 diff --git a/test/bin/yaml2obj/load-cmdsize-too-small.yaml b/test/bin/yaml2obj/load-cmdsize-too-small.yaml new file mode 100644 index 000000000..aa6074e09 --- /dev/null +++ b/test/bin/yaml2obj/load-cmdsize-too-small.yaml @@ -0,0 +1,24 @@ +--- !mach-o +IsLittleEndian: true +FileHeader: + magic: 0xFEEDFACF + cputype: 0x01000007 + cpusubtype: 0x00000003 + filetype: 0x00000001 + ncmds: 1 + sizeofcmds: 56 + flags: 0x00002000 + reserved: 0x00000000 +LoadCommands: + - cmd: LC_SEGMENT_64 + cmdsize: 56 + segname: '__TEXT' + vmaddr: 0x1000 + vmsize: 0x10 + fileoff: 0 + filesize: 0 + maxprot: 7 + initprot: 5 + nsects: 0 + flags: 0 +... diff --git a/test/bin/yaml2obj/minimal.macho b/test/bin/yaml2obj/minimal.macho new file mode 100644 index 0000000000000000000000000000000000000000..1aeb60b315cc1f4649c1c0878088de253f0eb82f GIT binary patch literal 32 XcmX^A>+L@t1_nk3AOI5}1`+@Oa!&)p literal 0 HcmV?d00001 diff --git a/test/bin/yaml2obj/minimal.yaml b/test/bin/yaml2obj/minimal.yaml new file mode 100644 index 000000000..24b080353 --- /dev/null +++ b/test/bin/yaml2obj/minimal.yaml @@ -0,0 +1,12 @@ +--- !mach-o +IsLittleEndian: true +FileHeader: + magic: 0xFEEDFACF + cputype: 0x0100000C + cpusubtype: 0x00000000 + filetype: 0x00000001 + ncmds: 0 + sizeofcmds: 0 + flags: 0x00000000 + reserved: 0x00000000 +... diff --git a/test/bin/yaml2obj/rpath.macho b/test/bin/yaml2obj/rpath.macho new file mode 100644 index 0000000000000000000000000000000000000000..5f6eb484b1db67b28c19d90643a9926ccbdcd09c GIT binary patch literal 56 ucmX^A>+L^w1_nlE1_lNuAZ7$&2_S|683u+15T6H#^-GJ3^m8(kKym>19tVm5 literal 0 HcmV?d00001 diff --git a/test/bin/yaml2obj/rpath.yaml b/test/bin/yaml2obj/rpath.yaml new file mode 100644 index 000000000..8879a9689 --- /dev/null +++ b/test/bin/yaml2obj/rpath.yaml @@ -0,0 +1,18 @@ +--- !mach-o +IsLittleEndian: true +FileHeader: + magic: 0xFEEDFACF + cputype: 0x01000007 + cpusubtype: 0x00000003 + filetype: 0x00000002 + ncmds: 1 + sizeofcmds: 24 + flags: 0x00000000 + reserved: 0x00000000 +LoadCommands: + - cmd: LC_RPATH + cmdsize: 24 + path: 12 + Content: /usr/lib + ZeroPadBytes: 3 +... diff --git a/test/bin/yaml2obj/truncated-load-command-region.macho b/test/bin/yaml2obj/truncated-load-command-region.macho new file mode 100644 index 0000000000000000000000000000000000000000..088352781a2fed90bb02a38ffb624ab4bad334cc GIT binary patch literal 32 dcmX^A>+L^w1_nlE1|R{%Aix146hH(60|0YN1L*(& literal 0 HcmV?d00001 diff --git a/test/bin/yaml2obj/truncated-load-command-region.yaml b/test/bin/yaml2obj/truncated-load-command-region.yaml new file mode 100644 index 000000000..3b3a2634e --- /dev/null +++ b/test/bin/yaml2obj/truncated-load-command-region.yaml @@ -0,0 +1,12 @@ +--- !mach-o +IsLittleEndian: true +FileHeader: + magic: 0xFEEDFACF + cputype: 0x01000007 + cpusubtype: 0x00000003 + filetype: 0x00000001 + ncmds: 0 + sizeofcmds: 8 + flags: 0x00002000 + reserved: 0x00000000 +... diff --git a/test/bin/yaml2obj/truncated-load-commands.macho b/test/bin/yaml2obj/truncated-load-commands.macho new file mode 100644 index 0000000000000000000000000000000000000000..fad68c07f6d06d0918831e15458a7b859e76e2b9 GIT binary patch literal 32 ccmX^A>+L^w1_nlE1|R{&K#GAu0ZcLg0CM{S;s5{u literal 0 HcmV?d00001 diff --git a/test/bin/yaml2obj/truncated-load-commands.yaml b/test/bin/yaml2obj/truncated-load-commands.yaml new file mode 100644 index 000000000..0eab121e8 --- /dev/null +++ b/test/bin/yaml2obj/truncated-load-commands.yaml @@ -0,0 +1,12 @@ +--- !mach-o +IsLittleEndian: true +FileHeader: + magic: 0xFEEDFACF + cputype: 0x01000007 + cpusubtype: 0x00000003 + filetype: 0x00000001 + ncmds: 1 + sizeofcmds: 0 + flags: 0x00002000 + reserved: 0x00000000 +... diff --git a/test/bin/yaml2obj/unaligned-cmdsize.macho b/test/bin/yaml2obj/unaligned-cmdsize.macho new file mode 100644 index 0000000000000000000000000000000000000000..e49b2cfd5b90f5e359910ae03ba7f0feae5a3ef8 GIT binary patch literal 57 ycmX^A>+L^w1_nlE1_lNuAZ7$&Ng##+X&?j47gREK2~8{Sn7L~AnY-`)GXMY$whRaW literal 0 HcmV?d00001 diff --git a/test/bin/yaml2obj/unaligned-cmdsize.yaml b/test/bin/yaml2obj/unaligned-cmdsize.yaml new file mode 100644 index 000000000..87b560a59 --- /dev/null +++ b/test/bin/yaml2obj/unaligned-cmdsize.yaml @@ -0,0 +1,17 @@ +--- !mach-o +IsLittleEndian: true +FileHeader: + magic: 0xFEEDFACF + cputype: 0x01000007 + cpusubtype: 0x00000003 + filetype: 0x00000002 + ncmds: 1 + sizeofcmds: 25 + flags: 0x00000000 + reserved: 0x00000000 +LoadCommands: + - cmd: LC_UUID + cmdsize: 25 + uuid: 00112233-4455-6677-8899-AABBCCDDEEFF + ZeroPadBytes: 1 +... diff --git a/test/test_macho.rb b/test/test_macho.rb index 34f01410d..eea38ecd3 100644 --- a/test/test_macho.rb +++ b/test/test_macho.rb @@ -27,6 +27,71 @@ def test_truncated_file end end + def test_load_command_with_size_smaller_than_its_structure + assert_raises MachO::LoadCommandSizeError do + MachO::MachOFile.new(fixture(:yaml2obj, "load-cmdsize-too-small.macho")) + end + end + + def test_load_command_with_size_beyond_its_region + assert_raises MachO::LoadCommandSizeError do + MachO::MachOFile.new(fixture(:yaml2obj, "load-cmdsize-too-large.macho")) + end + end + + def test_declared_load_commands_are_truncated + assert_raises MachO::TruncatedFileError do + MachO::MachOFile.new(fixture(:yaml2obj, "truncated-load-commands.macho")) + end + end + + def test_declared_load_command_region_is_truncated + assert_raises MachO::TruncatedFileError do + MachO::MachOFile.new(fixture(:yaml2obj, "truncated-load-command-region.macho")) + end + end + + def test_load_command_with_unaligned_size + assert_raises MachO::LoadCommandSizeError do + MachO::MachOFile.new(fixture(:yaml2obj, "unaligned-cmdsize.macho")) + end + end + + def test_load_command_with_invalid_string_offset + command = MachO::MachOFile.new(fixture(:yaml2obj, "invalid-lcstr-offset.macho"))[:LC_LOAD_DYLINKER].first + + assert_raises MachO::LCStrMalformedError do + command.name + end + end + + def test_minimal_macho + file = MachO::MachOFile.new(fixture(:yaml2obj, "minimal.macho")) + + assert file.object? + assert_equal :arm64, file.cputype + end + + def test_big_endian_macho + file = MachO::MachOFile.new(fixture(:yaml2obj, "big-endian.macho")) + + assert_equal :big, file.endianness + assert file.magic32? + assert_equal :i386, file.cputype + end + + def test_build_version_command + command = MachO::MachOFile.new(fixture(:yaml2obj, "build-version.macho"))[:LC_BUILD_VERSION].first + + assert_equal "8.0.0", command.minos_string + assert_equal "9.0.0", command.sdk_string + assert_equal 1, command.tool_entries.tools.size + end + + def test_rpath_command + assert_equal ["/usr/lib"], MachO::MachOFile.new(fixture(:yaml2obj, "rpath.macho")).rpaths + end + def test_load_commands filenames = SINGLE_ARCHES.map { |a| fixture(a, "hello.bin") }