patchelf 0.1.0 → 1.3.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: d0268e11ae1f743826ab85b81245876d13ec987f4bf9b530f399260447b939aa
4
- data.tar.gz: f8fca7603e33bdea3d44c328076a64a37172d61c49d1fd7a7915fe5b4d9ddba3
3
+ metadata.gz: 8182cf70ee88eceed07b85e8edda947e647842fb0e1767b36c0bfc61651ae9a6
4
+ data.tar.gz: a739c96f21f48a4ae7036ba184e46e3ca6a6d3c1cc79215b0e8e4a5b4b970562
5
5
  SHA512:
6
- metadata.gz: 462df528984a4f51efb4f9b0f589b0378ff4b28a1008357c0f7b7b4cdaa4a41739402c3821cc6b06a1dccd7292e02c1f2e1d9513a7926e9708b56e0abc66ba84
7
- data.tar.gz: acddd987c408cf26dc323a6873d5db81aba3635ffba5e9eb6a3d4ca76f2d0d5cfe395739073f0594307274be756e5f270b162cbc952ac5d4797672b321ddeb5f
6
+ metadata.gz: ed2e115d7925aef19b3b192f0d9fe70744bd267fe3ef1e9b4245340e741bd6a3cf62bb82d6a847edd8c99878dfaf2e64b50e82ff1049be1f8e89881279fbd682
7
+ data.tar.gz: 02bfde530e73b448ec73932a1be1dd378bb44d74235fa9ab9096bf3c45607f0194ccfe3979857e4b4389004ee1cef8d4852b687fd571c4d4cfb291e57c576f8a
data/README.md CHANGED
@@ -134,4 +134,4 @@ patcher.save('ls.patch')
134
134
 
135
135
  ## Environment
136
136
 
