ruby-macho 2.3.0 → 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: 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: []