patchelf 1.2.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 +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
|