patchelf 1.2.0 → 1.4.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.
@@ -0,0 +1,1052 @@
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 separate 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
+ return if sec.header.sh_type == ELFTools::Constants::SHT_NOBITS
137
+
138
+ shdr = sec.header
139
+ with_buf_at(shdr.sh_offset) do |buf|
140
+ dyn = ELFTools::Structs::ELF_Dyn.new(elf_class: elf_class, endian: endian)
141
+ loop do
142
+ buf_dyn_offset = buf.tell
143
+ dyn.clear
144
+ dyn.read(buf)
145
+ break if dyn.d_tag == ELFTools::Constants::DT_NULL
146
+
147
+ yield dyn, buf_dyn_offset
148
+ # there's a possibility for caller to modify @buffer.pos, seek to avoid such issues
149
+ buf.seek buf_dyn_offset + dyn.num_bytes
150
+ end
151
+ end
152
+ end
153
+
154
+ # the idea of uniquely identifying section by its name has its problems
155
+ # but this is how patchelf operates and is prone to bugs.
156
+ # e.g: https://github.com/NixOS/patchelf/issues/197
157
+ def find_section(sec_name)
158
+ idx = find_section_idx sec_name
159
+ return unless idx
160
+
161
+ @sections[idx]
162
+ end
163
+
164
+ def find_section_idx(sec_name)
165
+ @section_idx_by_name[sec_name]
166
+ end
167
+
168
+ def buf_grow!(newsz)
169
+ bufsz = @buffer.size
170
+ return if newsz <= bufsz
171
+
172
+ @buffer.truncate newsz
173
+ end
174
+
175
+ def modify_interpreter
176
+ @replaced_sections['.interp'] = "#{@set[:interpreter]}\x00"
177
+ end
178
+
179
+ def modify_needed
180
+ # due to gsoc time constraints only implmenting features used by brew.
181
+ raise NotImplementedError
182
+ end
183
+
184
+ # not checking for nil as modify_rpath is only called if @set[:rpath]
185
+ def modify_rpath
186
+ modify_rpath_helper @set[:rpath], force_rpath: true
187
+ end
188
+
189
+ # not checking for nil as modify_runpath is only called if @set[:runpath]
190
+ def modify_runpath
191
+ modify_rpath_helper @set[:runpath]
192
+ end
193
+
194
+ def collect_runpath_tags
195
+ tags = {}
196
+ each_dynamic_tags do |dyn, off|
197
+ case dyn.d_tag
198
+ when ELFTools::Constants::DT_RPATH
199
+ tag_type = :rpath
200
+ when ELFTools::Constants::DT_RUNPATH
201
+ tag_type = :runpath
202
+ else
203
+ next
204
+ end
205
+
206
+ # clone does shallow copy, and for some reason d_tag and d_val can't be pass as argument
207
+ dyn_rpath = ELFTools::Structs::ELF_Dyn.new(endian: endian, elf_class: elf_class)
208
+ dyn_rpath.assign({ d_tag: dyn.d_tag.to_i, d_val: dyn.d_val.to_i })
209
+ tags[tag_type] = { offset: off, header: dyn_rpath }
210
+ end
211
+ tags
212
+ end
213
+
214
+ def resolve_rpath_tag_conflict(dyn_tags, force_rpath: false)
215
+ dyn_runpath, dyn_rpath = dyn_tags.values_at(:runpath, :rpath)
216
+
217
+ update_sym =
218
+ if !force_rpath && dyn_rpath && dyn_runpath.nil?
219
+ :runpath
220
+ elsif force_rpath && dyn_runpath
221
+ :rpath
222
+ end
223
+ return unless update_sym
224
+
225
+ delete_sym, = %i[rpath runpath] - [update_sym]
226
+ dyn_tag = dyn_tags[update_sym] = dyn_tags[delete_sym]
227
+ dyn = dyn_tag[:header]
228
+ dyn.d_tag = ELFTools::Constants.const_get("DT_#{update_sym.upcase}")
229
+ with_buf_at(dyn_tag[:offset]) { |buf| dyn.write(buf) }
230
+ dyn_tags.delete(delete_sym)
231
+ end
232
+
233
+ def modify_rpath_helper(new_rpath, force_rpath: false)
234
+ shdr_dynstr = dynstr.header
235
+
236
+ dyn_tags = collect_runpath_tags
237
+ resolve_rpath_tag_conflict(dyn_tags, force_rpath: force_rpath)
238
+ # (:runpath, :rpath) order_matters.
239
+ resolved_rpath_dyn = dyn_tags.values_at(:runpath, :rpath).compact.first
240
+
241
+ old_rpath = ''
242
+ rpath_off = nil
243
+ if resolved_rpath_dyn
244
+ rpath_off = shdr_dynstr.sh_offset + resolved_rpath_dyn[:header].d_val
245
+ old_rpath = buf_cstr(rpath_off)
246
+ end
247
+ return if old_rpath == new_rpath
248
+
249
+ with_buf_at(rpath_off) { |b| b.write('X' * old_rpath.size) } if rpath_off
250
+ if new_rpath.size <= old_rpath.size
251
+ with_buf_at(rpath_off) { |b| b.write "#{new_rpath}\x00" }
252
+ return
253
+ end
254
+
255
+ Logger.debug 'rpath is too long, resizing...'
256
+ new_dynstr = replace_section '.dynstr', shdr_dynstr.sh_size + new_rpath.size + 1
257
+ new_rpath_strtab_idx = shdr_dynstr.sh_size.to_i
258
+ new_dynstr[new_rpath_strtab_idx..(new_rpath_strtab_idx + new_rpath.size)] = "#{new_rpath}\x00"
259
+
260
+ dyn_tags.each do |_, dyn|
261
+ dyn[:header].d_val = new_rpath_strtab_idx
262
+ with_buf_at(dyn[:offset]) { |b| dyn[:header].write(b) }
263
+ end
264
+
265
+ return unless dyn_tags.empty?
266
+
267
+ add_dt_rpath!(
268
+ d_tag: force_rpath ? ELFTools::Constants::DT_RPATH : ELFTools::Constants::DT_RUNPATH,
269
+ d_val: new_rpath_strtab_idx
270
+ )
271
+ end
272
+
273
+ def modify_soname
274
+ return unless ehdr.e_type == ELFTools::Constants::ET_DYN
275
+
276
+ # due to gsoc time constraints only implmenting features used by brew.
277
+ raise NotImplementedError
278
+ end
279
+
280
+ def add_segment!(**phdr_vals)
281
+ new_phdr = ELFTools::Structs::ELF_Phdr[elf_class].new(endian: endian, **phdr_vals)
282
+ # nil = no reference to stream; we only want @segments[i].header
283
+ new_segment = ELFTools::Segments::Segment.new(new_phdr, nil)
284
+ @segments.push new_segment
285
+ ehdr.e_phnum += 1
286
+ nil
287
+ end
288
+
289
+ def add_dt_rpath!(d_tag: nil, d_val: nil)
290
+ dyn_num_bytes = nil
291
+ dt_null_idx = 0
292
+ each_dynamic_tags do |dyn|
293
+ dyn_num_bytes ||= dyn.num_bytes
294
+ dt_null_idx += 1
295
+ end
296
+
297
+ if dyn_num_bytes.nil?
298
+ Logger.error 'no dynamic tags'
299
+ return
300
+ end
301
+
302
+ # allot for new dt_runpath
303
+ shdr_dynamic = find_section('.dynamic').header
304
+ new_dynamic_data = replace_section '.dynamic', shdr_dynamic.sh_size + dyn_num_bytes
305
+
306
+ # consider DT_NULL when copying
307
+ replacement_size = (dt_null_idx + 1) * dyn_num_bytes
308
+
309
+ # make space for dt_runpath tag at the top, shift data by one tag positon
310
+ new_dynamic_data[dyn_num_bytes..(replacement_size + dyn_num_bytes)] = new_dynamic_data[0..replacement_size]
311
+
312
+ dyn_rpath = ELFTools::Structs::ELF_Dyn.new endian: endian, elf_class: elf_class
313
+ dyn_rpath.d_tag = d_tag
314
+ dyn_rpath.d_val = d_val
315
+
316
+ zi = StringIO.new
317
+ dyn_rpath.write zi
318
+ zi.rewind
319
+ new_dynamic_data[0...dyn_num_bytes] = zi.read
320
+ end
321
+
322
+ # given a index into old_sections table
323
+ # returns the corresponding section index in @sections
324
+ #
325
+ # raises ArgumentError if old_shndx can't be found in old_sections
326
+ # TODO: handle case of non existing section in (new) @sections.
327
+ def new_section_idx(old_shndx)
328
+ return if old_shndx == ELFTools::Constants::SHN_UNDEF || old_shndx >= ELFTools::Constants::SHN_LORESERVE
329
+
330
+ raise ArgumentError if old_shndx >= old_sections.count
331
+
332
+ old_sec = old_sections[old_shndx]
333
+ raise PatchError, "old_sections[#{shndx}] is nil" if old_sec.nil?
334
+
335
+ # TODO: handle case of non existing section in (new) @sections.
336
+ find_section_idx(old_sec.name)
337
+ end
338
+
339
+ def page_size
340
+ Helper.page_size(ehdr.e_machine)
341
+ end
342
+
343
+ def patch_out
344
+ with_buf_at(0) { |b| ehdr.write(b) }
345
+
346
+ File.open(out_file, 'wb') do |f|
347
+ @buffer.rewind
348
+ f.write @buffer.read
349
+ end
350
+ end
351
+
352
+ # size includes NUL byte
353
+ def replace_section(section_name, size)
354
+ data = @replaced_sections[section_name]
355
+ unless data
356
+ shdr = find_section(section_name).header
357
+ # avoid calling +section.data+ as the @buffer contents may vary from
358
+ # the stream provided to section at initialization.
359
+ # ideally, calling section.data should work, however avoiding it to prevent
360
+ # future traps.
361
+ with_buf_at(shdr.sh_offset) { |b| data = b.read shdr.sh_size }
362
+ end
363
+ rep_data = if data.size == size
364
+ data
365
+ elsif data.size < size
366
+ data.ljust(size, "\x00")
367
+ else
368
+ "#{data[0...size]}\x00"
369
+ end
370
+ @replaced_sections[section_name] = rep_data
371
+ end
372
+
373
+ def write_phdrs_to_buf!
374
+ sort_phdrs!
375
+ with_buf_at(ehdr.e_phoff) do |buf|
376
+ @segments.each { |seg| seg.header.write(buf) }
377
+ end
378
+ end
379
+
380
+ def write_shdrs_to_buf!
381
+ raise PatchError, 'ehdr.e_shnum != @sections.count' if ehdr.e_shnum != @sections.count
382
+
383
+ sort_shdrs!
384
+ with_buf_at(ehdr.e_shoff) do |buf|
385
+ @sections.each { |section| section.header.write(buf) }
386
+ end
387
+ sync_dyn_tags!
388
+ end
389
+
390
+ # data for manual packing and unpacking of symbols in symtab sections.
391
+ def meta_sym_pack
392
+ return @meta_sym_pack if @meta_sym_pack
393
+
394
+ # resort to manual packing and unpacking of data,
395
+ # as using bindata is painfully slow :(
396
+ if elf_class == 32
397
+ sym_num_bytes = 16 # u32 u32 u32 u8 u8 u16
398
+ pack_code = endian == :little ? 'VVVCCv' : 'NNNCCn'
399
+ pack_st_info = 3
400
+ pack_st_shndx = 5
401
+ pack_st_value = 1
402
+ else # 64
403
+ sym_num_bytes = 24 # u32 u8 u8 u16 u64 u64
404
+ pack_code = endian == :little ? 'VCCvQ<Q<' : 'NCCnQ>Q>'
405
+ pack_st_info = 1
406
+ pack_st_shndx = 3
407
+ pack_st_value = 4
408
+ end
409
+
410
+ @meta_sym_pack = {
411
+ num_bytes: sym_num_bytes, code: pack_code,
412
+ st_info: pack_st_info, st_shndx: pack_st_shndx, st_value: pack_st_value
413
+ }
414
+ end
415
+
416
+ # yields +symbol+, +entry+
417
+ def each_symbol(shdr)
418
+ return unless [ELFTools::Constants::SHT_SYMTAB, ELFTools::Constants::SHT_DYNSYM].include?(shdr.sh_type)
419
+
420
+ pack_code, sym_num_bytes = meta_sym_pack.values_at(:code, :num_bytes)
421
+
422
+ with_buf_at(shdr.sh_offset) do |buf|
423
+ num_symbols = shdr.sh_size / sym_num_bytes
424
+ num_symbols.times do |entry|
425
+ sym = buf.read(sym_num_bytes).unpack(pack_code)
426
+ sym_modified = yield sym, entry
427
+
428
+ if sym_modified
429
+ buf.seek buf.tell - sym_num_bytes
430
+ buf.write sym.pack(pack_code)
431
+ end
432
+ end
433
+ end
434
+ end
435
+
436
+ def rewrite_headers(phdr_address)
437
+ # there can only be a single program header table according to ELF spec
438
+ @segments.find { |seg| seg.header.p_type == ELFTools::Constants::PT_PHDR }&.tap do |seg|
439
+ phdr = seg.header
440
+ phdr.p_offset = ehdr.e_phoff.to_i
441
+ phdr.p_vaddr = phdr.p_paddr = phdr_address.to_i
442
+ phdr.p_filesz = phdr.p_memsz = phdr.num_bytes * @segments.count # e_phentsize * e_phnum
443
+ end
444
+ write_phdrs_to_buf!
445
+ write_shdrs_to_buf!
446
+
447
+ pack = meta_sym_pack
448
+ @sections.each do |sec|
449
+ each_symbol(sec.header) do |sym, entry|
450
+ old_shndx = sym[pack[:st_shndx]]
451
+
452
+ begin
453
+ new_index = new_section_idx(old_shndx)
454
+ next unless new_index
455
+ rescue ArgumentError
456
+ Logger.warn "entry #{entry} in symbol table refers to a non existing section, skipping"
457
+ end
458
+
459
+ sym[pack[:st_shndx]] = new_index
460
+
461
+ # right 4 bits in the st_info field is st_type
462
+ if (sym[pack[:st_info]] & 0xF) == ELFTools::Constants::STT_SECTION
463
+ sym[pack[:st_value]] = @sections[new_index].header.sh_addr.to_i
464
+ end
465
+ true
466
+ end
467
+ end
468
+ end
469
+
470
+ def rewrite_sections
471
+ return if @replaced_sections.empty?
472
+
473
+ case ehdr.e_type
474
+ when ELFTools::Constants::ET_DYN
475
+ rewrite_sections_library
476
+ when ELFTools::Constants::ET_EXEC
477
+ rewrite_sections_executable
478
+ else
479
+ raise PatchError, 'unknown ELF type'
480
+ end
481
+ end
482
+
483
+ def replaced_section_indices
484
+ return enum_for(:replaced_section_indices) unless block_given?
485
+
486
+ last_replaced = 0
487
+ @sections.each_with_index do |sec, idx|
488
+ if @replaced_sections[sec.name]
489
+ last_replaced = idx
490
+ yield last_replaced
491
+ end
492
+ end
493
+ raise PatchError, 'last_replaced = 0' if last_replaced.zero?
494
+ raise PatchError, 'last_replaced + 1 >= @sections.size' if last_replaced + 1 >= @sections.size
495
+ end
496
+
497
+ def start_replacement_shdr
498
+ last_replaced = replaced_section_indices.max
499
+ start_replacement_hdr = @sections[last_replaced + 1].header
500
+
501
+ prev_sec_name = ''
502
+ (1..last_replaced).each do |idx|
503
+ sec = @sections[idx]
504
+ shdr = sec.header
505
+ if (sec.type == ELFTools::Constants::SHT_PROGBITS && sec.name != '.interp') || prev_sec_name == '.dynstr'
506
+ start_replacement_hdr = shdr
507
+ break
508
+ elsif @replaced_sections[sec.name].nil?
509
+ Logger.debug " replacing section #{sec.name} which is in the way"
510
+ replace_section(sec.name, shdr.sh_size)
511
+ end
512
+ prev_sec_name = sec.name
513
+ end
514
+
515
+ start_replacement_hdr
516
+ end
517
+
518
+ def copy_shdrs_to_eof
519
+ shoff_new = @buffer.size
520
+ # honestly idk why `ehdr.e_shoff` is considered when we are only moving shdrs.
521
+ sh_size = ehdr.e_shoff + (ehdr.e_shnum * ehdr.e_shentsize)
522
+ buf_grow! @buffer.size + sh_size
523
+ ehdr.e_shoff = shoff_new
524
+ raise PatchError, 'ehdr.e_shnum != @sections.size' if ehdr.e_shnum != @sections.size
525
+
526
+ with_buf_at(ehdr.e_shoff + @sections.first.header.num_bytes) do |buf| # skip writing to NULL section
527
+ @sections.each_with_index do |sec, idx|
528
+ next if idx.zero?
529
+
530
+ sec.header.write buf
531
+ end
532
+ end
533
+ end
534
+
535
+ def rewrite_sections_executable
536
+ sort_shdrs!
537
+ shdr = start_replacement_shdr
538
+ start_offset = shdr.sh_offset.to_i
539
+ start_addr = shdr.sh_addr.to_i
540
+ first_page = start_addr - start_offset
541
+
542
+ Logger.debug "first reserved offset/addr is 0x#{start_offset.to_s 16}/0x#{start_addr.to_s 16}"
543
+
544
+ unless start_addr % page_size == start_offset % page_size
545
+ raise PatchError, 'start_addr != start_offset (mod PAGE_SIZE)'
546
+ end
547
+
548
+ Logger.debug "first page is 0x#{first_page.to_i.to_s 16}"
549
+
550
+ copy_shdrs_to_eof if ehdr.e_shoff < start_offset
551
+
552
+ normalize_note_segments
553
+
554
+ seg_num_bytes = @segments.first.header.num_bytes
555
+ needed_space = (
556
+ ehdr.num_bytes +
557
+ (@segments.count * seg_num_bytes) +
558
+ @replaced_sections.sum { |_, str| Helper.alignup(str.size, @section_alignment) }
559
+ )
560
+
561
+ if needed_space > start_offset
562
+ needed_space += seg_num_bytes # new load segment is required
563
+
564
+ needed_pages = Helper.alignup(needed_space - start_offset, page_size) / page_size
565
+ Logger.debug "needed pages is #{needed_pages}"
566
+ raise PatchError, 'virtual address space underrun' if needed_pages * page_size > first_page
567
+
568
+ shift_file(needed_pages, start_offset)
569
+
570
+ first_page -= needed_pages * page_size
571
+ start_offset += needed_pages * page_size
572
+ end
573
+ Logger.debug "needed space is #{needed_space}"
574
+
575
+ cur_off = ehdr.num_bytes + (@segments.count * seg_num_bytes)
576
+ Logger.debug "clearing first #{start_offset - cur_off} bytes"
577
+ with_buf_at(cur_off) { |buf| buf.fill("\x00", (start_offset - cur_off)) }
578
+
579
+ cur_off = write_replaced_sections cur_off, first_page, 0
580
+ raise PatchError, "cur_off(#{cur_off}) != needed_space" if cur_off != needed_space
581
+
582
+ rewrite_headers first_page + ehdr.e_phoff
583
+ end
584
+
585
+ def replace_sections_in_the_way_of_phdr!
586
+ num_notes = @sections.count { |sec| sec.type == ELFTools::Constants::SHT_NOTE }
587
+ pht_size = ehdr.num_bytes + ((@segments.count + num_notes + 1) * @segments.first.header.num_bytes)
588
+
589
+ # replace sections that may overlap with expanded program header table
590
+ @sections.each_with_index do |sec, idx|
591
+ shdr = sec.header
592
+ next if idx.zero? || @replaced_sections[sec.name]
593
+ break if shdr.sh_offset > pht_size
594
+
595
+ replace_section sec.name, shdr.sh_size
596
+ end
597
+ end
598
+
599
+ def rewrite_sections_library
600
+ start_page = 0
601
+ first_page = 0
602
+ @segments.each do |seg|
603
+ phdr = seg.header
604
+ this_page = Helper.alignup(phdr.p_vaddr + phdr.p_memsz, page_size)
605
+ start_page = [start_page, this_page].max
606
+ first_page = phdr.p_vaddr - phdr.p_offset if phdr.p_type == ELFTools::Constants::PT_PHDR
607
+ end
608
+
609
+ Logger.debug "Last page is 0x#{start_page.to_s 16}"
610
+ Logger.debug "First page is 0x#{first_page.to_s 16}"
611
+ replace_sections_in_the_way_of_phdr!
612
+ needed_space = @replaced_sections.sum { |_, str| Helper.alignup(str.size, @section_alignment) }
613
+ Logger.debug "needed space = #{needed_space}"
614
+
615
+ start_offset = Helper.alignup(@buffer.size, page_size)
616
+ buf_grow! start_offset + needed_space
617
+
618
+ # executable shared object
619
+ if start_offset > start_page && @segments.any? { |seg| seg.header.p_type == ELFTools::Constants::PT_INTERP }
620
+ Logger.debug(
621
+ "shifting new PT_LOAD segment by #{start_offset - start_page} bytes to work around a Linux kernel bug"
622
+ )
623
+ start_page = start_offset
624
+ end
625
+
626
+ ehdr.e_phoff = ehdr.num_bytes
627
+ add_segment!(
628
+ p_type: ELFTools::Constants::PT_LOAD,
629
+ p_offset: start_offset,
630
+ p_vaddr: start_page,
631
+ p_paddr: start_page,
632
+ p_filesz: needed_space,
633
+ p_memsz: needed_space,
634
+ p_flags: ELFTools::Constants::PF_R | ELFTools::Constants::PF_W,
635
+ p_align: page_size
636
+ )
637
+
638
+ normalize_note_segments
639
+
640
+ cur_off = write_replaced_sections start_offset, start_page, start_offset
641
+ raise PatchError, 'cur_off != start_offset + needed_space' if cur_off != start_offset + needed_space
642
+
643
+ rewrite_headers(first_page + ehdr.e_phoff)
644
+ end
645
+
646
+ def normalize_note_segments
647
+ return if @replaced_sections.none? do |rsec_name, _|
648
+ find_section(rsec_name)&.type == ELFTools::Constants::SHT_NOTE
649
+ end
650
+
651
+ new_phdrs = []
652
+
653
+ phdrs_by_type(ELFTools::Constants::PT_NOTE) do |phdr|
654
+ # Binaries produced by older patchelf versions may contain empty PT_NOTE segments.
655
+ next if @sections.none? do |sec|
656
+ sec.header.sh_offset >= phdr.p_offset && sec.header.sh_offset < phdr.p_offset + phdr.p_filesz
657
+ end
658
+
659
+ new_phdrs += normalize_note_segment(phdr)
660
+ end
661
+
662
+ new_phdrs.each { |phdr| add_segment!(**phdr.snapshot) }
663
+ end
664
+
665
+ def normalize_note_segment(phdr)
666
+ start_off = phdr.p_offset.to_i
667
+ curr_off = start_off
668
+ end_off = start_off + phdr.p_filesz
669
+
670
+ new_phdrs = []
671
+
672
+ while curr_off < end_off
673
+ size = 0
674
+ sections_at_aligned_offset(curr_off) do |sec|
675
+ next if sec.type != ELFTools::Constants::SHT_NOTE
676
+
677
+ size = sec.header.sh_size.to_i
678
+ curr_off = sec.header.sh_offset.to_i
679
+ break
680
+ end
681
+
682
+ raise PatchError, 'cannot normalize PT_NOTE segment: non-contiguous SHT_NOTE sections' if size.zero?
683
+
684
+ if curr_off + size > end_off
685
+ raise PatchError, 'cannot normalize PT_NOTE segment: partially mapped SHT_NOTE section'
686
+ end
687
+
688
+ new_phdr = ELFTools::Structs::ELF_Phdr[elf_class].new(endian: endian, **phdr.snapshot)
689
+ new_phdr.p_offset = curr_off
690
+ new_phdr.p_vaddr = phdr.p_vaddr + (curr_off - start_off)
691
+ new_phdr.p_paddr = phdr.p_paddr + (curr_off - start_off)
692
+ new_phdr.p_filesz = size
693
+ new_phdr.p_memsz = size
694
+
695
+ if curr_off == start_off
696
+ phdr.assign(new_phdr)
697
+ else
698
+ new_phdrs << new_phdr
699
+ end
700
+
701
+ curr_off += size
702
+ end
703
+
704
+ new_phdrs
705
+ end
706
+
707
+ def sections_at_aligned_offset(offset)
708
+ @sections.each do |sec|
709
+ shdr = sec.header
710
+
711
+ aligned_offset = Helper.alignup(offset, shdr.sh_addralign)
712
+ next if shdr.sh_offset != aligned_offset
713
+
714
+ yield sec
715
+ end
716
+ end
717
+
718
+ def shift_sections(shift, start_offset)
719
+ ehdr.e_shoff += shift if ehdr.e_shoff >= start_offset
720
+
721
+ @sections.each_with_index do |sec, i|
722
+ next if i.zero? # dont touch NULL section
723
+
724
+ shdr = sec.header
725
+ next if shdr.sh_offset < start_offset
726
+
727
+ shdr.sh_offset += shift
728
+ end
729
+ end
730
+
731
+ def shift_segment_offset(phdr, shift)
732
+ phdr.p_offset += shift
733
+ phdr.p_align = page_size if phdr.p_align != 0 && (phdr.p_vaddr - phdr.p_offset) % phdr.p_align != 0
734
+ end
735
+
736
+ def shift_segment_virtual_address(phdr, shift)
737
+ phdr.p_paddr -= shift if phdr.p_paddr > shift
738
+ phdr.p_vaddr -= shift if phdr.p_vaddr > shift
739
+ end
740
+
741
+ # rubocop:disable Metrics/PerceivedComplexity
742
+ def shift_segments(shift, start_offset)
743
+ split_index = -1
744
+ split_shift = 0
745
+
746
+ @segments.each_with_index do |seg, idx|
747
+ phdr = seg.header
748
+ p_start = phdr.p_offset
749
+
750
+ if p_start <= start_offset && p_start + phdr.p_filesz > start_offset &&
751
+ phdr.p_type == ELFTools::Constants::PT_LOAD
752
+ raise PatchError, "split_index(#{split_index}) != -1" if split_index != -1
753
+
754
+ split_index = idx
755
+ split_shift = start_offset - p_start
756
+
757
+ phdr.p_offset = start_offset
758
+ phdr.p_memsz -= split_shift
759
+ phdr.p_filesz -= split_shift
760
+ phdr.p_paddr += split_shift
761
+ phdr.p_vaddr += split_shift
762
+
763
+ p_start = start_offset
764
+ end
765
+
766
+ if p_start >= start_offset
767
+ shift_segment_offset(phdr, shift)
768
+ else
769
+ shift_segment_virtual_address(phdr, shift)
770
+ end
771
+ end
772
+
773
+ raise PatchError, "split_index(#{split_index}) == -1" if split_index == -1
774
+
775
+ [split_index, split_shift]
776
+ end
777
+ # rubocop:enable Metrics/PerceivedComplexity
778
+
779
+ def shift_file(extra_pages, start_offset)
780
+ raise PatchError, "start_offset(#{start_offset}) < ehdr.num_bytes" if start_offset < ehdr.num_bytes
781
+
782
+ oldsz = @buffer.size
783
+ raise PatchError, "oldsz <= start_offset(#{start_offset})" if oldsz <= start_offset
784
+
785
+ shift = extra_pages * page_size
786
+ buf_grow!(oldsz + shift)
787
+ buf_move!(start_offset + shift, start_offset, oldsz - start_offset)
788
+ with_buf_at(start_offset) { |buf| buf.write "\x00" * shift }
789
+
790
+ ehdr.e_phoff = ehdr.num_bytes
791
+
792
+ shift_sections(shift, start_offset)
793
+
794
+ split_index, split_shift = shift_segments(shift, start_offset)
795
+
796
+ split_phdr = @segments[split_index].header
797
+ add_segment!(
798
+ p_type: ELFTools::Constants::PT_LOAD,
799
+ p_offset: split_phdr.p_offset - split_shift - shift,
800
+ p_vaddr: split_phdr.p_vaddr - split_shift - shift,
801
+ p_paddr: split_phdr.p_paddr - split_shift - shift,
802
+ p_filesz: split_shift + shift,
803
+ p_memsz: split_shift + shift,
804
+ p_flags: ELFTools::Constants::PF_R | ELFTools::Constants::PF_W,
805
+ p_align: page_size
806
+ )
807
+ end
808
+
809
+ def sort_phdrs!
810
+ pt_phdr = ELFTools::Constants::PT_PHDR
811
+ @segments.sort! do |me, you|
812
+ next 1 if you.header.p_type == pt_phdr
813
+ next -1 if me.header.p_type == pt_phdr
814
+
815
+ me.header.p_paddr.to_i <=> you.header.p_paddr.to_i
816
+ end
817
+ end
818
+
819
+ # section headers may contain sh_info and sh_link values that are
820
+ # references to another section
821
+ def collect_section_to_section_refs
822
+ rel_syms = [ELFTools::Constants::SHT_REL, ELFTools::Constants::SHT_RELA]
823
+ # Translate sh_link, sh_info mappings to section names.
824
+ @sections.each_with_object({ linkage: {}, info: {} }) do |s, collected|
825
+ hdr = s.header
826
+ collected[:linkage][s.name] = @sections[hdr.sh_link].name if hdr.sh_link.nonzero?
827
+ collected[:info][s.name] = @sections[hdr.sh_info].name if hdr.sh_info.nonzero? && rel_syms.include?(hdr.sh_type)
828
+ end
829
+ end
830
+
831
+ # @param collected
832
+ # this must be the value returned by +collect_section_to_section_refs+
833
+ def restore_section_to_section_refs!(collected)
834
+ rel_syms = [ELFTools::Constants::SHT_REL, ELFTools::Constants::SHT_RELA]
835
+ linkage, info = collected.values_at(:linkage, :info)
836
+ @sections.each do |sec|
837
+ hdr = sec.header
838
+ hdr.sh_link = find_section_idx(linkage[sec.name]) if hdr.sh_link.nonzero?
839
+ hdr.sh_info = find_section_idx(info[sec.name]) if hdr.sh_info.nonzero? && rel_syms.include?(hdr.sh_type)
840
+ end
841
+ end
842
+
843
+ def sort_shdrs!
844
+ return if @sections.empty?
845
+
846
+ section_dep_values = collect_section_to_section_refs
847
+ shstrtab = @sections[ehdr.e_shstrndx].header
848
+ @sections.sort! { |me, you| me.header.sh_offset.to_i <=> you.header.sh_offset.to_i }
849
+ update_section_idx!
850
+ restore_section_to_section_refs!(section_dep_values)
851
+ @sections.each_with_index do |sec, idx|
852
+ ehdr.e_shstrndx = idx if sec.header.sh_offset == shstrtab.sh_offset
853
+ end
854
+ end
855
+
856
+ def jmprel_section_name
857
+ sec_name = %w[.rel.plt .rela.plt .rela.IA_64.pltoff].find { |s| find_section(s) }
858
+ raise PatchError, 'cannot find section corresponding to DT_JMPREL' unless sec_name
859
+
860
+ sec_name
861
+ end
862
+
863
+ # given a +dyn.d_tag+, returns the section name it must be synced to.
864
+ # it may return nil, when given tag maps to no section,
865
+ # or when its okay to skip if section is not found.
866
+ def dyn_tag_to_section_name(d_tag)
867
+ case d_tag
868
+ when ELFTools::Constants::DT_STRTAB, ELFTools::Constants::DT_STRSZ
869
+ '.dynstr'
870
+ when ELFTools::Constants::DT_SYMTAB
871
+ '.dynsym'
872
+ when ELFTools::Constants::DT_HASH
873
+ '.hash'
874
+ when ELFTools::Constants::DT_GNU_HASH
875
+ # return nil if not found, patchelf claims no problem in skipping
876
+ find_section('.gnu.hash')&.name
877
+ when ELFTools::Constants::DT_MIPS_XHASH
878
+ return if ehdr.e_machine != ELFTools::Constants::EM_MIPS
879
+
880
+ '.MIPS.xhash'
881
+ when ELFTools::Constants::DT_JMPREL
882
+ jmprel_section_name
883
+ when ELFTools::Constants::DT_REL
884
+ # regarding .rel.got, NixOS/patchelf says
885
+ # "no idea if this makes sense, but it was needed for some program"
886
+ #
887
+ # return nil if not found, patchelf claims no problem in skipping
888
+ %w[.rel.dyn .rel.got].find { |s| find_section(s) }
889
+ when ELFTools::Constants::DT_RELA
890
+ # return nil if not found, patchelf claims no problem in skipping
891
+ find_section('.rela.dyn')&.name
892
+ when ELFTools::Constants::DT_VERNEED
893
+ '.gnu.version_r'
894
+ when ELFTools::Constants::DT_VERSYM
895
+ '.gnu.version'
896
+ end
897
+ end
898
+
899
+ # updates dyn tags by syncing it with @section values
900
+ def sync_dyn_tags!
901
+ dyn_table_offset = nil
902
+ each_dynamic_tags do |dyn, buf_off|
903
+ dyn_table_offset ||= buf_off
904
+
905
+ sec_name = dyn_tag_to_section_name(dyn.d_tag)
906
+
907
+ unless sec_name
908
+ if dyn.d_tag == ELFTools::Constants::DT_MIPS_RLD_MAP_REL && ehdr.e_machine == ELFTools::Constants::EM_MIPS
909
+ rld_map = find_section('.rld_map')
910
+ dyn.d_val = if rld_map
911
+ rld_map.header.sh_addr.to_i - (buf_off - dyn_table_offset) -
912
+ find_section('.dynamic').header.sh_addr.to_i
913
+ else
914
+ Logger.warn 'DT_MIPS_RLD_MAP_REL entry is present, but .rld_map section is not'
915
+ 0
916
+ end
917
+ end
918
+
919
+ next
920
+ end
921
+
922
+ shdr = find_section(sec_name).header
923
+ dyn.d_val = dyn.d_tag == ELFTools::Constants::DT_STRSZ ? shdr.sh_size.to_i : shdr.sh_addr.to_i
924
+
925
+ with_buf_at(buf_off) { |wbuf| dyn.write(wbuf) }
926
+ end
927
+ end
928
+
929
+ def update_section_idx!
930
+ @section_idx_by_name = @sections.map.with_index { |sec, idx| [sec.name, idx] }.to_h
931
+ end
932
+
933
+ def with_buf_at(pos)
934
+ return unless block_given?
935
+
936
+ opos = @buffer.tell
937
+ @buffer.seek pos
938
+ yield @buffer
939
+ @buffer.seek opos
940
+ nil
941
+ end
942
+
943
+ def sync_sec_to_seg(shdr, phdr)
944
+ phdr.p_offset = shdr.sh_offset.to_i
945
+ phdr.p_vaddr = phdr.p_paddr = shdr.sh_addr.to_i
946
+ phdr.p_filesz = phdr.p_memsz = shdr.sh_size.to_i
947
+ end
948
+
949
+ def phdrs_by_type(seg_type)
950
+ return unless seg_type
951
+
952
+ @segments.each_with_index do |seg, idx|
953
+ next unless (phdr = seg.header).p_type == seg_type
954
+
955
+ yield phdr, idx
956
+ end
957
+ end
958
+
959
+ # Returns a blank shdr if the section doesn't exist.
960
+ def find_or_create_section_header(rsec_name)
961
+ shdr = find_section(rsec_name)&.header
962
+ shdr ||= ELFTools::Structs::ELF_Shdr.new(endian: endian, elf_class: elf_class)
963
+ shdr
964
+ end
965
+
966
+ def overwrite_replaced_sections
967
+ # the original source says this has to be done separately to
968
+ # prevent clobbering the previously written section contents.
969
+ @replaced_sections.each do |rsec_name, _|
970
+ shdr = find_section(rsec_name)&.header
971
+ next unless shdr
972
+
973
+ next if shdr.sh_type == ELFTools::Constants::SHT_NOBITS
974
+
975
+ with_buf_at(shdr.sh_offset) { |b| b.fill('X', shdr.sh_size) }
976
+ end
977
+ end
978
+
979
+ def write_section_aligment(shdr)
980
+ return if shdr.sh_type == ELFTools::Constants::SHT_NOTE && shdr.sh_addralign <= @section_alignment
981
+
982
+ shdr.sh_addralign = @section_alignment
983
+ end
984
+
985
+ def section_bounds_within_segment?(s_start, s_end, p_start, p_end)
986
+ (s_start >= p_start && s_start < p_end) || (s_end > p_start && s_end <= p_end)
987
+ end
988
+
989
+ def write_replaced_sections(cur_off, start_addr, start_offset)
990
+ overwrite_replaced_sections
991
+
992
+ noted_phdrs = Set.new
993
+
994
+ # the sort is necessary, the strategy in ruby and Cpp to iterate map/hash
995
+ # is different, patchelf v0.10 iterates the replaced_sections sorted by
996
+ # keys.
997
+ @replaced_sections.sort.each do |rsec_name, rsec_data|
998
+ shdr = find_or_create_section_header(rsec_name)
999
+
1000
+ Logger.debug <<~DEBUG
1001
+ rewriting section '#{rsec_name}'
1002
+ from offset 0x#{shdr.sh_offset.to_i.to_s 16}(size #{shdr.sh_size})
1003
+ to offset 0x#{cur_off.to_i.to_s 16}(size #{rsec_data.size})
1004
+ DEBUG
1005
+
1006
+ with_buf_at(cur_off) { |b| b.write rsec_data }
1007
+
1008
+ orig_sh_offset = shdr.sh_offset.to_i
1009
+ orig_sh_size = shdr.sh_size.to_i
1010
+
1011
+ shdr.sh_offset = cur_off
1012
+ shdr.sh_addr = start_addr + (cur_off - start_offset)
1013
+ shdr.sh_size = rsec_data.size
1014
+
1015
+ write_section_aligment(shdr)
1016
+
1017
+ seg_type = {
1018
+ '.interp' => ELFTools::Constants::PT_INTERP,
1019
+ '.dynamic' => ELFTools::Constants::PT_DYNAMIC,
1020
+ '.MIPS.abiflags' => ELFTools::Constants::PT_MIPS_ABIFLAGS,
1021
+ '.note.gnu.property' => ELFTools::Constants::PT_GNU_PROPERTY
1022
+ }[rsec_name]
1023
+
1024
+ phdrs_by_type(seg_type) { |phdr| sync_sec_to_seg(shdr, phdr) }
1025
+
1026
+ if shdr.sh_type == ELFTools::Constants::SHT_NOTE
1027
+ phdrs_by_type(ELFTools::Constants::PT_NOTE) do |phdr, idx|
1028
+ next if noted_phdrs.include?(idx)
1029
+
1030
+ s_start = orig_sh_offset
1031
+ s_end = s_start + orig_sh_size
1032
+ p_start = phdr.p_offset
1033
+ p_end = p_start + phdr.p_filesz
1034
+
1035
+ next unless section_bounds_within_segment?(s_start, s_end, p_start, p_end)
1036
+
1037
+ raise PatchError, 'unsupported overlap of SHT_NOTE and PT_NOTE' if p_start != s_start || p_end != s_end
1038
+
1039
+ sync_sec_to_seg(shdr, phdr)
1040
+
1041
+ noted_phdrs << idx
1042
+ end
1043
+ end
1044
+
1045
+ cur_off += Helper.alignup(rsec_data.size, @section_alignment)
1046
+ end
1047
+ @replaced_sections.clear
1048
+
1049
+ cur_off
1050
+ end
1051
+ end
1052
+ end