ruby-macho 2.5.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cfe1d04c6c841958f4ac428e7207d89e02371fa9ca9ac3cb48e3e2a34a65dc24
4
- data.tar.gz: 21dc14db70485e37ff5e430d5f8a99af6cb1c0ac14f02ae1943121fa2c482629
3
+ metadata.gz: ba2fc7b81345cb788c6e1de15bace8d35f7ac1f9a17b6b6a12289a4e51b978de
4
+ data.tar.gz: 395dd63235823f33f11463d395b790b1cee2003ab430bb54326c03ca3de402b8
5
5
  SHA512:
6
- metadata.gz: d668c48f840f4423ae4bbc89d30c53e26a709491d2f8ca66bc98c637563e596d107638779bd517bf1fe89458d5f05beadf76ea60d80a44578aa0b409be61fc43
7
- data.tar.gz: 84009cdfbe5c61bc84ffd8838c7fe033171ebc05af18b7748c53ced99a0e92fd7f45f1a7c0f50001c05eb253e00564b3f8dbe7735fe04df087b3fef72d2be7e6
6
+ metadata.gz: c048978f3e0b1becaa3b050db8730f2f914cc8a8d662e1e7d24ca0a24c03c043747485e7e6033bad5789069795d8c04daad053d817f26c54dd809082975a01fe
7
+ data.tar.gz: 53a576e666842a464af2f961825becfa51da0b26f3dd9ec656ce9859a6c0e9c017fc6622ec330bb0fc04e63089053a6ecd3740f505eb0e5aefecfb5728672d72
data/README.md CHANGED
@@ -59,7 +59,7 @@ puts lc_vers.version_string # => "10.10.0"
59
59
  Attribution:
60
60
 
61
61
  * Constants were taken from Apple, Inc's
