ruby-macho 2.5.1 → 4.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.
@@ -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,18 @@ 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
- # @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 = {})
384
+ def change_rpath(old_path, new_path, options = {})
378
385
  old_lc = command(:LC_RPATH).find { |r| r.path.to_s == old_path }
379
386
  raise RpathUnknownError, old_path if old_lc.nil?
380
- raise RpathExistsError, new_path if rpaths.include?(new_path)
381
387
 
382
388
  new_lc = LoadCommands::LoadCommand.create(:LC_RPATH, new_path)
383
389
 
384
- delete_rpath(old_path)
390
+ delete_rpath(old_path, options)
385
391
  insert_command(old_lc.view.offset, new_lc)
386
392
  end
387
393
 
@@ -409,13 +415,27 @@ module MachO
409
415
  # file.delete_rpath("/lib")
410
416
  # file.rpaths # => []
411
417
  # @param path [String] the runtime path to delete
412
- # @param _options [Hash]
418
+ # @param options [Hash]
419
+ # @option options [Boolean] :uniq (false) if true, also delete
420
+ # duplicates of the requested path. If false, delete the first
421
+ # instance (by offset) of the requested path, unless :last is true.
422
+ # Incompatible with :last.
423
+ # @option options [Boolean] :last (false) if true, delete the last
424
+ # instance (by offset) of the requested path. Incompatible with :uniq.
413
425
  # @return void
414
426
  # @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 }
427
+ # @raise [ArgumentError] if both :uniq and :last are true
428
+ def delete_rpath(path, options = {})
429
+ uniq = options.fetch(:uniq, false)
430
+ last = options.fetch(:last, false)
431
+ raise ArgumentError, "Cannot set both :uniq and :last to true" if uniq && last
432
+
433
+ search_method = uniq || last ? :select : :find
434
+ rpath_cmds = command(:LC_RPATH).public_send(search_method) { |r| r.path.to_s == path }
435
+ rpath_cmds = rpath_cmds.last if last
436
+
437
+ # Cast rpath_cmds into an Array so we can handle the uniq and non-uniq cases the same way
438
+ rpath_cmds = Array(rpath_cmds)
419
439
  raise RpathUnknownError, path if rpath_cmds.empty?
420
440
 
421
441
  # delete the commands in reverse order, offset descending.
@@ -426,7 +446,7 @@ module MachO
426
446
  # @param filename [String] the file to write to
427
447
  # @return [void]
428
448
  def write(filename)
429
- File.open(filename, "wb") { |f| f.write(@raw_data) }
449
+ File.binwrite(filename, @raw_data)
430
450
  end
431
451
 
432
452
  # Write all Mach-O data to the file used to initialize the instance.
@@ -436,7 +456,7 @@ module MachO
436
456
  def write!
437
457
  raise MachOError, "no initial file to write to" if @filename.nil?
438
458
 
439
- File.open(@filename, "wb") { |f| f.write(@raw_data) }
459
+ File.binwrite(@filename, @raw_data)
440
460
  end
441
461
 
442
462
  # @return [Hash] a hash representation of this {MachOFile}
@@ -458,6 +478,9 @@ module MachO
458
478
  # the smallest Mach-O header is 28 bytes
459
479
  raise TruncatedFileError if @raw_data.size < 28
460
480
 
481
+ magic = @raw_data[0..3].unpack1("N")
482
+ populate_prelinked_kernel_header if Utils.compressed_magic?(magic)
483
+
461
484
  magic = populate_and_check_magic
462
485
  mh_klass = Utils.magic32?(magic) ? Headers::MachHeader : Headers::MachHeader64
463
486
  mh = mh_klass.new_from_bin(endianness, @raw_data[0, mh_klass.bytesize])
@@ -469,6 +492,48 @@ module MachO
469
492
  mh
470
493
  end
471
494
 
