ruby-macho 2.3.0 → 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: 73c21b87da2cf6cdac1c2123d846fa6fd8eb2863c4fe87d0170fa6162d110329
4
- data.tar.gz: 0a59a2d38d59665eee474aad3f394aa54f1f2da5926c01b65824a75992da3e61
3
+ metadata.gz: ba2fc7b81345cb788c6e1de15bace8d35f7ac1f9a17b6b6a12289a4e51b978de
4
+ data.tar.gz: 395dd63235823f33f11463d395b790b1cee2003ab430bb54326c03ca3de402b8
5
5
  SHA512:
6
- metadata.gz: ac41936e243431a8346059b40e32fa6ed33c7f9f1f9dbbb9efcd7d1391d40931a6bdc33bf8fb5f676fb87af305cb92d18df9fe47151724cfb141996db47a2bd5
7
- data.tar.gz: 4feeb4b940e38cb0fe641b28ae08dff26a17d213e6c8602442912ce59d7de756618adbd36feac62846ee1b8f0aa0bb2f0a765f5b74fe462b371954184eaa65bc
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
@@ -9,6 +9,11 @@ module MachO
9
9
  class ModificationError < MachOError
10
10
  end
11
11
 
12
+ # Raised when codesigning fails. Certain environments
13
+ # may want to rescue this to treat it as non-fatal.
14
+ class CodeSigningError < MachOError
15
+ end
16
+
12
17
  # Raised when a Mach-O file modification fails but can be recovered when
13
18
  # operating on multiple Mach-O slices of a fat binary in non-strict mode.
14
19
  class RecoverableModificationError < ModificationError
@@ -27,10 +32,6 @@ module MachO
27
32
 
28
33
  # Raised when a file is not a Mach-O.
29
34
  class NotAMachOError < MachOError
30
- # @param error [String] the error in question
31
- def initialize(error)
32
- super error
33
- end
34
35
  end
35
36
 
36
37
  # Raised when a file is too short to be a valid Mach-O file.
@@ -83,7 +84,7 @@ module MachO
83
84
  # @param cpusubtype [Integer] the CPU sub-type of the unknown pair
84
85
  def initialize(cputype, cpusubtype)
85
86
  super "Unrecognized CPU sub-type: 0x%08<cpusubtype>x" \
86
- " (for CPU type: 0x%08<cputype>x" % { :cputype => cputype, :cpusubtype => cpusubtype }
87
+ " (for CPU type: 0x%08<cputype>x" % { :cputype => cputype, :cpusubtype => cpusubtype }
87
88
  end
88
89
  end
89
90
 
@@ -119,7 +120,7 @@ module MachO
119
120
  # @param actual_arity [Integer] the number of arguments received
120
121
  def initialize(cmd_sym, expected_arity, actual_arity)
121
122
  super "Expected #{expected_arity} arguments for #{cmd_sym} creation," \
122
- " got #{actual_arity}"
123
+ " got #{actual_arity}"
123
124
  end
124
125
  end
125
126
 
@@ -136,7 +137,7 @@ module MachO
136
137
  # @param lc [MachO::LoadCommand] the load command containing the string
137
138
  def initialize(lc)
138
139
  super "Load command #{lc.type} at offset #{lc.view.offset} contains a" \
139
- " malformed string"
140
+ " malformed string"
140
141
  end
141
142
  end
142
143
 
@@ -153,8 +154,8 @@ module MachO
153
154
  # @param filename [String] the filename
154
155
  def initialize(filename)
155
156
  super "Updated load commands do not fit in the header of " \
156
- "#{filename}. #{filename} needs to be relinked, possibly with " \
157
- "-headerpad or -headerpad_max_install_names"
157
+ "#{filename}. #{filename} needs to be relinked, possibly with " \
158
+ "-headerpad or -headerpad_max_install_names"
158
159
  end
159
160
  end
160
161
 
@@ -206,4 +207,14 @@ module MachO
206
207
  " Consider merging with `fat64: true`"
207
208
  end
208
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
209
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
 
@@ -66,7 +66,7 @@ module MachO
66
66
  offset += (macho.serialize.bytesize + macho_pads[macho])
67
67
  end
68
68
 
69
- machos.each do |macho|
69
+ machos.each do |macho| # rubocop:disable Style/CombinableLoops
70
70
  bin << Utils.nullpad(macho_pads[macho])
71
71
  bin << macho.serialize
72
72
  end
@@ -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}
@@ -398,16 +403,14 @@ module MachO
398
403
  errors = []
399
404
 
400
405
  machos.each_with_index do |macho, index|
401
- begin
402
- yield macho
403
- rescue RecoverableModificationError => e
404
- e.macho_slice = index
406
+ yield macho
407
+ rescue RecoverableModificationError => e
408
+ e.macho_slice = index
405
409
 
406
- # Strict mode: Immediately re-raise. Otherwise: Retain, check later.
407
- raise e if strict
410
+ # Strict mode: Immediately re-raise. Otherwise: Retain, check later.
411
+ raise e if strict
408
412
 