62
- [`loader.h` in `cctools/include/mach-o`](https://www.opensource.apple.com/source/cctools/cctools-870/include/mach-o/loader.h).
62
+ [`loader.h` in `cctools/include/mach-o`](https://opensource.apple.com/source/cctools/cctools-973.0.1/include/mach-o/loader.h.auto.html).
63
63
  (Apple Public Source License 2.0).
64
64
 
65
65
  ### License
@@ -84,7 +84,7 @@ module MachO
84
84
  # @param cpusubtype [Integer] the CPU sub-type of the unknown pair
85
85
  def initialize(cputype, cpusubtype)
86
86
  super "Unrecognized CPU sub-type: 0x%08<cpusubtype>x" \
87
- " (for CPU type: 0x%08<cputype>x" % { :cputype => cputype, :cpusubtype => cpusubtype }
87
+ " (for CPU type: 0x%08<cputype>x" % { :cputype => cputype, :cpusubtype => cpusubtype }
88
88
  end
89
89
  end
90
90
 
@@ -120,7 +120,7 @@ module MachO
120
120
  # @param actual_arity [Integer] the number of arguments received
121
121
  def initialize(cmd_sym, expected_arity, actual_arity)
122
122
  super "Expected #{expected_arity} arguments for #{cmd_sym} creation," \
123
- " got #{actual_arity}"
123
+ " got #{actual_arity}"
124
124
  end
125
125
  end
126
126
 
@@ -137,7 +137,7 @@ module MachO
137
137
  # @param lc [MachO::LoadCommand] the load command containing the string
138
138
  def initialize(lc)
139
139
  super "Load command #{lc.type} at offset #{lc.view.offset} contains a" \
140
- " malformed string"
140
+ " malformed string"
141
141
  end
142
142
  end
143
143
 
@@ -154,8 +154,8 @@ module MachO
154
154
  # @param filename [String] the filename
155
155
  def initialize(filename)
156
156
  super "Updated load commands do not fit in the header of " \
157
- "#{filename}. #{filename} needs to be relinked, possibly with " \
158
- "-headerpad or -headerpad_max_install_names"
157
+ "#{filename}. #{filename} needs to be relinked, possibly with " \
158
+ "-headerpad or -headerpad_max_install_names"
159
159
  end
160
160
  end
161
161
 
@@ -207,4 +207,14 @@ module MachO
207
207
  " Consider merging with `fat64: true`"
208
208
  end
209
209
  end
210
+
211
+ # Raised when attempting to parse a compressed Mach-O without explicitly
212
+ # requesting decompression.
213
+ class CompressedMachOError < MachOError
214
+ end
215
+
216
+ # Raised when attempting to decompress a compressed Mach-O without adequate
217
+ # dependencies, or on other decompression errors.
218
+ class DecompressionError < MachOError
219
+ end
210
220
  end
@@ -55,7 +55,7 @@ module MachO
55
55
  machos.each do |macho|
56
56
  macho_offset = Utils.round(offset, 2**macho.segment_alignment)
57
57
 
58
- raise FatArchOffsetOverflowError, macho_offset if !fat64 && macho_offset > (2**32 - 1)
58
+ raise FatArchOffsetOverflowError, macho_offset if !fat64 && macho_offset > ((2**32) - 1)
59
59
 
60
60
  macho_pads[macho] = Utils.padding_for(offset, 2**macho.segment_alignment)
61
61
 
@@ -96,7 +96,7 @@ module MachO
96
96
 
97
97
  @filename = filename
98
98
  @options = opts
99
- @raw_data = File.open(@filename, "rb", &:read)
99
+ @raw_data = File.binread(@filename)
100
100
  populate_fields
101
101
  end
102
102
 
@@ -238,6 +238,8 @@ module MachO
238
238
  # @param options [Hash]
239
239
  # @option options [Boolean] :strict (true) if true, fail if one slice fails.
240
240
  # if false, fail only if all slices fail.
241
+ # @option options [Boolean] :uniq (false) for each slice: if true, change
242
+ # each rpath simultaneously.
241
243
  # @return [void]
242
244
  # @see MachOFile#change_rpath
243
245
  def change_rpath(old_path, new_path, options = {})
@@ -268,6 +270,9 @@ module MachO
268
270
  # @param options [Hash]
269
271
  # @option options [Boolean] :strict (true) if true, fail if one slice fails.
270
272
  # if false, fail only if all slices fail.
273
+ # @option options [Boolean] :uniq (false) for each slice: if true, delete
274
+ # only the first runtime path that matches. if false, delete all duplicate
275
+ # paths that match.
271
276
  # @return void
272
277
  # @see MachOFile#delete_rpath
273
278
  def delete_rpath(path, options = {})
@@ -291,7 +296,7 @@ module MachO
291
296
  # @param filename [String] the file to write to
292
297
  # @return [void]
293
298
  def write(filename)
294
- File.open(filename, "wb") { |f| f.write(@raw_data) }
299
+ File.binwrite(filename, @raw_data)
295
300
  end
296
301
 
297
302
  # Write all (fat) data to the file used to initialize the instance.
@@ -301,7 +306,7 @@ module MachO
301
306
  def write!
302
307
  raise MachOError, "no initial file to write to" if filename.nil?
303
308
 
304
- File.open(@filename, "wb") { |f| f.write(@raw_data) }
309
+ File.binwrite(@filename, @raw_data)
305
310
  end
306
311
 
307
312
  # @return [Hash] a hash representation of this {FatFile}
data/lib/macho/headers.rb CHANGED
@@ -37,6 +37,18 @@ module MachO
37
37
  # @api private
38
38
  MH_CIGAM_64 = 0xcffaedfe
39
39
 
40
+ # compressed mach-o magic
41
+ # @api private
42
+ COMPRESSED_MAGIC = 0x636f6d70 # "comp"
43
+
44
+ # a compressed mach-o slice, using LZSS for compression
45
+ # @api private
46
+ COMP_TYPE_LZSS = 0x6c7a7373 # "lzss"
47
+
48
+ # a compressed mach-o slice, using LZVN ("FastLib") for compression
49
+ # @api private
50
+ COMP_TYPE_FASTLIB = 0x6c7a766e # "lzvn"
51
+
40
52
  # association of magic numbers to string representations
41
53
  # @api private
42
54
  MH_MAGICS = {
@@ -433,6 +445,11 @@ module MachO
433
445
  # @api private
434
446
  MH_KEXT_BUNDLE = 0xb
435
447
 
448
+ # a set of Mach-Os, running in the same userspace, sharing a linkedit. The kext collection files are an example
449
+ # of this object type
450
+ # @api private
451
+ MH_FILESET = 0xc
452
+
436
453
  # association of filetypes to Symbol representations
437
454
  # @api private
438
455
  MH_FILETYPES = {
@@ -447,6 +464,7 @@ module MachO
447
464
  MH_DYLIB_STUB => :dylib_stub,
448
465
  MH_DSYM => :dsym,
449
466
  MH_KEXT_BUNDLE => :kext_bundle,
467
+ MH_FILESET => :fileset,
450
468
  }.freeze
451
469
 
452
470
  # association of mach header flag symbols to values
@@ -478,6 +496,9 @@ module MachO
478
496
  :MH_HAS_TLV_DESCRIPTORS => 0x800000,
479
497
  :MH_NO_HEAP_EXECUTION => 0x1000000,
480
498
  :MH_APP_EXTENSION_SAFE => 0x02000000,
499
+ :MH_NLIST_OUTOFSYNC_WITH_DYLDINFO => 0x04000000,
500
+ :MH_SIM_SUPPORT => 0x08000000,
501
+ :MH_DYLIB_IN_CACHE => 0x80000000,
481
502
  }.freeze
482
503
 
483
504
  # Fat binary header structure
@@ -724,6 +745,11 @@ module MachO
724
745
  filetype == Headers::MH_KEXT_BUNDLE
725
746
  end
726
747
 
748
+ # @return [Boolean] whether or not the file is of type `MH_FILESET`
749
+ def fileset?
750
+ filetype == Headers::MH_FILESET
751
+ end
752
+
727
753
  # @return [Boolean] true if the Mach-O has 32-bit magic, false otherwise
728
754
  def magic32?
729
755
  Utils.magic32?(magic)
@@ -785,5 +811,88 @@ module MachO
785
811
  }.merge super
786
812
  end
787
813
  end
814
+
815
+ # Prelinked kernel/"kernelcache" header structure
816
+ class PrelinkedKernelHeader < MachOStructure
817
+ # @return [Integer] the magic number for a compressed header ({COMPRESSED_MAGIC})
818
+ attr_reader :signature
819
+
820
+ # @return [Integer] the type of compression used
821
+ attr_reader :compress_type
822
+
823
+ # @return [Integer] a checksum for the uncompressed data
824
+ attr_reader :adler32
825
+
826
+ # @return [Integer] the size of the uncompressed data, in bytes
827
+ attr_reader :uncompressed_size
828
+
829
+ # @return [Integer] the size of the compressed data, in bytes
830
+ attr_reader :compressed_size
831
+
832
+ # @return [Integer] the version of the prelink format
833
+ attr_reader :prelink_version
834
+
835
+ # @return [void]
836
+ attr_reader :reserved
837
+
838
+ # @return [void]
839
+ attr_reader :platform_name
840
+
841
+ # @return [void]
842
+ attr_reader :root_path
843
+
844
+ # @see MachOStructure::FORMAT
845
+ # @api private
846
+ FORMAT = "L>6a40a64a256"
847
+
848
+ # @see MachOStructure::SIZEOF
849
+ # @api private
850
+ SIZEOF = 384
851
+
852
+ # @api private
853
+ def initialize(signature, compress_type, adler32, uncompressed_size, compressed_size, prelink_version, reserved, platform_name, root_path)
854
+ super()
855
+
856
+ @signature = signature
857
+ @compress_type = compress_type
858
+ @adler32 = adler32
859
+ @uncompressed_size = uncompressed_size
860
+ @compressed_size = compressed_size
861
+ @prelink_version = prelink_version
862
+ @reserved = reserved.unpack("L>10")
863
+ @platform_name = platform_name
864
+ @root_path = root_path
865
+ end
866
+
867
+ # @return [Boolean] whether this prelinked kernel supports KASLR
868
+ def kaslr?
869
+ prelink_version >= 1
870
+ end
871
+
872
+ # @return [Boolean] whether this prelinked kernel is compressed with LZSS
873
+ def lzss?
874
+ compress_type == COMP_TYPE_LZSS
875
+ end
876
+
877
+ # @return [Boolean] whether this prelinked kernel is compressed with LZVN
878
+ def lzvn?
879
+ compress_type == COMP_TYPE_FASTLIB
880
+ end
881
+
882
+ # @return [Hash] a hash representation of this {PrelinkedKernelHeader}
883
+ def to_h
884
+ {
885
+ "signature" => signature,
886
+ "compress_type" => compress_type,
887
+ "adler32" => adler32,
888
+ "uncompressed_size" => uncompressed_size,
889
+ "compressed_size" => compressed_size,
890
+ "prelink_version" => prelink_version,
891
+ "reserved" => reserved,
892
+ "platform_name" => platform_name,
893
+ "root_path" => root_path,
894
+ }.merge super
895
+ end
896
+ end
788
897
  end
789
898
  end
@@ -63,7 +63,8 @@ module MachO
63
63
  0x31 => :LC_NOTE,
64
64
  0x32 => :LC_BUILD_VERSION,
65
65
  (0x33 | LC_REQ_DYLD) => :LC_DYLD_EXPORTS_TRIE,
66
- (0x34 | LC_REQ_DYLD) => :LD_DYLD_CHAINED_FIXUPS,
66
+ (0x34 | LC_REQ_DYLD) => :LC_DYLD_CHAINED_FIXUPS,
67
+ (0x35 | LC_REQ_DYLD) => :LC_FILESET_ENTRY,
67
68
  }.freeze