495
+ # Read a compressed Mach-O header and check its validity, as well as whether we're able
496
+ # to parse it.
497
+ # @return [void]
498
+ # @raise [CompressedMachOError] if we weren't asked to perform decompression
499
+ # @raise [DecompressionError] if decompression is impossible or fails
500
+ # @api private
501
+ def populate_prelinked_kernel_header
502
+ raise CompressedMachOError unless options.fetch(:decompress, false)
503
+
504
+ @plh = Headers::PrelinkedKernelHeader.new_from_bin :big, @raw_data[0, Headers::PrelinkedKernelHeader.bytesize]
505
+
506
+ raise DecompressionError, "unsupported compression type: LZSS" if @plh.lzss?
507
+ raise DecompressionError, "unknown compression type: 0x#{plh.compress_type.to_s 16}" unless @plh.lzvn?
508
+
509
+ decompress_macho_lzvn
510
+ end
511
+
512
+ # Attempt to decompress a Mach-O file from the data specified in a prelinked kernel header.
513
+ # @return [void]
514
+ # @raise [DecompressionError] if decompression is impossible or fails
515
+ # @api private
516
+ # @note This method rewrites the internal state of {MachOFile} to pretend as if it was never
517
+ # compressed to begin with, allowing all other APIs to transparently act on compressed Mach-Os.
518
+ def decompress_macho_lzvn
519
+ begin
520
+ require "lzfse"
521
+ rescue LoadError
522
+ raise DecompressionError, "LZVN required but the optional 'lzfse' gem is not installed"
523
+ end
524
+
525
+ # From this point onwards, the internal buffer of this MachOFile refers to the decompressed
526
+ # contents specified by the prelinked kernel header.
527
+ begin
528
+ @raw_data = LZFSE.lzvn_decompress @raw_data.slice(Headers::PrelinkedKernelHeader.bytesize, @plh.compressed_size)
529
+ # Sanity checks.
530
+ raise DecompressionError if @raw_data.size != @plh.uncompressed_size
531
+ # TODO: check the adler32 CRC in @plh
532
+ rescue LZFSE::DecodeError
533
+ raise DecompressionError, "LZVN decompression failed"
534
+ end
535
+ end
536
+
472
537
  # Read just the file's magic number and check its validity.
473
538
  # @return [Integer] the magic
474
539
  # @raise [MagicError] if the magic is not valid Mach-O magic
@@ -534,7 +599,7 @@ module MachO
534
599
  LoadCommands::LoadCommand
535
600
  end
536
601
 
537
- view = MachOView.new(@raw_data, endianness, offset)
602
+ view = MachOView.new(self, @raw_data, endianness, offset)
538
603
  command = klass.new_from_bin(view)
539
604
 
540
605
  load_commands << command
@@ -553,8 +618,8 @@ module MachO
553
618
  segments.each do |seg|
554
619
  seg.sections.each do |sect|
555
620
  next if sect.empty?
556
- next if sect.flag?(:S_ZEROFILL)
557
- next if sect.flag?(:S_THREAD_LOCAL_ZEROFILL)
621
+ next if sect.type?(:S_ZEROFILL)
622
+ next if sect.type?(:S_THREAD_LOCAL_ZEROFILL)
558
623
  next unless sect.offset < offset
559
624
 
