ruby-macho 2.5.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []