From b571e444b9c480e95a6953dda70a0343ff7a8e84 Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Fri, 10 Jul 2026 19:20:01 +0100 Subject: [PATCH] Validate Mach-O headers before full reads - Avoid loading large invalid files before `MachOFile` and `FatFile` can reject malformed headers. - Keep successful parsing on one handle so serialisation and mutation continue to use complete file data. - Preserve compressed Mach-O handling when decompression needs the full payload. - Coerce tail reads so a `nil` EOF cannot replace parser errors with `TypeError`. --- lib/macho/fat_file.rb | 7 ++++++- lib/macho/macho_file.rb | 7 ++++++- test/test_fat.rb | 14 ++++++++++++++ test/test_macho.rb | 14 ++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/macho/fat_file.rb b/lib/macho/fat_file.rb index 0439ab5f7..30e348d2f 100644 --- a/lib/macho/fat_file.rb +++ b/lib/macho/fat_file.rb @@ -96,7 +96,12 @@ def initialize(filename, **opts) @filename = filename @options = opts - @raw_data = File.binread(@filename) + File.open(@filename, "rb") do |file| + @raw_data = file.read(Headers::FatHeader.bytesize) + @raw_data ||= "" + populate_fat_header + @raw_data << file.read.to_s + end populate_fields end diff --git a/lib/macho/macho_file.rb b/lib/macho/macho_file.rb index 6b50f7c02..03a205408 100644 --- a/lib/macho/macho_file.rb +++ b/lib/macho/macho_file.rb @@ -60,7 +60,12 @@ def initialize(filename, **opts) @filename = filename @options = opts - @raw_data = File.binread(@filename) + File.open(@filename, "rb") do |file| + @raw_data = file.read(Headers::MachHeader.bytesize) + @raw_data ||= "" + populate_mach_header if !opts.fetch(:decompress, false) || !Utils.compressed_magic?(@raw_data.unpack1("N")) + @raw_data << file.read.to_s + end populate_fields end diff --git a/test/test_fat.rb b/test/test_fat.rb index 11bfe6fd6..0c7b98801 100644 --- a/test/test_fat.rb +++ b/test/test_fat.rb @@ -36,6 +36,20 @@ def test_java_classfile end end + def test_invalid_header_is_rejected_before_full_read + tempfile_with_data("invalid_header", [MachO::Headers::FAT_MAGIC, 31].pack("N2") + ("x" * 1024)) do |invalid_header| + assert_raises MachO::JavaClassFileError do + Class.new(MachO::FatFile) do + def populate_fat_header + raise "read entire file" if serialize.bytesize > MachO::Headers::FatHeader.bytesize + + super + end + end.new(invalid_header.path) + end + end + end + def test_zero_arch_file assert_raises MachO::ZeroArchitectureError do MachO::FatFile.new("test/bin/llvm/macho-invalid-fat-header") diff --git a/test/test_macho.rb b/test/test_macho.rb index 34f01410d..3c9ab7087 100644 --- a/test/test_macho.rb +++ b/test/test_macho.rb @@ -27,6 +27,20 @@ def test_truncated_file end end + def test_invalid_header_is_rejected_before_full_read + tempfile_with_data("invalid_header", [MachO::Headers::MH_MAGIC, 0, 0, MachO::Headers::MH_EXECUTE, 0, 0, 0].pack("N7") + ("x" * 1024)) do |invalid_header| + assert_raises MachO::CPUTypeError do + Class.new(MachO::MachOFile) do + def populate_mach_header + raise "read entire file" if serialize.bytesize > MachO::Headers::MachHeader.bytesize + + super + end + end.new(invalid_header.path) + end + end + end + def test_load_commands filenames = SINGLE_ARCHES.map { |a| fixture(a, "hello.bin") }