560
625
  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 = {
@@ -76,61 +89,38 @@ module MachO
76
89
  # Represents a section of a segment for 32-bit architectures.
77
90
  class Section < MachOStructure
78
91
  # @return [String] the name of the section, including null pad bytes
79
- attr_reader :sectname
92
+ field :sectname, :string, :padding => :null, :size => 16
80
93
 
81
94
  # @return [String] the name of the segment's section, including null
82
95
  # pad bytes
83
- attr_reader :segname
96
+ field :segname, :string, :padding => :null, :size => 16
84
97
 
85
98
  # @return [Integer] the memory address of the section
86
- attr_reader :addr
99
+ field :addr, :uint32
87
100
 
88
101
  # @return [Integer] the size, in bytes, of the section
89
- attr_reader :size
102
+ field :size, :uint32
90
103
 
91
104
  # @return [Integer] the file offset of the section
92
- attr_reader :offset
105
+ field :offset, :uint32
93
106
 
94
107
  # @return [Integer] the section alignment (power of 2) of the section
95
- attr_reader :align
108
+ field :align, :uint32
96
109
 
97
110
  # @return [Integer] the file offset of the section's relocation entries
98
- attr_reader :reloff
111
+ field :reloff, :uint32
99
112
 
100
113
  # @return [Integer] the number of relocation entries
101
- attr_reader :nreloc
114
+ field :nreloc, :uint32
102
115
 
103
116
  # @return [Integer] flags for type and attributes of the section
104
- attr_reader :flags
117
+ field :flags, :uint32
105
118
 
106
119
  # @return [void] reserved (for offset or index)
107
- attr_reader :reserved1
120
+ field :reserved1, :uint32
108
121
 
109
122
  # @return [void] reserved (for count or sizeof)
110
- attr_reader :reserved2
111
-
112
- # @see MachOStructure::FORMAT
113
- FORMAT = "Z16Z16L=9"
114
-
115
- # @see MachOStructure::SIZEOF
116
- SIZEOF = 68
117
-
118
- # @api private
119
- def initialize(sectname, segname, addr, size, offset, align, reloff,
120
- nreloc, flags, reserved1, reserved2)
121
- super()
122
- @sectname = sectname
123
- @segname = segname
124
- @addr = addr
125
- @size = size
126
- @offset = offset
127
- @align = align
128
- @reloff = reloff
129
- @nreloc = nreloc
130
- @flags = flags
131
- @reserved1 = reserved1
132
- @reserved2 = reserved2
133
- end
123
+ field :reserved2, :uint32
134
124
 
135
125
  # @return [String] the section's name
136
126
  def section_name
@@ -147,6 +137,33 @@ module MachO
147
137
  size.zero?
148
138
  end
149
139
 
140
+ # @return [Integer] the raw numeric type of this section
141
+ def type
142
+ flags & SECTION_TYPE_MASK
143
+ end
144
+
145
+ # @example
146
+ # puts "this section is regular" if sect.type?(:S_REGULAR)
147
+ # @param type_sym [Symbol] a section type symbol
148
+ # @return [Boolean] whether this section is of the given type
149
+ def type?(type_sym)
150
+ type == SECTION_TYPES[type_sym]
151
+ end
152
+
153
+ # @return [Integer] the raw numeric attributes of this section
154
+ def attributes
155
+ flags & SECTION_ATTRIBUTES_MASK
156
+ end
157
+
158
+ # @example
159
+ # puts "pure instructions" if sect.attribute?(:S_ATTR_PURE_INSTRUCTIONS)
160
+ # @param attr_sym [Symbol] a section attribute symbol
161
+ # @return [Boolean] whether this section is of the given type
162
+ def attribute?(attr_sym)
163
+ !!(attributes & SECTION_ATTRIBUTES[attr_sym])
164
+ end
165
+
166
+ # @deprecated Use {#type?} or {#attribute?} instead.
150
167
  # @example
151
168
  # puts "this section is regular" if sect.flag?(:S_REGULAR)
152
169
  # @param flag [Symbol] a section flag symbol
@@ -179,22 +196,14 @@ module MachO
179
196
 
180
197
  # Represents a section of a segment for 64-bit architectures.
181
198
  class Section64 < Section
182
- # @return [void] reserved
183
- attr_reader :reserved3
184
-
185
- # @see MachOStructure::FORMAT
186
- FORMAT = "Z16Z16Q=2L=8"
199
+ # @return [Integer] the memory address of the section
200
+ field :addr, :uint64
187
201
 
188
- # @see MachOStructure::SIZEOF
189
- SIZEOF = 80
202
+ # @return [Integer] the size, in bytes, of the section
203
+ field :size, :uint64
190
204
 
191
- # @api private
192
- def initialize(sectname, segname, addr, size, offset, align, reloff,
193
- nreloc, flags, reserved1, reserved2, reserved3)
194
- super(sectname, segname, addr, size, offset, align, reloff,
195
- nreloc, flags, reserved1, reserved2)
196
- @reserved3 = reserved3
197
- end
205
+ # @return [void] reserved
206
+ field :reserved3, :uint32
198
207
 
199
208
  # @return [Hash] a hash representation of this {Section64}
200
209
  def to_h