137
- patchelf.rb is implemented in pure Ruby, so it should work in all environments include Linux, maxOS, and Windows!
137
+ patchelf.rb is implemented in pure Ruby, so it should work in all environments include Linux, macOS, and Windows!
@@ -0,0 +1,831 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'elftools/constants'
4
+ require 'elftools/elf_file'
5
+ require 'elftools/structs'
6
+ require 'elftools/util'
7
+ require 'fileutils'
8
+
9
+ require 'patchelf/helper'
10
+
11
+ #:nodoc:
12
+ module PatchELF
13
+ # TODO: refactor buf_* methods here
14
+ # TODO: move all refinements into a seperate file / helper file.
15
+ # refinements for cleaner syntax / speed / memory optimizations
16
+ module Refinements
17
+ refine StringIO do
18
+ # behaves like C memset. Equivalent to calling stream.write(char * nbytes)
19
+ # the benefit of preferring this over `stream.write(char * nbytes)` is only when data to be written is large.
20
+ # @param [String] char
21
+ # @param [Integer] nbytes
22
+ # @return[void]
23
+ def fill(char, nbytes)
24
+ at_once = Helper::PAGE_SIZE
25
+ pending = nbytes
26
+
27
+ if pending > at_once
28
+ to_write = char * at_once
29
+ while pending >= at_once
30
+ write(to_write)
31
+ pending -= at_once
32
+ end
33
+ end
34
+ write(char * pending) if pending.positive?
35
+ end
36
+ end
37
+ end
38
+ using Refinements
39
+
40
+ # Internal use only.
41
+ # alternative to +Saver+, that aims to be byte to byte equivalent with NixOS/patchelf.
42
+ #
43
+ # *DISCLAIMER*: This differs from +Saver+ in number of ways. No lazy reading,
44
+ # inconsistent use of existing internal API(e.g: manual reading of data instead of calling +section.data+)
45
+ # @private
46
+ class AltSaver
47
+ attr_reader :in_file # @return [String] Input filename.
48
+ attr_reader :out_file # @return [String] Output filename.
49
+
50
+ # Instantiate a {AltSaver} object.
51
+ # the params passed are the same as the ones passed to +Saver+
52
+ # @param [String] in_file
53
+ # @param [String] out_file
54
+ # @param [{Symbol => String, Array}] set
55
+ def initialize(in_file, out_file, set)
56
+ @in_file = in_file
57
+ @out_file = out_file
58
+ @set = set
59
+
60
+ f = File.open(in_file, 'rb')
61
+ # the +@buffer+ and +@elf+ both could work on same +StringIO+ stream,
62
+ # the updating of @buffer in place blocks us from looking up old values.
63
+ # TODO: cache the values needed later, use same stream for +@buffer+ and +@elf+.
64
+ # also be sure to update the stream offset passed to Segments::Segment.
65
+ @elf = ELFTools::ELFFile.new(f)
66
+ @buffer = StringIO.new(f.tap(&:rewind).read) # StringIO makes easier to work with Bindata
67
+
68
+ @ehdr = @elf.header
69
+ @endian = @elf.endian
70
+ @elf_class = @elf.elf_class
71
+
72
+ @segments = @elf.segments # usage similar to phdrs
73
+ @sections = @elf.sections # usage similar to shdrs
74
+ update_section_idx!
75
+
76
+ # {String => String}
77
+ # section name to its data mapping
78
+ @replaced_sections = {}
79
+ @section_alignment = ehdr.e_phoff.num_bytes
80
+
81
+ # using the same environment flag as patchelf, makes it easier for debugging
82
+ Logger.level = ::Logger.const_get(ENV['PATCHELF_DEBUG'] ? :DEBUG : :WARN)
83
+ end
84
+
85
+ # @return [void]
86
+ def save!
87
+ @set.each { |mtd, val| send(:"modify_#{mtd}") if val }
88
+ rewrite_sections
89
+
90
+ FileUtils.cp(in_file, out_file) if out_file != in_file
91
+ patch_out
92
+ # Let output file have the same permission as input.
93
+ FileUtils.chmod(File.stat(in_file).mode, out_file)
94
+ end
95
+
96
+ private
97
+
98
+ attr_reader :ehdr, :endian, :elf_class
99
+
100
+ def old_sections
101
+ @old_sections ||= @elf.sections
102
+ end
103
+
104
+ def buf_cstr(off)
105
+ cstr = []
106
+ with_buf_at(off) do |buf|
107
+ loop do
108
+ c = buf.read 1
109
+ break if c.nil? || c == "\x00"
110
+
111
+ cstr.push c
112
+ end
113
+ end
114
+ cstr.join
115
+ end
116
+
117
+ def buf_move!(dst_idx, src_idx, n_bytes)
118
+ with_buf_at(src_idx) do |buf|
119
+ to_write = buf.read(n_bytes)
120
+ buf.seek dst_idx
121
+ buf.write to_write
122
+ end
123
+ end
124
+
125
+ def dynstr
126
+ find_section '.dynstr'
127
+ end
128
+
129
+ # yields dynamic tag, and offset in buffer
130
+ def each_dynamic_tags
131
+ return unless block_given?
132
+
133
+ sec = find_section '.dynamic'
134
+ return unless sec
135
+
136
+ shdr = sec.header
137
+ with_buf_at(shdr.sh_offset) do |buf|
138
+ dyn = ELFTools::Structs::ELF_Dyn.new(elf_class: elf_class, endian: endian)
139
+ loop do
140
+ buf_dyn_offset = buf.tell
141
+ dyn.clear
142
+ dyn.read(buf)
143
+ break if dyn.d_tag == ELFTools::Constants::DT_NULL
144
+
145
+ yield dyn, buf_dyn_offset
146
+ # there's a possibility for caller to modify @buffer.pos, seek to avoid such issues
147
+ buf.seek buf_dyn_offset + dyn.num_bytes
148
+ end
149
+ end
150
+ end
151
+
152
+ # the idea of uniquely identifying section by its name has its problems
153
+ # but this is how patchelf operates and is prone to bugs.
154
+ # e.g: https://github.com/NixOS/patchelf/issues/197
155
+ def find_section(sec_name)
156
+ idx = find_section_idx sec_name
157
+ return unless idx
158
+
159
+ @sections[idx]
160
+ end
161
+
162
+ def find_section_idx(sec_name)
163
+ @section_idx_by_name[sec_name]
164
+ end
165
+
166
+ def buf_grow!(newsz)
167
+ bufsz = @buffer.size
168
+ return if newsz <= bufsz
169
+
170
+ @buffer.truncate newsz
171
+ end
172
+
173
+ def modify_interpreter
174
+ @replaced_sections['.interp'] = @set[:interpreter] + "\x00"
175
+ end
176
+
177
+ def modify_needed
178
+ # due to gsoc time constraints only implmenting features used by brew.
179
+ raise NotImplementedError
180
+ end
181
+
182
+ # not checking for nil as modify_rpath is only called if @set[:rpath]
183
+ def modify_rpath
184
+ modify_rpath_helper @set[:rpath], force_rpath: true
185
+ end
186
+
187
+ # not checking for nil as modify_runpath is only called if @set[:runpath]
188
+ def modify_runpath
189
+ modify_rpath_helper @set[:runpath]
190
+ end
191
+
192
+ def collect_runpath_tags
193
+ tags = {}
194
+ each_dynamic_tags do |dyn, off|
195
+ case dyn.d_tag
196
+ when ELFTools::Constants::DT_RPATH
197
+ tag_type = :rpath
198
+ when ELFTools::Constants::DT_RUNPATH
199
+ tag_type = :runpath
200
+ else
201
+ next
202
+ end
203
+
204
+ # clone does shallow copy, and for some reason d_tag and d_val can't be pass as argument
205
+ dyn_rpath = ELFTools::Structs::ELF_Dyn.new(endian: endian, elf_class: elf_class)
206
+ dyn_rpath.assign({ d_tag: dyn.d_tag.to_i, d_val: dyn.d_val.to_i })
207
+ tags[tag_type] = { offset: off, header: dyn_rpath }
208
+ end
209
+ tags
210
+ end
211
+
212
+ def resolve_rpath_tag_conflict(dyn_tags, force_rpath: false)
213
+ dyn_runpath, dyn_rpath = dyn_tags.values_at(:runpath, :rpath)
214
+
215
+ update_sym =
216
+ if !force_rpath && dyn_rpath && dyn_runpath.nil?
217
+ :runpath
218
+ elsif force_rpath && dyn_runpath
219
+ :rpath
220
+ end
221
+ return unless update_sym
222
+
223
+ delete_sym, = %i[rpath runpath] - [update_sym]
224
+ dyn_tag = dyn_tags[update_sym] = dyn_tags[delete_sym]
225
+ dyn = dyn_tag[:header]
226
+ dyn.d_tag = ELFTools::Constants.const_get("DT_#{update_sym.upcase}")
227
+ with_buf_at(dyn_tag[:offset]) { |buf| dyn.write(buf) }
228
+ dyn_tags.delete(delete_sym)
229
+ end
230
+
231
+ def modify_rpath_helper(new_rpath, force_rpath: false)
232
+ shdr_dynstr = dynstr.header
233
+
234
+ dyn_tags = collect_runpath_tags
235
+ resolve_rpath_tag_conflict(dyn_tags, force_rpath: force_rpath)
236
+ # (:runpath, :rpath) order_matters.
237
+ resolved_rpath_dyns = dyn_tags.values_at(:runpath, :rpath).compact
238
+
239
+ old_rpath = ''
240
+ rpath_off = nil
241
+ resolved_rpath_dyns.each do |dyn|
242
+ rpath_off = shdr_dynstr.sh_offset + dyn[:header].d_val
243
+ old_rpath = buf_cstr(rpath_off)
244
+ break
245
+ end
246
+ return if old_rpath == new_rpath
247
+
248
+ with_buf_at(rpath_off) { |b| b.write('X' * old_rpath.size) } if rpath_off
249
+ if new_rpath.size <= old_rpath.size
250
+ with_buf_at(rpath_off) { |b| b.write "#{new_rpath}\x00" }
251
+ return
252
+ end
253
+
254
+ Logger.debug 'rpath is too long, resizing...'
255
+ new_dynstr = replace_section '.dynstr', shdr_dynstr.sh_size + new_rpath.size + 1
256
+ new_rpath_strtab_idx = shdr_dynstr.sh_size.to_i
257
+ new_dynstr[new_rpath_strtab_idx..(new_rpath_strtab_idx + new_rpath.size)] = "#{new_rpath}\x00"
258
+
259
+ dyn_tags.each do |_, dyn|
260
+ dyn[:header].d_val = new_rpath_strtab_idx
261
+ with_buf_at(dyn[:offset]) { |b| dyn[:header].write(b) }
262
+ end
263
+
264
+ return unless dyn_tags.empty?
265
+
266
+ add_dt_rpath!(
267
+ d_tag: force_rpath ? ELFTools::Constants::DT_RPATH : ELFTools::Constants::DT_RUNPATH,
268
+ d_val: new_rpath_strtab_idx
269
+ )
270
+ end
271
+
272
+ def modify_soname
273
+ return unless ehdr.e_type == ELFTools::Constants::ET_DYN
274
+
275
+ # due to gsoc time constraints only implmenting features used by brew.
276
+ raise NotImplementedError
277
+ end
278
+
279
+ def add_segment!(**phdr_vals)
280
+ new_phdr = ELFTools::Structs::ELF_Phdr[elf_class].new(endian: endian, **phdr_vals)
281
+ # nil = no reference to stream; we only want @segments[i].header
282
+ new_segment = ELFTools::Segments::Segment.new(new_phdr, nil)
283
+ @segments.push new_segment
284
+ ehdr.e_phnum += 1
285
+ nil
286
+ end
287
+
288
+ def add_dt_rpath!(d_tag: nil, d_val: nil)
289
+ dyn_num_bytes = nil
290
+ dt_null_idx = 0
291
+ each_dynamic_tags do |dyn|
292
+ dyn_num_bytes ||= dyn.num_bytes
293
+ dt_null_idx += 1
294
+ end
295
+
296
+ # allot for new dt_runpath
297
+ shdr_dynamic = find_section('.dynamic').header
298
+ new_dynamic_data = replace_section '.dynamic', shdr_dynamic.sh_size + dyn_num_bytes
299
+
300
+ # consider DT_NULL when copying
301
+ replacement_size = (dt_null_idx + 1) * dyn_num_bytes
302
+
303
+ # make space for dt_runpath tag at the top, shift data by one tag positon
304
+ new_dynamic_data[dyn_num_bytes..(replacement_size + dyn_num_bytes)] = new_dynamic_data[0..replacement_size]
305
+
306
+ dyn_rpath = ELFTools::Structs::ELF_Dyn.new endian: endian, elf_class: elf_class
307
+ dyn_rpath.d_tag = d_tag
308
+ dyn_rpath.d_val = d_val
309
+
310
+ zi = StringIO.new
311
+ dyn_rpath.write zi
312
+ zi.rewind
313
+ new_dynamic_data[0...dyn_num_bytes] = zi.read
314
+ end
315
+
316
+ # given a index into old_sections table
317
+ # returns the corresponding section index in @sections
318
+ #
319
+ # raises ArgumentError if old_shndx can't be found in old_sections
320
+ # TODO: handle case of non existing section in (new) @sections.
321
+ def new_section_idx(old_shndx)
322
+ return if old_shndx == ELFTools::Constants::SHN_UNDEF || old_shndx >= ELFTools::Constants::SHN_LORESERVE
323
+
324
+ raise ArgumentError if old_shndx >= old_sections.count
325
+
326
+ old_sec = old_sections[old_shndx]
327
+ raise PatchError, "old_sections[#{shndx}] is nil" if old_sec.nil?
328
+
329
+ # TODO: handle case of non existing section in (new) @sections.
330
+ find_section_idx(old_sec.name)
331
+ end
332
+
333
+ def page_size
334
+ Helper::PAGE_SIZE
335
+ end
336
+
337
+ def patch_out
338
+ with_buf_at(0) { |b| ehdr.write(b) }
339
+
340
+ File.open(out_file, 'wb') do |f|
341
+ @buffer.rewind
342
+ f.write @buffer.read
343
+ end
344
+ end
345
+
346
+ # size includes NUL byte
347
+ def replace_section(section_name, size)
348
+ data = @replaced_sections[section_name]
349
+ unless data
350
+ shdr = find_section(section_name).header
351
+ # avoid calling +section.data+ as the @buffer contents may vary from
352
+ # the stream provided to section at initialization.
353
+ # ideally, calling section.data should work, however avoiding it to prevent
354
+ # future traps.
355
+ with_buf_at(shdr.sh_offset) { |b| data = b.read shdr.sh_size }
356
+ end
357
+ rep_data = if data.size == size
358
+ data
359
+ elsif data.size < size
360
+ data.ljust(size, "\x00")
361
+ else
362
+ data[0...size] + "\x00"
363
+ end
364
+ @replaced_sections[section_name] = rep_data
365
+ end
366
+
367
+ def write_phdrs_to_buf!
368
+ sort_phdrs!
369
+ with_buf_at(ehdr.e_phoff) do |buf|
370
+ @segments.each { |seg| seg.header.write(buf) }
371
+ end
372
+ end
373
+
374
+ def write_shdrs_to_buf!
375
+ raise PatchError, 'ehdr.e_shnum != @sections.count' if ehdr.e_shnum != @sections.count
376
+
377
+ sort_shdrs!
378
+ with_buf_at(ehdr.e_shoff) do |buf|
379
+ @sections.each { |section| section.header.write(buf) }
380
+ end
381
+ sync_dyn_tags!
382
+ end
383
+
384
+ # data for manual packing and unpacking of symbols in symtab sections.
385
+ def meta_sym_pack
386
+ return @meta_sym_pack if @meta_sym_pack
387
+
388
+ # resort to manual packing and unpacking of data,
389
+ # as using bindata is painfully slow :(
390
+ if elf_class == 32
391
+ sym_num_bytes = 16 # u32 u32 u32 u8 u8 u16
392
+ pack_code = endian == :little ? 'VVVCCv' : 'NNNCCn'
393
+ pack_st_info = 3
394
+ pack_st_shndx = 5
395
+ pack_st_value = 1
396
+ else # 64
397
+ sym_num_bytes = 24 # u32 u8 u8 u16 u64 u64
398
+ pack_code = endian == :little ? 'VCCvQ<Q<' : 'NCCnQ>Q>'
399
+ pack_st_info = 1
400
+ pack_st_shndx = 3
401
+ pack_st_value = 4
402
+ end
403
+
404
+ @meta_sym_pack = {
405
+ num_bytes: sym_num_bytes, code: pack_code,
406
+ st_info: pack_st_info, st_shndx: pack_st_shndx, st_value: pack_st_value
407
+ }
408
+ end
409
+
410
+ # yields +symbol+, +entry+
411
+ def each_symbol(shdr)
412
+ return unless [ELFTools::Constants::SHT_SYMTAB, ELFTools::Constants::SHT_DYNSYM].include?(shdr.sh_type)
413
+
414
+ pack_code, sym_num_bytes = meta_sym_pack.values_at(:code, :num_bytes)
415
+
416
+ with_buf_at(shdr.sh_offset) do |buf|
417
+ num_symbols = shdr.sh_size / sym_num_bytes
418
+ num_symbols.times do |entry|
419
+ sym = buf.read(sym_num_bytes).unpack(pack_code)
420
+ sym_modified = yield sym, entry
421
+
422
+ if sym_modified
423
+ buf.seek buf.tell - sym_num_bytes
424
+ buf.write sym.pack(pack_code)
425
+ end
426
+ end
427
+ end
428
+ end
429
+
430
+ def rewrite_headers(phdr_address)
431
+ # there can only be a single program header table according to ELF spec
432
+ @segments.find { |seg| seg.header.p_type == ELFTools::Constants::PT_PHDR }&.tap do |seg|
433
+ phdr = seg.header
434
+ phdr.p_offset = ehdr.e_phoff.to_i
435
+ phdr.p_vaddr = phdr.p_paddr = phdr_address.to_i
436
+ phdr.p_filesz = phdr.p_memsz = phdr.num_bytes * @segments.count # e_phentsize * e_phnum
437
+ end
438
+ write_phdrs_to_buf!
439
+ write_shdrs_to_buf!
440
+
441
+ pack = meta_sym_pack
442
+ @sections.each do |sec|
443
+ each_symbol(sec.header) do |sym, entry|
444
+ old_shndx = sym[pack[:st_shndx]]
445
+
446
+ begin
447
+ new_index = new_section_idx(old_shndx)
448
+ next unless new_index
449
+ rescue ArgumentError
450
+ Logger.warn "entry #{entry} in symbol table refers to a non existing section, skipping"
451
+ end
452
+
453
+ sym[pack[:st_shndx]] = new_index
454
+
455
+ # right 4 bits in the st_info field is st_type
456
+ if (sym[pack[:st_info]] & 0xF) == ELFTools::Constants::STT_SECTION
457
+ sym[pack[:st_value]] = @sections[new_index].header.sh_addr.to_i
458
+ end
459
+ true
460
+ end
461
+ end
462
+ end
463
+
464
+ def rewrite_sections
465
+ return if @replaced_sections.empty?
466
+
467
+ case ehdr.e_type
468
+ when ELFTools::Constants::ET_DYN
469
+ rewrite_sections_library
470
+ when ELFTools::Constants::ET_EXEC
471
+ rewrite_sections_executable
472
+ else
473
+ raise PatchError, 'unknown ELF type'
474
+ end
475
+ end
476
+
477
+ def replaced_section_indices
478
+ return enum_for(:replaced_section_indices) unless block_given?
479
+
480
+ last_replaced = 0
481
+ @sections.each_with_index do |sec, idx|
482
+ if @replaced_sections[sec.name]
483
+ last_replaced = idx
484
+ yield last_replaced
485
+ end
486
+ end
487
+ raise PatchError, 'last_replaced = 0' if last_replaced.zero?
488
+ raise PatchError, 'last_replaced + 1 >= @sections.size' if last_replaced + 1 >= @sections.size
489
+ end
490
+
491
+ def start_replacement_shdr
492
+ last_replaced = replaced_section_indices.max
493
+ start_replacement_hdr = @sections[last_replaced + 1].header
494
+
495
+ prev_sec_name = ''
496
+ (1..last_replaced).each do |idx|
497
+ sec = @sections[idx]
498
+ shdr = sec.header
499
+ if (sec.type == ELFTools::Constants::SHT_PROGBITS && sec.name != '.interp') || prev_sec_name == '.dynstr'
500
+ start_replacement_hdr = shdr
501
+ break
502
+ elsif @replaced_sections[sec.name].nil?
503
+ Logger.debug " replacing section #{sec.name} which is in the way"
504
+ replace_section(sec.name, shdr.sh_size)
505
+ end
506
+ prev_sec_name = sec.name
507
+ end
508
+
509
+ start_replacement_hdr
510
+ end
511
+
512
+ def copy_shdrs_to_eof
513
+ shoff_new = @buffer.size
514
+ # honestly idk why `ehdr.e_shoff` is considered when we are only moving shdrs.
515
+ sh_size = ehdr.e_shoff + ehdr.e_shnum * ehdr.e_shentsize
516
+ buf_grow! @buffer.size + sh_size
517
+ ehdr.e_shoff = shoff_new
518
+ raise PatchError, 'ehdr.e_shnum != @sections.size' if ehdr.e_shnum != @sections.size
519
+
520
+ with_buf_at(ehdr.e_shoff + @sections.first.header.num_bytes) do |buf| # skip writing to NULL section
521
+ @sections.each_with_index do |sec, idx|
522
+ next if idx.zero?
523
+
524
+ sec.header.write buf
525
+ end
526
+ end
527
+ end
528
+
529
+ def rewrite_sections_executable
530
+ sort_shdrs!
531
+ shdr = start_replacement_shdr
532
+ start_offset = shdr.sh_offset
533
+ start_addr = shdr.sh_addr
534
+ first_page = start_addr - start_offset
535
+
536
+ Logger.debug "first reserved offset/addr is 0x#{start_offset.to_i.to_s 16}/0x#{start_addr.to_i.to_s 16}"
537
+
538
+ unless start_addr % page_size == start_offset % page_size
539
+ raise PatchError, 'start_addr != start_offset (mod PAGE_SIZE)'
540
+ end
541
+
542
+ Logger.debug "first page is 0x#{first_page.to_i.to_s 16}"
543
+
544
+ copy_shdrs_to_eof if ehdr.e_shoff < start_offset
545
+
546
+ seg_num_bytes = @segments.first.header.num_bytes
547
+ needed_space = (
548
+ ehdr.num_bytes +
549
+ (@segments.count * seg_num_bytes) +
550
+ @replaced_sections.sum { |_, str| Helper.alignup(str.size, @section_alignment) }
551
+ )
552
+
553
+ if needed_space > start_offset
554
+ needed_space += seg_num_bytes # new load segment is required
555
+
556
+ needed_pages = Helper.alignup(needed_space - start_offset, page_size) / page_size
557
+ Logger.debug "needed pages is #{needed_pages}"
558
+ raise PatchError, 'virtual address space underrun' if needed_pages * page_size > first_page
559
+
560
+ first_page -= needed_pages * page_size
561
+ start_offset += needed_pages * page_size
562
+
563
+ shift_file(needed_pages, first_page)
564
+ end
565
+ Logger.debug "needed space is #{needed_space}"
566
+
567
+ cur_off = ehdr.num_bytes + (@segments.count * seg_num_bytes)
568
+ Logger.debug "clearing first #{start_offset - cur_off} bytes"
569
+ with_buf_at(cur_off) { |buf| buf.fill("\x00", (start_offset - cur_off)) }
570
+
571
+ cur_off = write_replaced_sections cur_off, first_page, 0
572
+ raise PatchError, "cur_off(#{cur_off}) != needed_space" if cur_off != needed_space
573
+
574
+ rewrite_headers first_page + ehdr.e_phoff
575
+ end
576
+
577
+ def replace_sections_in_the_way_of_phdr!
578
+ pht_size = ehdr.num_bytes + (@segments.count + 1) * @segments.first.header.num_bytes
579
+
580
+ # replace sections that may overlap with expanded program header table
581
+ @sections.each_with_index do |sec, idx|
582
+ shdr = sec.header
583
+ next if idx.zero? || @replaced_sections[sec.name]
584
+ break if shdr.sh_addr > pht_size
585
+
586
+ replace_section sec.name, shdr.sh_size
587
+ end
588
+ end
589
+
590
+ def seg_end_addr(seg)
591
+ phdr = seg.header
592
+ Helper.alignup(phdr.p_vaddr + phdr.p_memsz, page_size)
593
+ end
594
+
595
+ def rewrite_sections_library
596
+ start_page = seg_end_addr(@segments.max_by(&method(:seg_end_addr)))
597
+
598
+ Logger.debug "Last page is 0x#{start_page.to_s 16}"
599
+ replace_sections_in_the_way_of_phdr!
600
+ needed_space = @replaced_sections.sum { |_, str| Helper.alignup(str.size, @section_alignment) }
601
+ Logger.debug "needed space = #{needed_space}"
602
+
603
+ start_offset = Helper.alignup(@buffer.size, page_size)
604
+ buf_grow! start_offset + needed_space
605
+
606
+ # executable shared object
607
+ if start_offset > start_page && @segments.any? { |seg| seg.header.p_type == ELFTools::Constants::PT_INTERP }
608
+ Logger.debug(
609
+ "shifting new PT_LOAD segment by #{start_offset - start_page} bytes to work around a Linux kernel bug"
610
+ )
611
+ start_page = start_offset
612
+ end
613
+
614
+ ehdr.e_phoff = ehdr.num_bytes
615
+ add_segment!(
616
+ p_type: ELFTools::Constants::PT_LOAD,
617
+ p_offset: start_offset,
618
+ p_vaddr: start_page,
619
+ p_paddr: start_page,
620
+ p_filesz: needed_space,
621
+ p_memsz: needed_space,
622
+ p_flags: ELFTools::Constants::PF_R | ELFTools::Constants::PF_W,
623
+ p_align: page_size
624
+ )
625
+
626
+ cur_off = write_replaced_sections start_offset, start_page, start_offset
627
+ raise PatchError, 'cur_off != start_offset + needed_space' if cur_off != start_offset + needed_space
628
+
629
+ rewrite_headers ehdr.e_phoff
630
+ end
631
+
632
+ def shift_file(extra_pages, start_page)
633
+ oldsz = @buffer.size
634
+ shift = extra_pages * page_size
635
+ buf_grow!(oldsz + shift)
636
+ buf_move! shift, 0, oldsz
637
+ with_buf_at(ehdr.num_bytes) { |buf| buf.write "\x00" * (shift - ehdr.num_bytes) }
638
+
639
+ ehdr.e_phoff = ehdr.num_bytes
640
+ ehdr.e_shoff = ehdr.e_shoff + shift
641
+
642
+ @sections.each_with_index do |sec, i|
643
+ next if i.zero? # dont touch NULL section
644
+
645
+ shdr = sec.header
646
+ shdr.sh_offset += shift
647
+ end
648
+
649
+ @segments.each do |seg|
650
+ phdr = seg.header
651
+ phdr.p_offset += shift
652
+ phdr.p_align = page_size if phdr.p_align != 0 && (phdr.p_vaddr - phdr.p_offset) % phdr.p_align != 0
653
+ end
654
+
655
+ add_segment!(
656
+ p_type: ELFTools::Constants::PT_LOAD,
657
+ p_offset: 0,
658
+ p_vaddr: start_page,
659
+ p_paddr: start_page,
660
+ p_filesz: shift,
661
+ p_memsz: shift,
662
+ p_flags: ELFTools::Constants::PF_R | ELFTools::Constants::PF_W,
663
+ p_align: page_size
664
+ )
665
+ end
666
+
667
+ def sort_phdrs!
668
+ pt_phdr = ELFTools::Constants::PT_PHDR
669
+ @segments.sort! do |me, you|
670
+ next 1 if you.header.p_type == pt_phdr
671
+ next -1 if me.header.p_type == pt_phdr
672
+
673
+ me.header.p_paddr.to_i <=> you.header.p_paddr.to_i
674
+ end
675
+ end
676
+
677
+ # section headers may contain sh_info and sh_link values that are
678
+ # references to another section
679
+ def collect_section_to_section_refs
680
+ rel_syms = [ELFTools::Constants::SHT_REL, ELFTools::Constants::SHT_RELA]
681
+ # Translate sh_link, sh_info mappings to section names.
682
+ @sections.each_with_object({ linkage: {}, info: {} }) do |s, collected|
683
+ hdr = s.header
684
+ collected[:linkage][s.name] = @sections[hdr.sh_link].name if hdr.sh_link.nonzero?
685
+ collected[:info][s.name] = @sections[hdr.sh_info].name if hdr.sh_info.nonzero? && rel_syms.include?(hdr.sh_type)
686
+ end
687
+ end
688
+
689
+ # @param collected
690
+ # this must be the value returned by +collect_section_to_section_refs+
691
+ def restore_section_to_section_refs!(collected)
692
+ rel_syms = [ELFTools::Constants::SHT_REL, ELFTools::Constants::SHT_RELA]
693
+ linkage, info = collected.values_at(:linkage, :info)
694
+ @sections.each do |sec|
695
+ hdr = sec.header
696
+ hdr.sh_link = find_section_idx(linkage[sec.name]) if hdr.sh_link.nonzero?
697
+ hdr.sh_info = find_section_idx(info[sec.name]) if hdr.sh_info.nonzero? && rel_syms.include?(hdr.sh_type)
698
+ end
699
+ end
700
+
701
+ def sort_shdrs!
702
+ section_dep_values = collect_section_to_section_refs
703
+ shstrtab_name = @sections[ehdr.e_shstrndx].name
704
+ @sections.sort! { |me, you| me.header.sh_offset.to_i <=> you.header.sh_offset.to_i }
705
+ update_section_idx!
706
+ restore_section_to_section_refs!(section_dep_values)
707
+ ehdr.e_shstrndx = find_section_idx shstrtab_name
708
+ end
709
+
710
+ # given a +dyn.d_tag+, returns the section name it must be synced to.
711
+ # it may return nil, when given tag maps to no section,
712
+ # or when its okay to skip if section is not found.
713
+ def dyn_tag_to_section_name(d_tag)
714
+ case d_tag
715
+ when ELFTools::Constants::DT_STRTAB, ELFTools::Constants::DT_STRSZ
716
+ '.dynstr'
717
+ when ELFTools::Constants::DT_SYMTAB
718
+ '.dynsym'
719
+ when ELFTools::Constants::DT_HASH
720
+ '.hash'
721
+ when ELFTools::Constants::DT_GNU_HASH
722
+ '.gnu.hash'
723
+ when ELFTools::Constants::DT_JMPREL
724
+ sec_name = %w[.rel.plt .rela.plt .rela.IA_64.pltoff].find { |s| find_section(s) }
725
+ raise PatchError, 'cannot find section corresponding to DT_JMPREL' unless sec_name
726
+
727
+ sec_name
728
+ when ELFTools::Constants::DT_REL
729
+ # regarding .rel.got, NixOS/patchelf says
730
+ # "no idea if this makes sense, but it was needed for some program"
731
+ #
732
+ # return nil if not found, patchelf claims no problem in skipping
733
+ %w[.rel.dyn .rel.got].find { |s| find_section(s) }
734
+ when ELFTools::Constants::DT_RELA
735
+ # return nil if not found, patchelf claims no problem in skipping
736
+ find_section('.rela.dyn')&.name
737
+ when ELFTools::Constants::DT_VERNEED
738
+ '.gnu.version_r'
739
+ when ELFTools::Constants::DT_VERSYM
740
+ '.gnu.version'
741
+ end
742
+ end
743
+
744
+ # updates dyn tags by syncing it with @section values
745
+ def sync_dyn_tags!
746
+ each_dynamic_tags do |dyn, buf_off|
747
+ sec_name = dyn_tag_to_section_name(dyn.d_tag)
748
+ next unless sec_name
749
+
750
+ shdr = find_section(sec_name).header
751
+ dyn.d_val = dyn.d_tag == ELFTools::Constants::DT_STRSZ ? shdr.sh_size.to_i : shdr.sh_addr.to_i
752
+
753
+ with_buf_at(buf_off) { |wbuf| dyn.write(wbuf) }
754
+ end
755
+ end
756
+
757
+ def update_section_idx!
758
+ @section_idx_by_name = @sections.map.with_index { |sec, idx| [sec.name, idx] }.to_h
759
+ end
760
+
761
+ def with_buf_at(pos)
762
+ return unless block_given?
763
+
764
+ opos = @buffer.tell
765
+ @buffer.seek pos
766
+ yield @buffer
767
+ @buffer.seek opos
768
+ nil
769
+ end
770
+
771
+ def sync_sec_to_seg(shdr, phdr)
772
+ phdr.p_offset = shdr.sh_offset.to_i
773
+ phdr.p_vaddr = phdr.p_paddr = shdr.sh_addr.to_i
774
+ phdr.p_filesz = phdr.p_memsz = shdr.sh_size.to_i
775
+ end
776
+
777
+ def phdrs_by_type(seg_type)
778
+ return unless seg_type
779
+
780
+ @segments.each_with_index do |seg, idx|
781
+ next unless (phdr = seg.header).p_type == seg_type
782
+
783
+ yield phdr, idx
784
+ end
785
+ end
786
+
787
+ def write_replaced_sections(cur_off, start_addr, start_offset)
788
+ sht_no_bits = ELFTools::Constants::SHT_NOBITS
789
+
790
+ # the original source says this has to be done seperately to
791
+ # prevent clobbering the previously written section contents.
792
+ @replaced_sections.each do |rsec_name, _|
793
+ shdr = find_section(rsec_name).header
794
+ with_buf_at(shdr.sh_offset) { |b| b.fill('X', shdr.sh_size) } if shdr.sh_type != sht_no_bits
795
+ end
796
+
797
+ # the sort is necessary, the strategy in ruby and Cpp to iterate map/hash
798
+ # is different, patchelf v0.10 iterates the replaced_sections sorted by
799
+ # keys.
800
+ @replaced_sections.sort.each do |rsec_name, rsec_data|
801
+ section = find_section(rsec_name)
802
+ shdr = section.header
803
+
804
+ Logger.debug <<~DEBUG
805
+ rewriting section '#{rsec_name}'
806
+ from offset 0x#{shdr.sh_offset.to_i.to_s 16}(size #{shdr.sh_size})
807
+ to offset 0x#{cur_off.to_i.to_s 16}(size #{rsec_data.size})
808
+ DEBUG
809
+
810
+ with_buf_at(cur_off) { |b| b.write rsec_data }
811
+
812
+ shdr.sh_offset = cur_off
813
+ shdr.sh_addr = start_addr + (cur_off - start_offset)
814
+ shdr.sh_size = rsec_data.size
815
+ shdr.sh_addralign = @section_alignment
816
+
817
+ seg_type = {
818
+ '.interp' => ELFTools::Constants::PT_INTERP,
819
+ '.dynamic' => ELFTools::Constants::PT_DYNAMIC
820
+ }[section.name]
821
+
822
+ phdrs_by_type(seg_type) { |phdr| sync_sec_to_seg(shdr, phdr) }
823
+
824
+ cur_off += Helper.alignup(rsec_data.size, @section_alignment)
825
+ end
826
+ @replaced_sections.clear
827
+
828
+ cur_off
829
+ end
830
+ end
831
+ end
@@ -34,9 +34,24 @@ module PatchELF
34
34
  return $stdout.puts option_parser unless parse(argv)
35
35
 
36
36
  # Now the options are (hopefully) valid, let's process the ELF file.
37
- patcher = PatchELF::Patcher.new(@options[:in_file])
37
+ begin
38
+ @patcher = PatchELF::Patcher.new(@options[:in_file])
39
+ rescue ELFTools::ELFError, Errno::ENOENT => e
40
+ return PatchELF::Logger.error(e.message)
41
+ end
38
42
  patcher.use_rpath! if @options[:force_rpath]
39
- # TODO: Handle ELFTools::ELFError
43
+ readonly
44
+ patch_requests
45
+ patcher.save(@options[:out_file])
46
+ end
47
+
48
+ private
49
+
50
+ def patcher
51
+ @patcher
52
+ end
53
+
54
+ def readonly
40
55
  @options[:print].uniq.each do |s|
41
56
  content = patcher.__send__(s)
42
57
  next if content.nil?
@@ -44,7 +59,9 @@ module PatchELF
44
59
  s = :rpath if @options[:force_rpath] && s == :runpath
45
60
  $stdout.puts "#{s}: #{Array(content).join(' ')}"
46
61
  end
62
+ end
47
63
 
64
+ def patch_requests
48
65
  @options[:set].each do |sym, val|
49
66
  patcher.__send__("#{sym}=".to_sym, val)
