Merge pull request #8510 from Homebrew/dependabot/bundler/Library/Homebrew/patchelf-1.3.0

Bump patchelf from 1.2.0 to 1.3.0 in /Library/Homebrew
This commit is contained in:
Jonathan Chang 2020-08-27 23:57:45 +10:00 committed by GitHub
commit dfd0b1bf2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 880 additions and 28 deletions

View File

@ -23,7 +23,7 @@ GEM
docile (1.3.2) docile (1.3.2)
domain_name (0.5.20190701) domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
elftools (1.1.2) elftools (1.1.3)
bindata (~> 2) bindata (~> 2)
highline (2.0.3) highline (2.0.3)
hpricot (0.8.6) hpricot (0.8.6)
@ -64,8 +64,8 @@ GEM
sorbet-runtime (>= 0.5) sorbet-runtime (>= 0.5)
parser (2.7.1.4) parser (2.7.1.4)
ast (~> 2.4.1) ast (~> 2.4.1)
patchelf (1.2.0) patchelf (1.3.0)
elftools (~> 1.1) elftools (>= 1.1.3)
plist (3.5.0) plist (3.5.0)
pry (0.13.1) pry (0.13.1)
coderay (~> 1.1) coderay (~> 1.1)

View File

@ -31,7 +31,7 @@ $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/extensions/universal-darwi
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/unf_ext-0.0.7.7/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/unf_ext-0.0.7.7/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/unf-0.1.4/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/unf-0.1.4/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/domain_name-0.5.20190701/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/domain_name-0.5.20190701/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/elftools-1.1.2/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/elftools-1.1.3/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/extensions/universal-darwin-19/2.6.0/hpricot-0.8.6" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/extensions/universal-darwin-19/2.6.0/hpricot-0.8.6"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/hpricot-0.8.6/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/hpricot-0.8.6/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/http-cookie-1.0.3/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/http-cookie-1.0.3/lib"
@ -53,7 +53,7 @@ $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/parser-2.7.1.4/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rainbow-3.0.0/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/rainbow-3.0.0/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/sorbet-runtime-0.5.5880/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/sorbet-runtime-0.5.5880/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/parlour-4.0.1/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/parlour-4.0.1/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/patchelf-1.2.0/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/patchelf-1.3.0/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/plist-3.5.0/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/plist-3.5.0/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/pry-0.13.1/lib" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/gems/pry-0.13.1/lib"
$:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/extensions/universal-darwin-19/2.6.0/rdiscount-2.2.0.2" $:.unshift "#{path}/../#{ruby_engine}/#{ruby_version}/extensions/universal-darwin-19/2.6.0/rdiscount-2.2.0.2"

View File

@ -95,6 +95,7 @@ module ELFTools
DT_VERDEF = 0x6ffffffc # address of version definition table DT_VERDEF = 0x6ffffffc # address of version definition table
DT_VERDEFNUM = 0x6ffffffd # number of entries in {DT_VERDEF} DT_VERDEFNUM = 0x6ffffffd # number of entries in {DT_VERDEF}
DT_VERNEED = 0x6ffffffe # address of version dependency table DT_VERNEED = 0x6ffffffe # address of version dependency table
DT_VERSYM = 0x6ffffff0 # section address of .gnu.version
DT_VERNEEDNUM = 0x6fffffff # number of entries in {DT_VERNEED} DT_VERNEEDNUM = 0x6fffffff # number of entries in {DT_VERNEED}
# Values between {DT_LOPROC} and {DT_HIPROC} are reserved for processor-specific semantics. # Values between {DT_LOPROC} and {DT_HIPROC} are reserved for processor-specific semantics.
DT_LOPROC = 0x70000000 DT_LOPROC = 0x70000000
@ -212,6 +213,14 @@ module ELFTools
end end
include ET include ET
# Program header permission flags, records bitwise OR value in +p_flags+.
module PF
PF_X = 1
PF_W = 2
PF_R = 4
end
include PF
# Program header types, records in +p_type+. # Program header types, records in +p_type+.
module PT module PT
PT_NULL = 0 # null segment PT_NULL = 0 # null segment
@ -233,6 +242,14 @@ module ELFTools
end end
include PT include PT
# Special indices to section. These are used when there is no valid index to section header.
# The meaning of these values is left upto the embedding header.
module SHN
SHN_UNDEF = 0 # undefined section
SHN_LORESERVE = 0xff00 # start of reserved indices
end
include SHN
# Section header types, records in +sh_type+. # Section header types, records in +sh_type+.
module SHT module SHT
SHT_NULL = 0 # null section SHT_NULL = 0 # null section

