patchelf 1.2.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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