50
67
  end
@@ -52,12 +69,8 @@ module PatchELF
52
69
  @options[:needed].each do |type, val|
53
70
  patcher.__send__("#{type}_needed".to_sym, *val)
54
71
  end
55
-
56
- patcher.save(@options[:out_file])
57
72
  end
58
73
 
59
- private
60
-
61
74
  def parse(argv)
62
75
  remain = option_parser.permute(argv)
63
76
  return false if remain.first.nil?
@@ -0,0 +1,13 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ require 'elftools/exceptions'
5
+
6
+ module PatchELF
7
+ # Raised on an error during ELF modification.
8
+ class PatchError < ELFTools::ELFError; end
9
+ # Raised when Dynamic Tag is missing
10
+ class MissingTagError < PatchError; end
11
+ # Raised on missing Program Header(segment)
12
+ class MissingSegmentError < PatchError; end
13
+ end
@@ -15,7 +15,7 @@ module PatchELF
15
15
  end
16
16
  end
17
17
 
18
- %i[info warn error].each do |sym|
18
+ %i[debug info warn error level=].each do |sym|
19
19
  define_method(sym) do |msg|
20
20
  @logger.__send__(sym, msg)
21
21
  nil
@@ -25,7 +25,8 @@ module PatchELF
25
25
  # 1. Set ELF headers' attributes (with ELFTools)