68
69
 
69
70
  # association of symbol representations to load command constants
@@ -150,7 +151,8 @@ module MachO
150
151
  :LC_NOTE => "NoteCommand",
151
152
  :LC_BUILD_VERSION => "BuildVersionCommand",
152
153
  :LC_DYLD_EXPORTS_TRIE => "LinkeditDataCommand",
153
- :LD_DYLD_CHAINED_FIXUPS => "LinkeditDataCommand",
154
+ :LC_DYLD_CHAINED_FIXUPS => "LinkeditDataCommand",
155
+ :LC_FILESET_ENTRY => "FilesetEntryCommand",
154
156
  }.freeze
155
157
 
156
158
  # association of segment name symbols to names
@@ -173,6 +175,7 @@ module MachO
173
175
  :SG_FVMLIB => 0x2,
174
176
  :SG_NORELOC => 0x4,
175
177
  :SG_PROTECTED_VERSION_1 => 0x8,
178
+ :SG_READ_ONLY => 0x10,
176
179
  }.freeze
177
180
 
178
181
  # The top-level Mach-O load command structure.
@@ -1794,5 +1797,48 @@ module MachO
1794
1797
  }.merge super
1795
1798
  end
1796
1799
  end
1800
+
1801
+ # A load command containing a description of a Mach-O that is a constituent of a fileset.
1802
+ # Each entry is further described by its own Mach header.
1803
+ # Corresponds to LC_FILESET_ENTRY.
1804
+ class FilesetEntryCommand < LoadCommand
1805
+ # @return [Integer] the virtual memory address of the entry
1806
+ attr_reader :vmaddr
1807
+
1808
+ # @return [Integer] the file offset of the entry
1809
+ attr_reader :fileoff
1810
+
1811
+ # @return [LCStr] the entry's ID
1812
+ attr_reader :entry_id
1813
+
1814
+ # @return [void]
1815
+ attr_reader :reserved
1816
+
1817
+ # @see MachOStructure::FORMAT
1818
+ # @api private
1819
+ FORMAT = "L=2Q=2L=2"
1820
+
1821
+ # @see MachOStructure::SIZEOF
1822
+ # @api private
1823
+ SIZEOF = 28
1824
+
1825
+ def initialize(view, cmd, cmdsize, vmaddr, fileoff, entry_id, reserved)
1826
+ super(view, cmd, cmdsize)
1827
+ @vmaddr = vmaddr
1828
+ @fileoff = fileoff
1829
+ @entry_id = LCStr.new(self, entry_id)
1830
+ @reserved = reserved
1831
+ end
1832
+
1833
+ # @return [Hash] a hash representation of this {FilesetEntryCommand}
1834
+ def to_h
1835
+ {
1836
+ "vmaddr" => vmaddr,
1837
+ "fileoff" => fileoff,
1838
+ "entry_id" => entry_id,
1839
+ "reserved" => reserved,
1840
+ }.merge super
1841
+ end
1842
+ end
1797
1843
  end