View File

@ -56,7 +56,7 @@ module ELFTools
note = section.notes.first note = section.notes.first
return nil if note.nil? return nil if note.nil?
note.desc.unpack('H*').first note.desc.unpack1('H*')
end end
# Get machine architecture. # Get machine architecture.

View File

@ -2,5 +2,5 @@
module ELFTools module ELFTools
# Current gem version # Current gem version
VERSION = '1.1.2' VERSION = '1.1.3'
end end

View File

@ -0,0 +1,831 @@
# frozen_string_literal: true
require 'elftools/constants'
require 'elftools/elf_file'
require 'elftools/structs'
require 'elftools/util'
require 'fileutils'
require 'patchelf/helper'
#:nodoc:
module PatchELF
# TODO: refactor buf_* methods here
# TODO: move all refinements into a seperate file / helper file.
# refinements for cleaner syntax / speed / memory optimizations
module Refinements
refine StringIO do
# behaves like C memset. Equivalent to calling stream.write(char * nbytes)
# the benefit of preferring this over `stream.write(char * nbytes)` is only when data to be written is large.
# @param [String] char
# @param [Integer] nbytes
# @return[void]
def fill(char, nbytes)
at_once = Helper::PAGE_SIZE
pending = nbytes
if pending > at_once
to_write = char * at_once
while pending >= at_once
write(to_write)
pending -= at_once
end
end
write(char * pending) if pending.positive?
end
end
end
using Refinements
# Internal use only.
# alternative to +Saver+, that aims to be byte to byte equivalent with NixOS/patchelf.
#
# *DISCLAIMER*: This differs from +Saver+ in number of ways. No lazy reading,
# inconsistent use of existing internal API(e.g: manual reading of data instead of calling +section.data+)
# @private
class AltSaver
attr_reader :in_file # @return [String] Input filename.
attr_reader :out_file # @return [String] Output filename.
# Instantiate a {AltSaver} object.
# the params passed are the same as the ones passed to +Saver+
# @param [String] in_file
# @param [String] out_file
# @param [{Symbol => String, Array}] set
def initialize(in_file, out_file, set)
@in_file = in_file
@out_file = out_file
@set = set
f = File.open(in_file, 'rb')
# the +@buffer+ and +@elf+ both could work on same +StringIO+ stream,
# the updating of @buffer in place blocks us from looking up old values.
# TODO: cache the values needed later, use same stream for +@buffer+ and +@elf+.
# also be sure to update the stream offset passed to Segments::Segment.
@elf = ELFTools::ELFFile.new(f)
@buffer = StringIO.new(f.tap(&:rewind).read) # StringIO makes easier to work with Bindata
@ehdr = @elf.header
@endian = @elf.endian
@elf_class = @elf.elf_class
@segments = @elf.segments # usage similar to phdrs
@sections = @elf.sections # usage similar to shdrs
update_section_idx!
# {String => String}
# section name to its data mapping
@replaced_sections = {}
@section_alignment = ehdr.e_phoff.num_bytes
# using the same environment flag as patchelf, makes it easier for debugging
Logger.level = ::Logger.const_get(ENV['PATCHELF_DEBUG'] ? :DEBUG : :WARN)
end
# @return [void]
def save!
@set.each { |mtd, val| send(:"modify_#{mtd}") if val }
rewrite_sections
FileUtils.cp(in_file, out_file) if out_file != in_file
patch_out
# Let output file have the same permission as input.
FileUtils.chmod(File.stat(in_file).mode, out_file)
end
private
attr_reader :ehdr, :endian, :elf_class
def old_sections
@old_sections ||= @elf.sections
end
def buf_cstr(off)
cstr = []
with_buf_at(off) do |buf|
loop do
c = buf.read 1
break if c.nil? || c == "\x00"
cstr.push c
end
end
cstr.join
end
def buf_move!(dst_idx, src_idx, n_bytes)
with_buf_at(src_idx) do |buf|
to_write = buf.read(n_bytes)
buf.seek dst_idx
buf.write to_write
end
end
def dynstr
find_section '.dynstr'
end
# yields dynamic tag, and offset in buffer
def each_dynamic_tags
return unless block_given?
sec = find_section '.dynamic'
return unless sec
shdr = sec.header
with_buf_at(shdr.sh_offset) do |buf|
dyn = ELFTools::Structs::ELF_Dyn.new(elf_class: elf_class, endian: endian)
loop do
buf_dyn_offset = buf.tell
dyn.clear
dyn.read(buf)
break if dyn.d_tag == ELFTools::Constants::DT_NULL
yield dyn, buf_dyn_offset
# there's a possibility for caller to modify @buffer.pos, seek to avoid such issues
buf.seek buf_dyn_offset + dyn.num_bytes
end
end
end
# the idea of uniquely identifying section by its name has its problems
# but this is how patchelf operates and is prone to bugs.
# e.g: https://github.com/NixOS/patchelf/issues/197
def find_section(sec_name)
idx = find_section_idx sec_name
return unless idx
@sections[idx]
end
def find_section_idx(sec_name)
@section_idx_by_name[sec_name]
end
def buf_grow!(newsz)
bufsz = @buffer.size
return if newsz <= bufsz
@buffer.truncate newsz
end
def modify_interpreter
@replaced_sections['.interp'] = @set[:interpreter] + "\x00"
end
def modify_needed
# due to gsoc time constraints only implmenting features used by brew.
raise NotImplementedError
end
# not checking for nil as modify_rpath is only called if @set[:rpath]
def modify_rpath
modify_rpath_helper @set[:rpath], force_rpath: true
end
# not checking for nil as modify_runpath is only called if @set[:runpath]
def modify_runpath
modify_rpath_helper @set[:runpath]
end
def collect_runpath_tags
tags = {}
each_dynamic_tags do |dyn, off|
case dyn.d_tag
when ELFTools::Constants::DT_RPATH
tag_type = :rpath
when ELFTools::Constants::DT_RUNPATH
tag_type = :runpath
else
next
end
# clone does shallow copy, and for some reason d_tag and d_val can't be pass as argument
dyn_rpath = ELFTools::Structs::ELF_Dyn.new(endian: endian, elf_class: elf_class)
dyn_rpath.assign({ d_tag: dyn.d_tag.to_i, d_val: dyn.d_val.to_i })
tags[tag_type] = { offset: off, header: dyn_rpath }
end
tags
end
def resolve_rpath_tag_conflict(dyn_tags, force_rpath: false)
dyn_runpath, dyn_rpath = dyn_tags.values_at(:runpath, :rpath)
update_sym =
if !force_rpath && dyn_rpath && dyn_runpath.nil?
:runpath
elsif force_rpath && dyn_runpath
:rpath
end
return unless update_sym
delete_sym, = %i[rpath runpath] - [update_sym]
dyn_tag = dyn_tags[update_sym] = dyn_tags[delete_sym]
dyn = dyn_tag[:header]
dyn.d_tag = ELFTools::Constants.const_get("DT_#{update_sym.upcase}")
with_buf_at(dyn_tag[:offset]) { |buf| dyn.write(buf) }
dyn_tags.delete(delete_sym)
end
def modify_rpath_helper(new_rpath, force_rpath: false)
shdr_dynstr = dynstr.header
dyn_tags = collect_runpath_tags
resolve_rpath_tag_conflict(dyn_tags, force_rpath: force_rpath)
# (:runpath, :rpath) order_matters.
resolved_rpath_dyns = dyn_tags.values_at(:runpath, :rpath).compact
old_rpath = ''
rpath_off = nil
resolved_rpath_dyns.each do |dyn|
rpath_off = shdr_dynstr.sh_offset + dyn[:header].d_val
old_rpath = buf_cstr(rpath_off)
break
end
return if old_rpath == new_rpath
with_buf_at(rpath_off) { |b| b.write('X' * old_rpath.size) } if rpath_off
if new_rpath.size <= old_rpath.size
with_buf_at(rpath_off) { |b| b.write "#{new_rpath}\x00" }
return
end
Logger.debug 'rpath is too long, resizing...'
new_dynstr = replace_section '.dynstr', shdr_dynstr.sh_size + new_rpath.size + 1
new_rpath_strtab_idx = shdr_dynstr.sh_size.to_i
new_dynstr[new_rpath_strtab_idx..(new_rpath_strtab_idx + new_rpath.size)] = "#{new_rpath}\x00"
dyn_tags.each do |_, dyn|
dyn[:header].d_val = new_rpath_strtab_idx
with_buf_at(dyn[:offset]) { |b| dyn[:header].write(b) }
end
return unless dyn_tags.empty?
add_dt_rpath!(
d_tag: force_rpath ? ELFTools::Constants::DT_RPATH : ELFTools::Constants::DT_RUNPATH,
d_val: new_rpath_strtab_idx
)
end
def modify_soname
return unless ehdr.e_type == ELFTools::Constants::ET_DYN
# due to gsoc time constraints only implmenting features used by brew.
raise NotImplementedError
end
def add_segment!(**phdr_vals)
new_phdr = ELFTools::Structs::ELF_Phdr[elf_class].new(endian: endian, **phdr_vals)
# nil = no reference to stream; we only want @segments[i].header
new_segment = ELFTools::Segments::Segment.new(new_phdr, nil)
@segments.push new_segment
ehdr.e_phnum += 1
nil
end
def add_dt_rpath!(d_tag: nil, d_val: nil)
dyn_num_bytes = nil
dt_null_idx = 0
each_dynamic_tags do |dyn|
dyn_num_bytes ||= dyn.num_bytes
dt_null_idx += 1
end
# allot for new dt_runpath
shdr_dynamic = find_section('.dynamic').header
new_dynamic_data = replace_section '.dynamic', shdr_dynamic.sh_size + dyn_num_bytes
# consider DT_NULL when copying
replacement_size = (dt_null_idx + 1) * dyn_num_bytes
# make space for dt_runpath tag at the top, shift data by one tag positon
new_dynamic_data[dyn_num_bytes..(replacement_size + dyn_num_bytes)] = new_dynamic_data[0..replacement_size]
dyn_rpath = ELFTools::Structs::ELF_Dyn.new endian: endian, elf_class: elf_class
dyn_rpath.d_tag = d_tag
dyn_rpath.d_val = d_val
zi = StringIO.new
dyn_rpath.write zi
zi.rewind
new_dynamic_data[0...dyn_num_bytes] = zi.read
end
# given a index into old_sections table
# returns the corresponding section index in @sections
#
# raises ArgumentError if old_shndx can't be found in old_sections
# TODO: handle case of non existing section in (new) @sections.
def new_section_idx(old_shndx)
return if old_shndx == ELFTools::Constants::SHN_UNDEF || old_shndx >= ELFTools::Constants::SHN_LORESERVE
raise ArgumentError if old_shndx >= old_sections.count
old_sec = old_sections[old_shndx]
raise PatchError, "old_sections[#{shndx}] is nil" if old_sec.nil?
# TODO: handle case of non existing section in (new) @sections.
find_section_idx(old_sec.name)
end
def page_size
Helper::PAGE_SIZE
end
def patch_out
with_buf_at(0) { |b| ehdr.write(b) }
File.open(out_file, 'wb') do |f|
@buffer.rewind
f.write @buffer.read
end
end
# size includes NUL byte
def replace_section(section_name, size)
data = @replaced_sections[section_name]
unless data
shdr = find_section(section_name).header
# avoid calling +section.data+ as the @buffer contents may vary from
# the stream provided to section at initialization.
# ideally, calling section.data should work, however avoiding it to prevent
# future traps.
with_buf_at(shdr.sh_offset) { |b| data = b.read shdr.sh_size }
end
rep_data = if data.size == size
data
elsif data.size < size
data.ljust(size, "\x00")
else
data[0...size] + "\x00"
end
@replaced_sections[section_name] = rep_data
end
def write_phdrs_to_buf!
sort_phdrs!
with_buf_at(ehdr.e_phoff) do |buf|
@segments.each { |seg| seg.header.write(buf) }
end
end
def write_shdrs_to_buf!
raise PatchError, 'ehdr.e_shnum != @sections.count' if ehdr.e_shnum != @sections.count
sort_shdrs!
with_buf_at(ehdr.e_shoff) do |buf|
@sections.each { |section| section.header.write(buf) }
end
sync_dyn_tags!
end
# data for manual packing and unpacking of symbols in symtab sections.
def meta_sym_pack
return @meta_sym_pack if @meta_sym_pack
# resort to manual packing and unpacking of data,
# as using bindata is painfully slow :(
if elf_class == 32
sym_num_bytes = 16 # u32 u32 u32 u8 u8 u16
pack_code = endian == :little ? 'VVVCCv' : 'NNNCCn'
pack_st_info = 3
pack_st_shndx = 5
pack_st_value = 1
else # 64
sym_num_bytes = 24 # u32 u8 u8 u16 u64 u64
pack_code = endian == :little ? 'VCCvQ<Q<' : 'NCCnQ>Q>'
pack_st_info = 1
pack_st_shndx = 3
pack_st_value = 4
end
@meta_sym_pack = {
num_bytes: sym_num_bytes, code: pack_code,
st_info: pack_st_info, st_shndx: pack_st_shndx, st_value: pack_st_value
}
end
# yields +symbol+, +entry+
def each_symbol(shdr)
return unless [ELFTools::Constants::SHT_SYMTAB, ELFTools::Constants::SHT_DYNSYM].include?(shdr.sh_type)
pack_code, sym_num_bytes = meta_sym_pack.values_at(:code, :num_bytes)
with_buf_at(shdr.sh_offset) do |buf|
num_symbols = shdr.sh_size / sym_num_bytes
num_symbols.times do |entry|
sym = buf.read(sym_num_bytes).unpack(pack_code)
sym_modified = yield sym, entry
if sym_modified
buf.seek buf.tell - sym_num_bytes
buf.write sym.pack(pack_code)
end
end
end
end
def rewrite_headers(phdr_address)
# there can only be a single program header table according to ELF spec
@segments.find { |seg| seg.header.p_type == ELFTools::Constants::PT_PHDR }&.tap do |seg|
phdr = seg.header
phdr.p_offset = ehdr.e_phoff.to_i
phdr.p_vaddr = phdr.p_paddr = phdr_address.to_i
phdr.p_filesz = phdr.p_memsz = phdr.num_bytes * @segments.count # e_phentsize * e_phnum
end
write_phdrs_to_buf!
write_shdrs_to_buf!
pack = meta_sym_pack
@sections.each do |sec|
each_symbol(sec.header) do |sym, entry|
old_shndx = sym[pack[:st_shndx]]
begin
new_index = new_section_idx(old_shndx)
next unless new_index
rescue ArgumentError
Logger.warn "entry #{entry} in symbol table refers to a non existing section, skipping"
end
sym[pack[:st_shndx]] = new_index
# right 4 bits in the st_info field is st_type
if (sym[pack[:st_info]] & 0xF) == ELFTools::Constants::STT_SECTION
sym[pack[:st_value]] = @sections[new_index].header.sh_addr.to_i
end
true
end
end
end
def rewrite_sections
return if @replaced_sections.empty?
case ehdr.e_type
when ELFTools::Constants::ET_DYN
rewrite_sections_library
when ELFTools::Constants::ET_EXEC
rewrite_sections_executable
else
raise PatchError, 'unknown ELF type'
end
end
def replaced_section_indices
return enum_for(:replaced_section_indices) unless block_given?
last_replaced = 0
@sections.each_with_index do |sec, idx|
if @replaced_sections[sec.name]
last_replaced = idx
yield last_replaced
end
end
raise PatchError, 'last_replaced = 0' if last_replaced.zero?
raise PatchError, 'last_replaced + 1 >= @sections.size' if last_replaced + 1 >= @sections.size
end
def start_replacement_shdr
last_replaced = replaced_section_indices.max
start_replacement_hdr = @sections[last_replaced + 1].header
prev_sec_name = ''
(1..last_replaced).each do |idx|
sec = @sections[idx]
shdr = sec.header
if (sec.type == ELFTools::Constants::SHT_PROGBITS && sec.name != '.interp') || prev_sec_name == '.dynstr'
start_replacement_hdr = shdr
break
elsif @replaced_sections[sec.name].nil?
Logger.debug " replacing section #{sec.name} which is in the way"
replace_section(sec.name, shdr.sh_size)
end
prev_sec_name = sec.name
end
start_replacement_hdr
end
def copy_shdrs_to_eof
shoff_new = @buffer.size
# honestly idk why `ehdr.e_shoff` is considered when we are only moving shdrs.
sh_size = ehdr.e_shoff + ehdr.e_shnum * ehdr.e_shentsize
buf_grow! @buffer.size + sh_size
ehdr.e_shoff = shoff_new
raise PatchError, 'ehdr.e_shnum != @sections.size' if ehdr.e_shnum != @sections.size
with_buf_at(ehdr.e_shoff + @sections.first.header.num_bytes) do |buf| # skip writing to NULL section
@sections.each_with_index do |sec, idx|
next if idx.zero?
sec.header.write buf
end
end
end
def rewrite_sections_executable
sort_shdrs!
shdr = start_replacement_shdr
start_offset = shdr.sh_offset
start_addr = shdr.sh_addr
first_page = start_addr - start_offset
Logger.debug "first reserved offset/addr is 0x#{start_offset.to_i.to_s 16}/0x#{start_addr.to_i.to_s 16}"
unless start_addr % page_size == start_offset % page_size
raise PatchError, 'start_addr != start_offset (mod PAGE_SIZE)'
end
Logger.debug "first page is 0x#{first_page.to_i.to_s 16}"
copy_shdrs_to_eof if ehdr.e_shoff < start_offset
seg_num_bytes = @segments.first.header.num_bytes
needed_space = (
ehdr.num_bytes +
(@segments.count * seg_num_bytes) +
@replaced_sections.sum { |_, str| Helper.alignup(str.size, @section_alignment) }
)
if needed_space > start_offset
needed_space += seg_num_bytes # new load segment is required
needed_pages = Helper.alignup(needed_space - start_offset, page_size) / page_size
Logger.debug "needed pages is #{needed_pages}"
raise PatchError, 'virtual address space underrun' if needed_pages * page_size > first_page
first_page -= needed_pages * page_size
start_offset += needed_pages * page_size
shift_file(needed_pages, first_page)
end
Logger.debug "needed space is #{needed_space}"
cur_off = ehdr.num_bytes + (@segments.count * seg_num_bytes)
Logger.debug "clearing first #{start_offset - cur_off} bytes"
with_buf_at(cur_off) { |buf| buf.fill("\x00", (start_offset - cur_off)) }
cur_off = write_replaced_sections cur_off, first_page, 0
raise PatchError, "cur_off(#{cur_off}) != needed_space" if cur_off != needed_space
rewrite_headers first_page + ehdr.e_phoff
end
def replace_sections_in_the_way_of_phdr!
pht_size = ehdr.num_bytes + (@segments.count + 1) * @segments.first.header.num_bytes
# replace sections that may overlap with expanded program header table
@sections.each_with_index do |sec, idx|
shdr = sec.header
next if idx.zero? || @replaced_sections[sec.name]
break if shdr.sh_addr > pht_size
replace_section sec.name, shdr.sh_size
end
end
def seg_end_addr(seg)
phdr = seg.header
Helper.alignup(phdr.p_vaddr + phdr.p_memsz, page_size)
end
def rewrite_sections_library
start_page = seg_end_addr(@segments.max_by(&method(:seg_end_addr)))
Logger.debug "Last page is 0x#{start_page.to_s 16}"
replace_sections_in_the_way_of_phdr!
needed_space = @replaced_sections.sum { |_, str| Helper.alignup(str.size, @section_alignment) }
Logger.debug "needed space = #{needed_space}"
start_offset = Helper.alignup(@buffer.size, page_size)
buf_grow! start_offset + needed_space
# executable shared object
if start_offset > start_page && @segments.any? { |seg| seg.header.p_type == ELFTools::Constants::PT_INTERP }
Logger.debug(
"shifting new PT_LOAD segment by #{start_offset - start_page} bytes to work around a Linux kernel bug"
)
start_page = start_offset
end
ehdr.e_phoff = ehdr.num_bytes
add_segment!(
p_type: ELFTools::Constants::PT_LOAD,
p_offset: start_offset,
p_vaddr: start_page,
p_paddr: start_page,
p_filesz: needed_space,
p_memsz: needed_space,
p_flags: ELFTools::Constants::PF_R | ELFTools::Constants::PF_W,
p_align: page_size
)
cur_off = write_replaced_sections start_offset, start_page, start_offset
raise PatchError, 'cur_off != start_offset + needed_space' if cur_off != start_offset + needed_space
rewrite_headers ehdr.e_phoff
end
def shift_file(extra_pages, start_page)
oldsz = @buffer.size
shift = extra_pages * page_size
buf_grow!(oldsz + shift)
buf_move! shift, 0, oldsz
with_buf_at(ehdr.num_bytes) { |buf| buf.write "\x00" * (shift - ehdr.num_bytes) }
ehdr.e_phoff = ehdr.num_bytes
ehdr.e_shoff = ehdr.e_shoff + shift
@sections.each_with_index do |sec, i|
next if i.zero? # dont touch NULL section
shdr = sec.header
shdr.sh_offset += shift
end
@segments.each do |seg|
phdr = seg.header
phdr.p_offset += shift
phdr.p_align = page_size if phdr.p_align != 0 && (phdr.p_vaddr - phdr.p_offset) % phdr.p_align != 0
end
add_segment!(
p_type: ELFTools::Constants::PT_LOAD,
p_offset: 0,
p_vaddr: start_page,
p_paddr: start_page,
p_filesz: shift,
p_memsz: shift,
p_flags: ELFTools::Constants::PF_R | ELFTools::Constants::PF_W,
p_align: page_size
)
end
def sort_phdrs!
pt_phdr = ELFTools::Constants::PT_PHDR
@segments.sort! do |me, you|
next 1 if you.header.p_type == pt_phdr
next -1 if me.header.p_type == pt_phdr
me.header.p_paddr.to_i <=> you.header.p_paddr.to_i
end
end
# section headers may contain sh_info and sh_link values that are
# references to another section
def collect_section_to_section_refs
rel_syms = [ELFTools::Constants::SHT_REL, ELFTools::Constants::SHT_RELA]
# Translate sh_link, sh_info mappings to section names.
@sections.each_with_object({ linkage: {}, info: {} }) do |s, collected|
hdr = s.header
collected[:linkage][s.name] = @sections[hdr.sh_link].name if hdr.sh_link.nonzero?
collected[:info][s.name] = @sections[hdr.sh_info].name if hdr.sh_info.nonzero? && rel_syms.include?(hdr.sh_type)
end
end
# @param collected
# this must be the value returned by +collect_section_to_section_refs+
def restore_section_to_section_refs!(collected)
rel_syms = [ELFTools::Constants::SHT_REL, ELFTools::Constants::SHT_RELA]
linkage, info = collected.values_at(:linkage, :info)
@sections.each do |sec|
hdr = sec.header
hdr.sh_link = find_section_idx(linkage[sec.name]) if hdr.sh_link.nonzero?
hdr.sh_info = find_section_idx(info[sec.name]) if hdr.sh_info.nonzero? && rel_syms.include?(hdr.sh_type)
end
end
def sort_shdrs!
section_dep_values = collect_section_to_section_refs
shstrtab_name = @sections[ehdr.e_shstrndx].name
@sections.sort! { |me, you| me.header.sh_offset.to_i <=> you.header.sh_offset.to_i }
update_section_idx!
restore_section_to_section_refs!(section_dep_values)
ehdr.e_shstrndx = find_section_idx shstrtab_name
end
# given a +dyn.d_tag+, returns the section name it must be synced to.
# it may return nil, when given tag maps to no section,
# or when its okay to skip if section is not found.
def dyn_tag_to_section_name(d_tag)
case d_tag
when ELFTools::Constants::DT_STRTAB, ELFTools::Constants::DT_STRSZ
'.dynstr'
when ELFTools::Constants::DT_SYMTAB
'.dynsym'
when ELFTools::Constants::DT_HASH
'.hash'
when ELFTools::Constants::DT_GNU_HASH
'.gnu.hash'
when ELFTools::Constants::DT_JMPREL
sec_name = %w[.rel.plt .rela.plt .rela.IA_64.pltoff].find { |s| find_section(s) }
raise PatchError, 'cannot find section corresponding to DT_JMPREL' unless sec_name
sec_name
when ELFTools::Constants::DT_REL
# regarding .rel.got, NixOS/patchelf says
# "no idea if this makes sense, but it was needed for some program"
#
# return nil if not found, patchelf claims no problem in skipping
%w[.rel.dyn .rel.got].find { |s| find_section(s) }
when ELFTools::Constants::DT_RELA
# return nil if not found, patchelf claims no problem in skipping
find_section('.rela.dyn')&.name
when ELFTools::Constants::DT_VERNEED
'.gnu.version_r'
when ELFTools::Constants::DT_VERSYM
'.gnu.version'
end
end
# updates dyn tags by syncing it with @section values
def sync_dyn_tags!
each_dynamic_tags do |dyn, buf_off|
sec_name = dyn_tag_to_section_name(dyn.d_tag)
next unless sec_name
shdr = find_section(sec_name).header
dyn.d_val = dyn.d_tag == ELFTools::Constants::DT_STRSZ ? shdr.sh_size.to_i : shdr.sh_addr.to_i
with_buf_at(buf_off) { |wbuf| dyn.write(wbuf) }
end
end
def update_section_idx!
@section_idx_by_name = @sections.map.with_index { |sec, idx| [sec.name, idx] }.to_h
end
def with_buf_at(pos)
return unless block_given?
opos = @buffer.tell
@buffer.seek pos
yield @buffer
@buffer.seek opos
nil
end
def sync_sec_to_seg(shdr, phdr)
phdr.p_offset = shdr.sh_offset.to_i
phdr.p_vaddr = phdr.p_paddr = shdr.sh_addr.to_i
phdr.p_filesz = phdr.p_memsz = shdr.sh_size.to_i
end
def phdrs_by_type(seg_type)
return unless seg_type
@segments.each_with_index do |seg, idx|
next unless (phdr = seg.header).p_type == seg_type
yield phdr, idx
end
end
def write_replaced_sections(cur_off, start_addr, start_offset)
sht_no_bits = ELFTools::Constants::SHT_NOBITS
# the original source says this has to be done seperately to
# prevent clobbering the previously written section contents.
@replaced_sections.each do |rsec_name, _|
shdr = find_section(rsec_name).header
with_buf_at(shdr.sh_offset) { |b| b.fill('X', shdr.sh_size) } if shdr.sh_type != sht_no_bits
end
# the sort is necessary, the strategy in ruby and Cpp to iterate map/hash
# is different, patchelf v0.10 iterates the replaced_sections sorted by
# keys.
@replaced_sections.sort.each do |rsec_name, rsec_data|
section = find_section(rsec_name)
shdr = section.header
Logger.debug <<~DEBUG
rewriting section '#{rsec_name}'
from offset 0x#{shdr.sh_offset.to_i.to_s 16}(size #{shdr.sh_size})
to offset 0x#{cur_off.to_i.to_s 16}(size #{rsec_data.size})
DEBUG
with_buf_at(cur_off) { |b| b.write rsec_data }
shdr.sh_offset = cur_off
shdr.sh_addr = start_addr + (cur_off - start_offset)
shdr.sh_size = rsec_data.size
shdr.sh_addralign = @section_alignment
seg_type = {
'.interp' => ELFTools::Constants::PT_INTERP,
'.dynamic' => ELFTools::Constants::PT_DYNAMIC
}[section.name]
phdrs_by_type(seg_type) { |phdr| sync_sec_to_seg(shdr, phdr) }
cur_off += Helper.alignup(rsec_data.size, @section_alignment)
end
@replaced_sections.clear
cur_off
end
end
end