26
26
  # 2. Invoke {Saver#inline_patch}
27
27
  def malloc(size, &block)
28
- # TODO: check size > 0
28
+ raise ArgumentError, 'malloc\'s size most be positive.' if size <= 0
29
+
29
30
  @request << [size, block]
30
31
  end
31
32
 
@@ -133,6 +134,7 @@ module PatchELF
133
134
  nil
134
135
  end
135
136
 
137
+ # TODO
136
138
  def new_load_method
137
139
  raise NotImplementedError
138
140
  end
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'elftools/elf_file'
5
5
 
6
+ require 'patchelf/exceptions'
6
7
  require 'patchelf/logger'
7
8
  require 'patchelf/saver'
8
9
 
@@ -12,14 +13,27 @@ module PatchELF
12
13
  # @!macro [new] note_apply
13
14
  # @note This setting will be saved after {#save} being invoked.
14
15
 
16
+ attr_reader :elf # @return [ELFTools::ELFFile] ELF parser object.
17
+
15
18
  # Instantiate a {Patcher} object.
16
19
  # @param [String] filename
17
20
  # Filename of input ELF.
18
- def initialize(filename)
21
+ # @param [Boolean] logging
22
+ # *deprecated*: use +on_error+ instead
23
+ # @param [:log, :silent, :exception] on_error
24
+ # action when the desired segment/tag field isn't present
25
+ # :log = logs to stderr
26
+ # :exception = raise exception related to the error
27
+ # :silent = ignore the errors
28
+ def initialize(filename, on_error: :log, logging: true)
19
29
  @in_file = filename