1798
1844
  end
@@ -34,7 +34,11 @@ module MachO
34
34
  # @param bin [String] a binary string containing raw Mach-O data
35
35
  # @param opts [Hash] options to control the parser with
36
36
  # @option opts [Boolean] :permissive whether to ignore unknown load commands
37
+ # @option opts [Boolean] :decompress whether to decompress, if capable
37
38
  # @return [MachOFile] a new MachOFile
39
+ # @note The `:decompress` option relies on non-default dependencies. Compression
40
+ # is only used in niche Mach-Os, so leaving this disabled is a reasonable default for
41
+ # virtually all normal uses.
38
42
  def self.new_from_bin(bin, **opts)
39
43
  instance = allocate
40
44
  instance.initialize_from_bin(bin, opts)
@@ -46,13 +50,17 @@ module MachO
46
50
  # @param filename [String] the Mach-O file to load from
47
51
  # @param opts [Hash] options to control the parser with
48
52
  # @option opts [Boolean] :permissive whether to ignore unknown load commands
53
+ # @option opts [Boolean] :decompress whether to decompress, if capable
49
54
  # @raise [ArgumentError] if the given file does not exist
55
+ # @note The `:decompress` option relies on non-default dependencies. Compression
56
+ # is only used in niche Mach-Os, so leaving this disabled is a reasonable default for
57
+ # virtually all normal uses.
50
58
  def initialize(filename, **opts)
