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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8560f68dba4f9e656585c38f31da63ae5f15320a03c1675adb13d27ffa2230b4
4
- data.tar.gz: 2d19b129beb5df7bd713fc6f0f65bb942194bea201857bda499a18e4496c4fe2
3
+ metadata.gz: 8182cf70ee88eceed07b85e8edda947e647842fb0e1767b36c0bfc61651ae9a6
4
+ data.tar.gz: a739c96f21f48a4ae7036ba184e46e3ca6a6d3c1cc79215b0e8e4a5b4b970562
5
5
  SHA512:
6
- metadata.gz: a2c443a08655333dc1b97ca62c1f6803bf6971565a9b40c80554aaf89a78b8e7548f0a854dd6703175e8be57154ca541ffe71f642a35ded2a734fbe1eae83b0e
7
- data.tar.gz: 73707a964c3cc1a9d47efcb0baaa45d1fc8e42e2450acca899b5258ca2b9236229e9e560a5c9fa347bbd7c5a429644a66ceed5ac072cb7ac06db3b98088c1a8c
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
@@ -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
@@ -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 = 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
183
190
 
184
191
  saver.save!
185
192
  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
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module PatchELF
4
4
  # Current gem version.
5
- VERSION = '1.2.0'.freeze
5
+ VERSION = '1.3.0'.freeze
6
6
  end
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.2.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: 2020-07-29 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
@@ -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.0.3
152
+ rubygems_version: 3.1.2
152
153
  signing_key:
153
154
  specification_version: 4
154
155
  summary: patchelf