20
30
  @elf = ELFTools::ELFFile.new(File.open(filename))
21
31
  @set = {}
22
32
  @rpath_sym = :runpath
33
+ @on_error = !logging ? :exception : on_error
34
+
35
+ on_error_syms = %i[exception log silent]
36
+ raise ArgumentError, "on_error must be one of #{on_error_syms}" unless on_error_syms.include?(@on_error)
23
37
  end
24
38
 
25
39
  # @return [String?]
@@ -120,7 +134,23 @@ module PatchELF
120
134
  # Get runpath.
121
135
  # @return [String?]
122
136
  def runpath
123
- @set[@rpath_sym] || runpath_
137
+ @set[@rpath_sym] || runpath_(@rpath_sym)
138
+ end
139
+
140
+ # Get rpath
141
+ # return [String?]
142
+ def rpath
143
+ @set[:rpath] || runpath_(:rpath)
144
+ end
145
+
146
+ # Set rpath
147
+ #
148
+ # Modify / set DT_RPATH of the given ELF.
149
+ # similar to runpath= except DT_RPATH is modifed/created in DYNAMIC segment.
150
+ # @param [String] rpath
151
+ # @macro note_apply
152
+ def rpath=(rpath)
153
+ @set[:rpath] = rpath
124
154
  end