51
59
  raise ArgumentError, "#{filename}: no such file" unless File.file?(filename)
52
60
 
53
61
  @filename = filename
54
62
  @options = opts
55
- @raw_data = File.open(@filename, "rb", &:read)
63
+ @raw_data = File.binread(@filename)
56
64
  populate_fields
57
65
  end
58
66
 
@@ -152,8 +160,8 @@ module MachO
152
160
  # the instance fields
153
161
  # @raise [OffsetInsertionError] if the offset is not in the load command region
154
162
  # @raise [HeaderPadError] if the new command exceeds the header pad buffer
155
- # @note Calling this method with an arbitrary offset in the load command
156
- # region **will leave the object in an inconsistent state**.
163
+ # @note Calling this method with an arbitrary offset in the load command region
164
+ # **will leave the object in an inconsistent state**.
157
165
  def insert_command(offset, lc, options = {})
158
166
  context = LoadCommands::LoadCommand::SerializationContext.context_for(self)
159
167
  cmd_raw = lc.serialize(context)
@@ -196,7 +204,7 @@ module MachO
196
204
  # Appends a new load command to the Mach-O.
197
205
  # @param lc [LoadCommands::LoadCommand] the load command being added
198
206
  # @param options [Hash]
199
- # @option options [Boolean] :repopulate (true) whether or not to repopulate
207
+ # @option f [Boolean] :repopulate (true) whether or not to repopulate
200
208
  # the instance fields
201
209
  # @return [void]
202
210
  # @see #insert_command
@@ -368,20 +376,20 @@ module MachO
368
376
  # file.change_rpath("/usr/lib", "/usr/local/lib")
369
377
  # @param old_path [String] the old runtime path
370
378
  # @param new_path [String] the new runtime path
371
- # @param _options [Hash]
379
+ # @param options [Hash]
380
+ # @option options [Boolean] :uniq (false) if true, change duplicate
381
+ # rpaths simultaneously.
372
382
  # @return [void]
373
383
  # @raise [RpathUnknownError] if no such old runtime path exists
374
384
  # @raise [RpathExistsError] if the new runtime path already exists
375
- # @note `_options` is currently unused and is provided for signature
376
- # compatibility with {MachO::FatFile#change_rpath}
377
- def change_rpath(old_path, new_path, _options = {})
385
+ def change_rpath(old_path, new_path, options = {})
378
386
  old_lc = command(:LC_RPATH).find { |r| r.path.to_s == old_path }
379
387
  raise RpathUnknownError, old_path if old_lc.nil?
380
388
  raise RpathExistsError, new_path if rpaths.include?(new_path)
381
389
 
382
390
  new_lc = LoadCommands::LoadCommand.create(:LC_RPATH, new_path)
383
391
 
384
- delete_rpath(old_path)
392
+ delete_rpath(old_path, options)
385
393
  insert_command(old_lc.view.offset, new_lc)
386
394
  end
387
395
 
@@ -409,13 +417,18 @@ module MachO
409
417
  # file.delete_rpath("/lib")
410
418
  # file.rpaths # => []
411
419
  # @param path [String] the runtime path to delete
412
- # @param _options [Hash]
420
+ # @param options [Hash]
421
+ # @option options [Boolean] :uniq (false) if true, also delete
422
+ # duplicates of the requested path. If false, delete the first
423
+ # instance (by offset) of the requested path.
413
424
  # @return void