409
- errors << e
410
- end
413
+ errors << e
411
414
  end
412
415
 
413
416
  # Non-strict mode: Raise first error if *all* Mach-O slices failed.
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
@@ -500,6 +521,7 @@ module MachO
500
521
 
501
522
  # @api private
502
523
  def initialize(magic, nfat_arch)
524
+ super()
503
525
  @magic = magic
504
526
  @nfat_arch = nfat_arch
505
527
  end
@@ -551,6 +573,7 @@ module MachO
551
573
 
552
574
  # @api private
553
575
  def initialize(cputype, cpusubtype, offset, size, align)
576
+ super()
554
577
  @cputype = cputype
555
578
  @cpusubtype = cpusubtype & ~CPU_SUBTYPE_MASK
556
579
  @offset = offset
@@ -648,6 +671,7 @@ module MachO
648
671
  # @api private
649
672
  def initialize(magic, cputype, cpusubtype, filetype, ncmds, sizeofcmds,
650
673
  flags)
674
+ super()
651
675
  @magic = magic
652
676
  @cputype = cputype
653
677
  # For now we're not interested in additional capability bits also to be
@@ -721,6 +745,11 @@ module MachO
721
745
  filetype == Headers::MH_KEXT_BUNDLE
722
746
  end
723
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
+
724
753
  # @return [Boolean] true if the Mach-O has 32-bit magic, false otherwise
725
754
  def magic32?
726
755
  Utils.magic32?(magic)
@@ -782,5 +811,88 @@ module MachO
782
811
  }.merge super
783
812
  end
784
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
785
897
  end
786
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.
@@ -231,6 +234,7 @@ module MachO
231
234
  # @param cmdsize [Integer] the size of the load command in bytes
232
235
  # @api private
233
236
  def initialize(view, cmd, cmdsize)
237
+ super()
234
238
  @view = view
235
239
  @cmd = cmd
236
240
  @cmdsize = cmdsize
@@ -1793,5 +1797,48 @@ module MachO
1793
1797
  }.merge super
1794
1798
  end
1795
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
1796
1843
  end
1797
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,27 +417,29 @@ 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 }
419
- raise RpathUnknownError, path if rpath_cmds.empty?
426
+ def delete_rpath(path, options = {})
427
+ uniq = options.fetch(:uniq, false)
428
+ search_method = uniq ? :select : :find
420
429
 
421
- # delete the commands in reverse order, offset descending. this
422
- # allows us to defer (expensive) field population until the very end
423
- rpath_cmds.reverse_each { |cmd| delete_command(cmd, :repopulate => false) }
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 })
432
+ raise RpathUnknownError, path if rpath_cmds.empty?
424
433
 
425
- populate_fields
434
+ # delete the commands in reverse order, offset descending.
435
+ rpath_cmds.reverse_each { |cmd| delete_command(cmd) }
426
436
  end
427
437
 
428
438
  # Write all Mach-O data to the given filename.
429
439
  # @param filename [String] the file to write to
430
440
  # @return [void]
431
441
  def write(filename)
432
- File.open(filename, "wb") { |f| f.write(@raw_data) }
442
+ File.binwrite(filename, @raw_data)
433
443
  end
434
444
 
435
445
  # Write all Mach-O data to the file used to initialize the instance.
@@ -439,7 +449,7 @@ module MachO
439
449
  def write!
440
450
  raise MachOError, "no initial file to write to" if @filename.nil?
441
451
 
442
- File.open(@filename, "wb") { |f| f.write(@raw_data) }
452
+ File.binwrite(@filename, @raw_data)
443
453
  end
444
454
 
445
455
  # @return [Hash] a hash representation of this {MachOFile}
@@ -461,6 +471,9 @@ module MachO
461
471
  # the smallest Mach-O header is 28 bytes
462
472
  raise TruncatedFileError if @raw_data.size < 28
463
473
 
474
+ magic = @raw_data[0..3].unpack1("N")
475
+ populate_prelinked_kernel_header if Utils.compressed_magic?(magic)
476
+
464
477
  magic = populate_and_check_magic
465
478
  mh_klass = Utils.magic32?(magic) ? Headers::MachHeader : Headers::MachHeader64
466
479
  mh = mh_klass.new_from_bin(endianness, @raw_data[0, mh_klass.bytesize])
@@ -472,6 +485,48 @@ module MachO
472
485
  mh
473
486
  end
474
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
+
475
530
  # Read just the file's magic number and check its validity.
476
531
  # @return [Integer] the magic
477
532
  # @raise [MagicError] if the magic is not valid Mach-O magic
@@ -556,8 +611,8 @@ module MachO
556
611
  segments.each do |seg|
557
612
  seg.sections.each do |sect|
558
613
  next if sect.empty?
559
- next if sect.flag?(:S_ZEROFILL)
560
- next if sect.flag?(:S_THREAD_LOCAL_ZEROFILL)
614
+ next if sect.type?(:S_ZEROFILL)
615
+ next if sect.type?(:S_THREAD_LOCAL_ZEROFILL)
561
616
  next unless sect.offset < offset