125
155
 
126
156
  # Set runpath.
@@ -143,22 +173,35 @@ module PatchELF
143
173
  # Save the patched ELF as +out_file+.
144
174
  # @param [String?] out_file
145
175
  # If +out_file+ is +nil+, the original input file will be modified.
176
+ # @param [Boolean] patchelf_compatible
177
+ # When +patchelf_compatible+ is true, tries to produce same ELF as the one produced by NixOS/patchelf.
146
178
  # @return [void]
147
- def save(out_file = nil)
179
+ def save(out_file = nil, patchelf_compatible: false)
148
180
  # If nothing is modified, return directly.
149
181
  return if out_file.nil? && !dirty?
150
182
 
151
183
  out_file ||= @in_file
152
- saver = PatchELF::Saver.new(@in_file, out_file, @set)
184
+ saver = if patchelf_compatible
185
+ require 'patchelf/alt_saver'
186
+ PatchELF::AltSaver.new(@in_file, out_file, @set)
187
+ else
188
+ PatchELF::Saver.new(@in_file, out_file, @set)
189
+ end
153
190
 
154
191
  saver.save!
155
192
  end
156
193
 
157
194
  private
158
195
 
196
+ def log_or_raise(msg, exception = PatchELF::PatchError)
197
+ raise exception, msg if @on_error == :exception
198
+
199
+ PatchELF::Logger.warn(msg) if @on_error == :log
200
+ end
201
+
159
202
  def interpreter_