414
425
  # @raise [RpathUnknownError] if no such runtime path exists
415
- # @note `_options` is currently unused and is provided for signature
416
- # compatibility with {MachO::FatFile#delete_rpath}
417
- def delete_rpath(path, _options = {})
418
- rpath_cmds = command(:LC_RPATH).select { |r| r.path.to_s == path }
426
+ def delete_rpath(path, options = {})
427
+ uniq = options.fetch(:uniq, false)
428
+ search_method = uniq ? :select : :find
429
+
430
+ # Cast rpath_cmds into an Array so we can handle the uniq and non-uniq cases the same way
431
+ rpath_cmds = Array(command(:LC_RPATH).method(search_method).call { |r| r.path.to_s == path })
419
432
  raise RpathUnknownError, path if rpath_cmds.empty?
420
433
 
421
434
  # delete the commands in reverse order, offset descending.
@@ -426,7 +439,7 @@ module MachO
426
439
  # @param filename [String] the file to write to
427
440
  # @return [void]
428
441
  def write(filename)
429
- File.open(filename, "wb") { |f| f.write(@raw_data) }
442
+ File.binwrite(filename, @raw_data)
430
443
  end
431
444
 
432
445
  # Write all Mach-O data to the file used to initialize the instance.
@@ -436,7 +449,7 @@ module MachO
436
449
  def write!
437
450
  raise MachOError, "no initial file to write to" if @filename.nil?
438
451
 
439
- File.open(@filename, "wb") { |f| f.write(@raw_data) }
452
+ File.binwrite(@filename, @raw_data)
440
453
  end
441
454
 
442
455
  # @return [Hash] a hash representation of this {MachOFile}
@@ -458,6 +471,9 @@ module MachO
458
471
  # the smallest Mach-O header is 28 bytes
459
472
  raise TruncatedFileError if @raw_data.size < 28
460
473
 
474
+ magic = @raw_data[0..3].unpack1("N")
475
+ populate_prelinked_kernel_header if Utils.compressed_magic?(magic)
476
+
461
477
  magic = populate_and_check_magic
462
478
  mh_klass = Utils.magic32?(magic) ? Headers::MachHeader : Headers::MachHeader64
463
479
  mh = mh_klass.new_from_bin(endianness, @raw_data[0, mh_klass.bytesize])
@@ -469,6 +485,48 @@ module MachO
469
485
  mh
470
486
  end
471
487
 
488
+ # Read a compressed Mach-O header and check its validity, as well as whether we're able
489
+ # to parse it.
490
+ # @return [void]
491
+ # @raise [CompressedMachOError] if we weren't asked to perform decompression
492
+ # @raise [DecompressionError] if decompression is impossible or fails
493
+ # @api private
494
+ def populate_prelinked_kernel_header
495
+ raise CompressedMachOError unless options.fetch(:decompress, false)
496
+
497
+ @plh = Headers::PrelinkedKernelHeader.new_from_bin :big, @raw_data[0, Headers::PrelinkedKernelHeader.bytesize]
498
+
499
+ raise DecompressionError, "unsupported compression type: LZSS" if @plh.lzss?
500
+ raise DecompressionError, "unknown compression type: 0x#{plh.compress_type.to_s 16}" unless @plh.lzvn?
501
+
502
+ decompress_macho_lzvn
503
+ end
504
+
505
+ # Attempt to decompress a Mach-O file from the data specified in a prelinked kernel header.
506
+ # @return [void]
507
+ # @raise [DecompressionError] if decompression is impossible or fails
508
+ # @api private
509
+ # @note This method rewrites the internal state of {MachOFile} to pretend as if it was never
510
+ # compressed to begin with, allowing all other APIs to transparently act on compressed Mach-Os.
511
+ def decompress_macho_lzvn
512
+ begin
513
+ require "lzfse"
514
+ rescue LoadError
515
+ raise DecompressionError, "LZVN required but the optional 'lzfse' gem is not installed"
516
+ end
517
+
518
+ # From this point onwards, the internal buffer of this MachOFile refers to the decompressed
519
+ # contents specified by the prelinked kernel header.
520
+ begin
521
+ @raw_data = LZFSE.lzvn_decompress @raw_data.slice(Headers::PrelinkedKernelHeader.bytesize, @plh.compressed_size)
522
+ # Sanity checks.
523
+ raise DecompressionError if @raw_data.size != @plh.uncompressed_size
524
+ # TODO: check the adler32 CRC in @plh
525
+ rescue LZFSE::DecodeError
526
+ raise DecompressionError, "LZVN decompression failed"
527
+ end
528
+ end
529
+
472
530
  # Read just the file's magic number and check its validity.