562
617
 
563
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 = {
@@ -118,6 +131,7 @@ module MachO
118
131
  # @api private
119
132
  def initialize(sectname, segname, addr, size, offset, align, reloff,
120
133
  nreloc, flags, reserved1, reserved2)
134
+ super()
121
135
  @sectname = sectname
122
136
  @segname = segname
123
137
  @addr = addr
@@ -146,6 +160,33 @@ module MachO
146
160
  size.zero?
147
161
  end
148
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.
149
190
  # @example
150
191
  # puts "this section is regular" if sect.flag?(:S_REGULAR)
151
192
  # @param flag [Symbol] a section flag symbol
data/lib/macho/tools.rb CHANGED
@@ -25,8 +25,6 @@ module MachO
25
25
 
26
26
  file.change_dylib_id(new_id, options)
27
27
  file.write!
28
-
29
- MachO.codesign!(filename)
30
28
  end
31
29
 
32
30
  # Changes a shared library install name in a Mach-O or Fat binary,
@@ -43,8 +41,6 @@ module MachO
43
41
 
44
42
  file.change_install_name(old_name, new_name, options)
45
43
  file.write!
46
-
47
- MachO.codesign!(filename)
48
44
  end
49
45
 
50
46
  # Changes a runtime path in a Mach-O or Fat binary, overwriting the source
@@ -55,14 +51,14 @@ module MachO
55
51
  # @param options [Hash]
56
52
  # @option options [Boolean] :strict (true) whether or not to fail loudly
57
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
58
56
  # @return [void]
59
57
  def self.change_rpath(filename, old_path, new_path, options = {})
60
58
  file = MachO.open(filename)
61
59
 
62
60
  file.change_rpath(old_path, new_path, options)
63
61
  file.write!
64
-
65
- MachO.codesign!(filename)
66
62
  end
67
63
 
68
64
  # Add a runtime path to a Mach-O or Fat binary, overwriting the source file.
@@ -77,8 +73,6 @@ module MachO
77
73
 
78
74
  file.add_rpath(new_path, options)
79
75
  file.write!
80
-
81
- MachO.codesign!(filename)
82
76
  end
83
77
 
84
78
  # Delete a runtime path from a Mach-O or Fat binary, overwriting the source
@@ -88,14 +82,14 @@ module MachO
88
82
  # @param options [Hash]
89
83
  # @option options [Boolean] :strict (true) whether or not to fail loudly
90
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
91
87
  # @return [void]
92
88
  def self.delete_rpath(filename, old_path, options = {})
93
89
  file = MachO.open(filename)
94
90
 
95
91
  file.delete_rpath(old_path, options)
96
92
  file.write!
97
-
98
- MachO.codesign!(filename)
99
93
  end
100
94
 
101
95
  # Merge multiple Mach-Os into one universal (Fat) binary.
@@ -116,8 +110,6 @@ module MachO
116
110
 
117
111
  fat_macho = MachO::FatFile.new_from_machos(*machos, :fat64 => fat64)
118
112
  fat_macho.write(filename)
119
-
120
- MachO.codesign!(filename)
121
113
  end
122
114
  end
123
115
  end
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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "English"
3
+ require "open3"
4
+
4
5
  require_relative "macho/structure"
5
6
  require_relative "macho/view"
6
7
  require_relative "macho/headers"
@@ -15,7 +16,7 @@ require_relative "macho/tools"
15
16
  # The primary namespace for ruby-macho.
16
17
  module MachO
17
18
  # release version
18
- VERSION = "2.3.0"
19
+ VERSION = "3.0.0"
19
20
 
20
21
  # Opens the given filename as a MachOFile or FatFile, depending on its magic.
21
22
  # @param filename [String] the file being opened
@@ -48,14 +49,13 @@ module MachO
48
49
  # @return [void]
49
50
  # @raise [ModificationError] if the operation fails
50
51
  def self.codesign!(filename)
51
- # codesign binary is not available on Linux
52
- return if RUBY_PLATFORM !~ /darwin/
52
+ raise ArgumentError, "codesign binary is not available on Linux" if RUBY_PLATFORM !~ /darwin/
53
53
  raise ArgumentError, "#{filename}: no such file" unless File.file?(filename)
54
54
 
55
- system("codesign", "--sign", "-", "--force",
56
- "--preserve-metadata=entitlements,requirements,flags,runtime",
57
- filename)
55
+ _, _, status = Open3.capture3("codesign", "--sign", "-", "--force",
56
+ "--preserve-metadata=entitlements,requirements,flags,runtime",
57
+ filename)
58
58
 
59
- raise ModificationError, "#{filename}: signing failed!" unless $CHILD_STATUS.success?
59
+ raise CodeSigningError, "#{filename}: signing failed!" unless status.success?
60
60
  end
61
61
  end
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.3.0
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: 2020-10-12 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.3'
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.1.2
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: []