160
203
  segment = @elf.segment_by_type(:interp)
161
- return PatchELF::Logger.warn('No interpreter found.') if segment.nil?
204
+ return log_or_raise 'No interpreter found.', PatchELF::MissingSegmentError if segment.nil?
162
205
 
163
206
  segment.interp_name
164
207
  end
@@ -172,8 +215,8 @@ module PatchELF
172
215
  end
173
216
 
174
217
  # @return [String?]
175
- def runpath_
176
- tag_name_or_log(@rpath_sym, "Entry DT_#{@rpath_sym.to_s.upcase} not found.")
218
+ def runpath_(rpath_sym = :runpath)
219
+ tag_name_or_log(rpath_sym, "Entry DT_#{rpath_sym.to_s.upcase} not found.")
177
220
  end
178
221
 
179
222
  # @return [String?]
@@ -191,14 +234,16 @@ module PatchELF
191
234
  return if segment.nil?
192
235
 
193
236
  tag = segment.tag_by_type(type)
194
- return PatchELF::Logger.warn(log_msg) if tag.nil?
237
+ return log_or_raise log_msg, PatchELF::MissingTagError if tag.nil?
195
238
 
196
239
  tag.name
197
240
  end
198
241
 
199
242
  def dynamic_or_log
200
243
  @elf.segment_by_type(:dynamic).tap do |s|
201
- PatchELF::Logger.warn('DYNAMIC segment not found, might be a statically-linked ELF?') if s.nil?
244
+ if s.nil?
245
+ log_or_raise 'DYNAMIC segment not found, might be a statically-linked ELF?', PatchELF::MissingSegmentError
246
+ end
202
247
  end
203
248
  end
204
249
  end
@@ -121,30 +121,27 @@ module PatchELF
121
121
  def patch_needed
122
122
  original_needs = dynamic.tags_by_type(:needed)
123
123
  @set[:needed].uniq!
124
+
125
+ original = original_needs.map(&:name)
126
+ replace = @set[:needed]
127
+
124
128
  # 3 sets:
125
129
  # 1. in original and in needs - remain unchanged
126
130
  # 2. in original but not in needs - remove
127
131
  # 3. not in original and in needs - append
128
- original_needs.each do |n|
129
- next if @set[:needed].include?(n.name)
130
-
131
- n.header.d_tag = IGNORE # temporarily mark
132
- end
133
-
134
- extra = @set[:needed] - original_needs.map(&:name)
135
- original_needs.each do |n|
136
- break if extra.empty?
137
- next if n.header.d_tag != IGNORE
132
+ append = replace - original
133
+ remove = original - replace
138
134
 
139
- n.header.d_tag = ELFTools::Constants::DT_NEEDED
140
- reg_str_table(extra.shift) { |idx| n.header.d_val = idx }
135
+ ignored_dyns = remove.each_with_object([]) do |name, ignored|
136
+ dyn = original_needs.find { |n| n.name == name }.header
137
+ dyn.d_tag = IGNORE
138
+ ignored << dyn
141
139
  end
142
- return if extra.empty?
143
140
 
144
- # no spaces, need append
145
- extra.each do |name|
146
- tag = lazy_dyn(:needed)
147
- reg_str_table(name) { |idx| tag.d_val = idx }
141
+ append.zip(ignored_dyns) do |name, ignored_dyn|
142
+ dyn = ignored_dyn || lazy_dyn(:needed)
143
+ dyn.d_tag = ELFTools::Constants::DT_NEEDED
144
+ reg_str_table(name) { |idx| dyn.d_val = idx }
148
145
  end
149
146
  end
150
147
 
@@ -189,9 +186,10 @@ module PatchELF
189
186
  @mm.malloc(need_size) do |off, vaddr|
190
187
  new_str = strtab_string + @strtab_extend_requests.map(&:first).join("\x00") + "\x00"
191
188
  inline_patch(off, new_str)
189
+ cur = strtab_string.size
192
190
  @strtab_extend_requests.each do |str, block|
193
- # TODO: make here more efficient
194
- block.call(new_str.index(str + "\x00"))
191
+ block.call(cur)
192
+ cur += str.size + 1
195
193
  end
196
194
  # Now patching strtab header
197
195
  strtab.header.d_val = vaddr
@@ -2,5 +2,5 @@
2
2
 
3
3
  module PatchELF
4
4
  # Current gem version.
5
- VERSION = '0.1.0'.freeze
5
+ VERSION = '1.3.0'.freeze
6
6
  end
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: patchelf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - david942j
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-15 00:00:00.000000000 Z
11
+ date: 2020-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: elftools
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.1'
19
+ version: 1.1.3
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.1'
26
+ version: 1.1.3
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '12.3'
33
+ version: '13.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '12.3'
40
+ version: '13.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: 0.16.1
75
+ version: '0.17'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: 0.16.1
82
+ version: '0.17'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: tty-platform
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -121,7 +121,9 @@ files:
121
121
  - README.md
122
122
  - bin/patchelf.rb
123
123
  - lib/patchelf.rb
124
+ - lib/patchelf/alt_saver.rb
124
125
  - lib/patchelf/cli.rb
126
+ - lib/patchelf/exceptions.rb
125
127
  - lib/patchelf/helper.rb
126
128
  - lib/patchelf/logger.rb
127
129
  - lib/patchelf/mm.rb
@@ -140,14 +142,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
140
142
  requirements:
141
143
  - - ">="
142
144
  - !ruby/object:Gem::Version
143
- version: 2.1.0
145
+ version: '2.3'
144
146
  required_rubygems_version: !ruby/object:Gem::Requirement
145
147
  requirements:
146
148
  - - ">="
147
149
  - !ruby/object:Gem::Version
148
150
  version: '0'
149
151
  requirements: []
150
- rubygems_version: 3.0.2
152
+ rubygems_version: 3.1.2
151
153
  signing_key:
152
154
  specification_version: 4
153
155
  summary: patchelf