473
531
  # @return [Integer] the magic
474
532
  # @raise [MagicError] if the magic is not valid Mach-O magic
@@ -553,8 +611,8 @@ module MachO
553
611
  segments.each do |seg|
554
612
  seg.sections.each do |sect|
555
613
  next if sect.empty?
556
- next if sect.flag?(:S_ZEROFILL)
557
- next if sect.flag?(:S_THREAD_LOCAL_ZEROFILL)
614
+ next if sect.type?(:S_ZEROFILL)
615
+ next if sect.type?(:S_THREAD_LOCAL_ZEROFILL)
558
616
  next unless sect.offset < offset
559
617
 
560
618
  offset = sect.offset
@@ -4,24 +4,24 @@ module MachO
4
4
  # Classes and constants for parsing sections in Mach-O binaries.
5
5
  module Sections
6
6
  # type mask
7
- SECTION_TYPE = 0x000000ff
7
+ SECTION_TYPE_MASK = 0x000000ff
8
8
 
9
9
  # attributes mask
10
- SECTION_ATTRIBUTES = 0xffffff00
10
+ SECTION_ATTRIBUTES_MASK = 0xffffff00
11
11
 
12
12
  # user settable attributes mask
13
- SECTION_ATTRIBUTES_USR = 0xff000000
13
+ SECTION_ATTRIBUTES_USR_MASK = 0xff000000
14
14
 
15
15
  # system settable attributes mask
16
- SECTION_ATTRIBUTES_SYS = 0x00ffff00
16
+ SECTION_ATTRIBUTES_SYS_MASK = 0x00ffff00
17
17
 
18
18
  # maximum specifiable section alignment, as a power of 2
19
19
  # @note see `MAXSECTALIGN` macro in `cctools/misc/lipo.c`
20
20
  MAX_SECT_ALIGN = 15
21
21
 
22
- # association of section flag symbols to values
22
+ # association of section type symbols to values
23
23
  # @api private