View File

@ -15,7 +15,7 @@ module PatchELF
end end
end end
%i[info warn error].each do |sym| %i[debug info warn error level=].each do |sym|
define_method(sym) do |msg| define_method(sym) do |msg|
@logger.__send__(sym, msg) @logger.__send__(sym, msg)
nil nil

View File

@ -173,13 +173,20 @@ module PatchELF
# Save the patched ELF as +out_file+. # Save the patched ELF as +out_file+.
# @param [String?] out_file # @param [String?] out_file
# If +out_file+ is +nil+, the original input file will be modified. # If +out_file+ is +nil+, the original input file will be modified.
# @param [Boolean] patchelf_compatible
# When +patchelf_compatible+ is true, tries to produce same ELF as the one produced by NixOS/patchelf.
# @return [void] # @return [void]
def save(out_file = nil) def save(out_file = nil, patchelf_compatible: false)
# If nothing is modified, return directly. # If nothing is modified, return directly.
return if out_file.nil? && !dirty? return if out_file.nil? && !dirty?
out_file ||= @in_file out_file ||= @in_file
saver = PatchELF::Saver.new(@in_file, out_file, @set) saver = if patchelf_compatible
require 'patchelf/alt_saver'
PatchELF::AltSaver.new(@in_file, out_file, @set)
else
PatchELF::Saver.new(@in_file, out_file, @set)
end
saver.save! saver.save!
end end

