patchelf 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/patchelf/alt_saver.rb +831 -0
- data/lib/patchelf/logger.rb +1 -1
- data/lib/patchelf/patcher.rb +9 -2
- data/lib/patchelf/saver.rb +14 -17
- data/lib/patchelf/version.rb +1 -1
- metadata +8 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8182cf70ee88eceed07b85e8edda947e647842fb0e1767b36c0bfc61651ae9a6
|
4
|
+
data.tar.gz: a739c96f21f48a4ae7036ba184e46e3ca6a6d3c1cc79215b0e8e4a5b4b970562
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed2e115d7925aef19b3b192f0d9fe70744bd267fe3ef1e9b4245340e741bd6a3cf62bb82d6a847edd8c99878dfaf2e64b50e82ff1049be1f8e89881279fbd682
|
7
|
+
data.tar.gz: 02bfde530e73b448ec73932a1be1dd378bb44d74235fa9ab9096bf3c45607f0194ccfe3979857e4b4389004ee1cef8d4852b687fd571c4d4cfb291e57c576f8a
|
@@ -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
|
data/lib/patchelf/logger.rb
CHANGED
data/lib/patchelf/patcher.rb
CHANGED
@@ -173,13 +173,20 @@ module PatchELF
|
|
173
173
|
# Save the patched ELF as +out_file+.
|
174
174
|
# @param [String?] out_file
|
175
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.
|
176
178
|
# @return [void]
|
177
|
-
def save(out_file = nil)
|
179
|
+
def save(out_file = nil, patchelf_compatible: false)
|
178
180
|
# If nothing is modified, return directly.
|
179
181
|
return if out_file.nil? && !dirty?
|
180
182
|
|
181
183
|
out_file ||= @in_file
|
182
|
-
saver =
|
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
|
183
190
|
|
184
191
|
saver.save!
|
185
192
|
end
|
data/lib/patchelf/saver.rb
CHANGED
@@ -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
|
-
|
129
|
-
|
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
|
-
|
140
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
147
|
-
reg_str_table(name) { |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
|
|
data/lib/patchelf/version.rb
CHANGED
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: patchelf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
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: 2020-
|
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:
|
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:
|
26
|
+
version: 1.1.3
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -121,6 +121,7 @@ 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
|
125
126
|
- lib/patchelf/exceptions.rb
|
126
127
|
- lib/patchelf/helper.rb
|
@@ -148,7 +149,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
148
149
|
- !ruby/object:Gem::Version
|
149
150
|
version: '0'
|
150
151
|
requirements: []
|
151
|
-
rubygems_version: 3.
|
152
|
+
rubygems_version: 3.1.2
|
152
153
|
signing_key:
|
153
154
|
specification_version: 4
|
154
155
|
summary: patchelf
|