24
- SECTION_FLAGS = {
24
+ SECTION_TYPES = {
25
25
  :S_REGULAR => 0x0,
26
26
  :S_ZEROFILL => 0x1,
27
27
  :S_CSTRING_LITERALS => 0x2,
@@ -44,6 +44,12 @@ module MachO
44
44
  :S_THREAD_LOCAL_VARIABLES => 0x13,
45
45
  :S_THREAD_LOCAL_VARIABLE_POINTERS => 0x14,
46
46
  :S_THREAD_LOCAL_INIT_FUNCTION_POINTERS => 0x15,
47
+ :S_INIT_FUNC_OFFSETS => 0x16,
48
+ }.freeze
49
+
50
+ # association of section attribute symbols to values
51
+ # @api private
52
+ SECTION_ATTRIBUTES = {
47
53
  :S_ATTR_PURE_INSTRUCTIONS => 0x80000000,
48
54
  :S_ATTR_NO_TOC => 0x40000000,
49
55
  :S_ATTR_STRIP_STATIC_SYMS => 0x20000000,
@@ -56,6 +62,13 @@ module MachO
56
62
  :S_ATTR_LOC_RELOC => 0x00000100,
57
63
  }.freeze
58
64
 
65
+ # association of section flag symbols to values
66
+ # @api private
67
+ SECTION_FLAGS = {
68
+ **SECTION_TYPES,
69
+ **SECTION_ATTRIBUTES,
70
+ }.freeze
71
+
59
72
  # association of section name symbols to names
60
73
  # @api private
61
74
  SECTION_NAMES = {
@@ -147,6 +160,33 @@ module MachO
147
160
  size.zero?
148
161
  end
149
162
 
163
+ # @return [Integer] the raw numeric type of this section
164
+ def type
165
+ flags & SECTION_TYPE_MASK
166
+ end
167
+
168
+ # @example
169
+ # puts "this section is regular" if sect.type?(:S_REGULAR)
170
+ # @param type_sym [Symbol] a section type symbol
171
+ # @return [Boolean] whether this section is of the given type
172
+ def type?(type_sym)
173
+ type == SECTION_TYPES[type_sym]
174
+ end
175
+
176
+ # @return [Integer] the raw numeric attributes of this section
177
+ def attributes
178
+ flags & SECTION_ATTRIBUTES_MASK
179
+ end
180
+
181
+ # @example
182
+ # puts "pure instructions" if sect.attribute?(:S_ATTR_PURE_INSTRUCTIONS)
183
+ # @param attr_sym [Symbol] a section attribute symbol
184
+ # @return [Boolean] whether this section is of the given type
185
+ def attribute?(attr_sym)
186
+ !!(attributes & SECTION_ATTRIBUTES[attr_sym])
187
+ end
188
+
189
+ # @deprecated Use {#type?} or {#attribute?} instead.
150
190
  # @example
151
191
  # puts "this section is regular" if sect.flag?(:S_REGULAR)
152
192
  # @param flag [Symbol] a section flag symbol
data/lib/macho/tools.rb CHANGED
@@ -51,6 +51,8 @@ module MachO
51
51
  # @param options [Hash]
52
52
  # @option options [Boolean] :strict (true) whether or not to fail loudly
53
53
  # with an exception if the change cannot be performed
54
+ # @option options [Boolean] :uniq (false) whether or not to change duplicate
55
+ # rpaths simultaneously
54
56
  # @return [void]
55
57
  def self.change_rpath(filename, old_path, new_path, options = {})
56
58
  file = MachO.open(filename)
@@ -80,6 +82,8 @@ module MachO
80
82
  # @param options [Hash]
81
83
  # @option options [Boolean] :strict (true) whether or not to fail loudly
82
84
  # with an exception if the change cannot be performed
85
+ # @option options [Boolean] :uniq (false) whether or not to delete duplicate
86
+ # rpaths simultaneously
83
87
  # @return [void]
84
88
  def self.delete_rpath(filename, old_path, options = {})
85
89
  file = MachO.open(filename)
data/lib/macho/utils.rb CHANGED
@@ -121,5 +121,12 @@ module MachO
121
121
  def self.big_magic?(num)
122
122
  [Headers::MH_MAGIC, Headers::MH_MAGIC_64].include? num
123
123
  end
124
+
125
+ # Compares the given number to the known magic number for a compressed Mach-O slice.
126
+ # @param num [Integer] the number being checked
127
+ # @return [Boolean] whether `num` is a valid compressed header magic number
128
+ def self.compressed_magic?(num)
129
+ num == Headers::COMPRESSED_MAGIC
130
+ end
124
131
  end
125
132
  end
data/lib/macho.rb CHANGED
@@ -16,7 +16,7 @@ require_relative "macho/tools"
16
16
  # The primary namespace for ruby-macho.
17
17
  module MachO
18
18
  # release version
19
- VERSION = "2.5.1"
19
+ VERSION = "3.0.0"
20
20
 
21
21
  # Opens the given filename as a MachOFile or FatFile, depending on its magic.
22
22
  # @param filename [String] the file being opened
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-macho
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.1
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - William Woodruff
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-15 00:00:00.000000000 Z
11
+ date: 2022-01-11 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A library for viewing and manipulating Mach-O files in Ruby.
14
14
  email: william@yossarian.net
@@ -34,7 +34,7 @@ homepage: https://github.com/Homebrew/ruby-macho
34
34
  licenses:
35
35
  - MIT
36
36
  metadata: {}
37
- post_install_message:
37
+ post_install_message:
38
38
  rdoc_options: []
39
39
  require_paths:
40
40
  - lib
@@ -42,15 +42,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
42
42
  requirements:
43
43
  - - ">="
44
44
  - !ruby/object:Gem::Version
45
- version: '2.5'
45
+ version: '2.6'
46
46
  required_rubygems_version: !ruby/object:Gem::Requirement
47
47
  requirements:
48
48
  - - ">="
49
49
  - !ruby/object:Gem::Version
50
50
  version: '0'
51
51
  requirements: []
52
- rubygems_version: 3.0.3
53
- signing_key:
52
+ rubygems_version: 3.2.32
53
+ signing_key:
54
54
  specification_version: 4
55
55
  summary: ruby-macho - Mach-O file analyzer.
56
56
  test_files: []