View File

@ -121,30 +121,27 @@ module PatchELF
def patch_needed def patch_needed
original_needs = dynamic.tags_by_type(:needed) original_needs = dynamic.tags_by_type(:needed)
@set[:needed].uniq! @set[:needed].uniq!
original = original_needs.map(&:name)
replace = @set[:needed]
# 3 sets: # 3 sets:
# 1. in original and in needs - remain unchanged # 1. in original and in needs - remain unchanged
# 2. in original but not in needs - remove # 2. in original but not in needs - remove
# 3. not in original and in needs - append # 3. not in original and in needs - append
original_needs.each do |n| append = replace - original
next if @set[:needed].include?(n.name) remove = original - replace
n.header.d_tag = IGNORE # temporarily mark ignored_dyns = remove.each_with_object([]) do |name, ignored|
dyn = original_needs.find { |n| n.name == name }.header
dyn.d_tag = IGNORE
ignored << dyn
end end
extra = @set[:needed] - original_needs.map(&:name) append.zip(ignored_dyns) do |name, ignored_dyn|
original_needs.each do |n| dyn = ignored_dyn || lazy_dyn(:needed)
break if extra.empty? dyn.d_tag = ELFTools::Constants::DT_NEEDED
next if n.header.d_tag != IGNORE reg_str_table(name) { |idx| dyn.d_val = idx }
n.header.d_tag = ELFTools::Constants::DT_NEEDED
reg_str_table(extra.shift) { |idx| n.header.d_val = idx }
end
return if extra.empty?
# no spaces, need append
extra.each do |name|
tag = lazy_dyn(:needed)
reg_str_table(name) { |idx| tag.d_val = idx }
end end
end end

View File

@ -2,5 +2,5 @@
module PatchELF module PatchELF
# Current gem version. # Current gem version.
VERSION = '1.2.0'.freeze VERSION = '1.3.0'.freeze
end end