2024-08-12 10:30:59 +01:00
|
|
|
# typed: true # rubocop:todo Sorbet/StrictSigil
|
2019-04-19 15:38:03 +09:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2024-04-24 02:23:13 +08:00
|
|
|
require "os/linux/ld"
|
|
|
|
|
2020-08-25 00:11:59 +02:00
|
|
|
# {Pathname} extension for dealing with ELF files.
|
2018-10-18 21:42:43 -04:00
|
|
|
# @see https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header
|
2017-12-01 16:29:50 -08:00
|
|
|
module ELFShim
|
2024-10-06 09:25:57 -07:00
|
|
|
extend T::Helpers
|
|
|
|
|
2017-12-01 16:29:50 -08:00
|
|
|
MAGIC_NUMBER_OFFSET = 0
|
2020-08-25 00:11:59 +02:00
|
|
|
private_constant :MAGIC_NUMBER_OFFSET
|
2019-04-19 15:38:03 +09:00
|
|
|
MAGIC_NUMBER_ASCII = "\x7fELF"
|
2020-08-25 00:11:59 +02:00
|
|
|
private_constant :MAGIC_NUMBER_ASCII
|
2017-12-01 16:29:50 -08:00
|
|
|
|
|
|
|
OS_ABI_OFFSET = 0x07
|
2020-08-25 00:11:59 +02:00
|
|
|
private_constant :OS_ABI_OFFSET
|
2017-12-01 16:29:50 -08:00
|
|
|
OS_ABI_SYSTEM_V = 0
|
2020-08-25 00:11:59 +02:00
|
|
|
private_constant :OS_ABI_SYSTEM_V
|
2017-12-01 16:29:50 -08:00
|
|
|
OS_ABI_LINUX = 3
|
2020-08-25 00:11:59 +02:00
|
|
|
private_constant :OS_ABI_LINUX
|
2017-12-01 16:29:50 -08:00
|
|
|
|
|
|
|
TYPE_OFFSET = 0x10
|
2020-08-25 00:11:59 +02:00
|
|
|
private_constant :TYPE_OFFSET
|
2017-12-01 16:29:50 -08:00
|
|
|
TYPE_EXECUTABLE = 2
|
2020-08-25 00:11:59 +02:00
|
|
|
private_constant :TYPE_EXECUTABLE
|
2017-12-01 16:29:50 -08:00
|
|
|
TYPE_SHARED = 3
|
2020-08-25 00:11:59 +02:00
|
|
|
private_constant :TYPE_SHARED
|
2017-12-01 16:29:50 -08:00
|
|
|
|
|
|
|
ARCHITECTURE_OFFSET = 0x12
|
2020-08-25 00:11:59 +02:00
|
|
|
private_constant :ARCHITECTURE_OFFSET
|
2017-12-01 16:29:50 -08:00
|
|
|
ARCHITECTURE_I386 = 0x3
|
2020-08-25 00:11:59 +02:00
|
|
|
private_constant :ARCHITECTURE_I386
|
2017-12-01 16:29:50 -08:00
|
|
|
ARCHITECTURE_POWERPC = 0x14
|
2020-08-25 00:11:59 +02:00
|
|
|
private_constant :ARCHITECTURE_POWERPC
|
2024-10-01 05:32:58 +01:00
|
|
|
ARCHITECTURE_POWERPC64 = 0x15
|
|
|
|
private_constant :ARCHITECTURE_POWERPC64
|
2017-12-01 16:29:50 -08:00
|
|
|
ARCHITECTURE_ARM = 0x28
|
2020-08-25 00:11:59 +02:00
|
|
|
private_constant :ARCHITECTURE_ARM
|
2021-07-18 16:55:57 +08:00
|
|
|
ARCHITECTURE_X86_64 = 0x3E
|
2020-08-25 00:11:59 +02:00
|
|
|
private_constant :ARCHITECTURE_X86_64
|
2017-12-01 16:29:50 -08:00
|
|
|
ARCHITECTURE_AARCH64 = 0xB7
|
2020-08-25 00:11:59 +02:00
|
|
|
private_constant :ARCHITECTURE_AARCH64
|
2017-12-01 16:29:50 -08:00
|
|
|
|
2024-10-06 09:25:57 -07:00
|
|
|
requires_ancestor { Pathname }
|
|
|
|
|
2025-07-11 08:02:27 +00:00
|
|
|
def initialize(*args)
|
|
|
|
@elf = T.let(nil, T.nilable(T::Boolean))
|
|
|
|
@arch = T.let(nil, T.nilable(Symbol))
|
|
|
|
@elf_type = T.let(nil, T.nilable(Symbol))
|
|
|
|
@rpath = T.let(nil, T.nilable(String))
|
|
|
|
@interpreter = T.let(nil, T.nilable(String))
|
|
|
|
@dynamic_elf = T.let(nil, T.nilable(T::Boolean))
|
|
|
|
@metadata = T.let(nil, T.nilable(Metadata))
|
|
|
|
@patchelf_patcher = nil
|
|
|
|
|
|
|
|
super
|
|
|
|
end
|
|
|
|
|
2017-12-01 16:29:50 -08:00
|
|
|
def read_uint8(offset)
|
2019-10-13 10:13:42 +01:00
|
|
|
read(1, offset).unpack1("C")
|
2017-12-01 16:29:50 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
def read_uint16(offset)
|
2019-10-13 10:13:42 +01:00
|
|
|
read(2, offset).unpack1("v")
|
2017-12-01 16:29:50 -08:00
|
|
|
end
|
|
|
|
|
2025-07-11 08:02:27 +00:00
|
|
|
sig { returns(T::Boolean) }
|
2017-12-01 16:29:50 -08:00
|
|
|
def elf?
|
2025-07-11 08:02:27 +00:00
|
|
|
return @elf unless @elf.nil?
|
|
|
|
|
2023-04-18 15:06:50 -07:00
|
|
|
return @elf = false if read(MAGIC_NUMBER_ASCII.size, MAGIC_NUMBER_OFFSET) != MAGIC_NUMBER_ASCII
|
2017-12-01 16:29:50 -08:00
|
|
|
|
|
|
|
# Check that this ELF file is for Linux or System V.
|
|
|
|
# OS_ABI is often set to 0 (System V), regardless of the target platform.
|
|
|
|
@elf = [OS_ABI_LINUX, OS_ABI_SYSTEM_V].include? read_uint8(OS_ABI_OFFSET)
|
|
|
|
end
|
|
|
|
|
2025-07-11 08:02:27 +00:00
|
|
|
sig { returns(Symbol) }
|
2017-12-01 16:29:50 -08:00
|
|
|
def arch
|
|
|
|
return :dunno unless elf?
|
|
|
|
|
|
|
|
@arch ||= case read_uint16(ARCHITECTURE_OFFSET)
|
|
|
|
when ARCHITECTURE_I386 then :i386
|
|
|
|
when ARCHITECTURE_X86_64 then :x86_64
|
2024-10-01 05:32:58 +01:00
|
|
|
when ARCHITECTURE_POWERPC then :ppc32
|
|
|
|
when ARCHITECTURE_POWERPC64 then :ppc64
|
2017-12-01 16:29:50 -08:00
|
|
|
when ARCHITECTURE_ARM then :arm
|
|
|
|
when ARCHITECTURE_AARCH64 then :arm64
|
|
|
|
else :dunno
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-09-29 05:15:36 +08:00
|
|
|
def arch_compatible?(wanted_arch)
|
|
|
|
return true unless elf?
|
|
|
|
|
2024-10-01 05:32:58 +01:00
|
|
|
# Treat ppc64le and ppc64 the same
|
|
|
|
wanted_arch = :ppc64 if wanted_arch == :ppc64le
|
|
|
|
|
2024-09-29 05:15:36 +08:00
|
|
|
wanted_arch == arch
|
|
|
|
end
|
|
|
|
|
2025-07-11 08:02:27 +00:00
|
|
|
sig { returns(Symbol) }
|
2017-12-01 16:29:50 -08:00
|
|
|
def elf_type
|
|
|
|
return :dunno unless elf?
|
|
|
|
|
|
|
|
@elf_type ||= case read_uint16(TYPE_OFFSET)
|
|
|
|
when TYPE_EXECUTABLE then :executable
|
|
|
|
when TYPE_SHARED then :dylib
|
|
|
|
else :dunno
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def dylib?
|
|
|
|
elf_type == :dylib
|
|
|
|
end
|
|
|
|
|
|
|
|
def binary_executable?
|
|
|
|
elf_type == :executable
|
|
|
|
end
|
|
|
|
|
2021-12-17 08:14:44 -08:00
|
|
|
# The runtime search path, such as:
|
|
|
|
# "/lib:/usr/lib:/usr/local/lib"
|
2025-07-11 08:02:27 +00:00
|
|
|
sig { returns(T.nilable(String)) }
|
2020-07-10 06:31:31 +05:30
|
|
|
def rpath
|
2025-07-11 08:02:27 +00:00
|
|
|
@rpath ||= rpath_using_patchelf_rb
|
2020-07-10 06:31:31 +05:30
|
|
|
end
|
|
|
|
|
2021-12-17 08:14:44 -08:00
|
|
|
# An array of runtime search path entries, such as:
|
|
|
|
# ["/lib", "/usr/lib", "/usr/local/lib"]
|
|
|
|
def rpaths
|
2022-08-26 22:29:55 +08:00
|
|
|
Array(rpath&.split(":"))
|
2021-12-17 08:14:44 -08:00
|
|
|
end
|
|
|
|
|
2025-07-11 08:02:27 +00:00
|
|
|
sig { returns(T.nilable(String)) }
|
2020-06-23 03:18:29 +05:30
|
|
|
def interpreter
|
2025-07-11 08:02:27 +00:00
|
|
|
@interpreter ||= patchelf_patcher.interpreter
|
2019-07-18 15:22:09 +08:00
|
|
|
end
|
|
|
|
|
2020-08-05 20:32:37 +05:30
|
|
|
def patch!(interpreter: nil, rpath: nil)
|
|
|
|
return if interpreter.blank? && rpath.blank?
|
|
|
|
|
2021-10-19 15:15:13 +01:00
|
|
|
save_using_patchelf_rb interpreter, rpath
|
2020-08-05 20:32:37 +05:30
|
|
|
end
|
|
|
|
|
2025-07-11 08:02:27 +00:00
|
|
|
sig { returns(T::Boolean) }
|
2017-12-01 16:29:50 -08:00
|
|
|
def dynamic_elf?
|
2025-07-11 08:02:27 +00:00
|
|
|
@dynamic_elf ||= patchelf_patcher.elf.segment_by_type(:DYNAMIC).present?
|
2017-12-01 16:29:50 -08:00
|
|
|
end
|
|
|
|
|
2020-08-25 00:11:59 +02:00
|
|
|
# Helper class for reading metadata from an ELF file.
|
2017-12-01 16:29:50 -08:00
|
|
|
class Metadata
|
2025-07-11 08:02:27 +00:00
|
|
|
sig { returns(ELFShim) }
|
|
|
|
attr_reader :path
|
|
|
|
|
|
|
|
sig { returns(T.nilable(String)) }
|
|
|
|
attr_reader :dylib_id
|
|
|
|
|
|
|
|
sig { returns(T::Array[String]) }
|
|
|
|
attr_reader :dylibs
|
2017-12-01 16:29:50 -08:00
|
|
|
|
2025-07-11 08:02:27 +00:00
|
|
|
sig { params(path: ELFShim).void }
|
2017-12-01 16:29:50 -08:00
|
|
|
def initialize(path)
|
2025-07-11 08:02:27 +00:00
|
|
|
@path = T.let(path, ELFShim)
|
|
|
|
@dylibs = T.let([], T::Array[String])
|
|
|
|
@dylib_id = T.let(nil, T.nilable(String))
|
2017-12-01 16:29:50 -08:00
|
|
|
@dylib_id, needed = needed_libraries path
|
2025-07-11 08:02:27 +00:00
|
|
|
@dylibs = needed.map { |lib| find_full_lib_path(lib).to_s } if needed.present?
|
2017-12-01 16:29:50 -08:00
|
|
|
|
2025-07-11 08:02:27 +00:00
|
|
|
@metadata = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
|
2017-12-01 16:29:50 -08:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def needed_libraries(path)
|
2020-07-26 04:43:30 +05:30
|
|
|
return [nil, []] unless path.dynamic_elf?
|
|
|
|
|
2020-08-08 06:41:51 +05:30
|
|
|
needed_libraries_using_patchelf_rb path
|
2017-12-01 16:29:50 -08:00
|
|
|
end
|
|
|
|
|
2020-06-02 18:36:13 +05:30
|
|
|
def needed_libraries_using_patchelf_rb(path)
|
|
|
|
patcher = path.patchelf_patcher
|
2020-07-10 06:31:31 +05:30
|
|
|
[patcher.soname, patcher.needed]
|
2020-06-02 18:36:13 +05:30
|
|
|
end
|
2024-04-24 02:23:13 +08:00
|
|
|
|
|
|
|
def find_full_lib_path(basename)
|
|
|
|
local_paths = (path.patchelf_patcher.runpath || path.patchelf_patcher.rpath)&.split(":")
|
|
|
|
|
|
|
|
# Search for dependencies in the runpath/rpath first
|
|
|
|
local_paths&.each do |local_path|
|
2024-04-24 23:56:55 -04:00
|
|
|
local_path = OS::Linux::Elf.expand_elf_dst(local_path, "ORIGIN", path.parent)
|
2024-04-24 02:23:13 +08:00
|
|
|
candidate = Pathname(local_path)/basename
|
|
|
|
return candidate if candidate.exist? && candidate.elf?
|
|
|
|
end
|
|
|
|
|
|
|
|
# Check if DF_1_NODEFLIB is set
|
|
|
|
dt_flags_1 = path.patchelf_patcher.elf.segment_by_type(:dynamic)&.tag_by_type(:flags_1)
|
|
|
|
nodeflib_flag = if dt_flags_1.nil?
|
|
|
|
false
|
|
|
|
else
|
|
|
|
dt_flags_1.value & ELFTools::Constants::DF::DF_1_NODEFLIB != 0
|
|
|
|
end
|
|
|
|
|
|
|
|
linker_library_paths = OS::Linux::Ld.library_paths
|
|
|
|
linker_system_dirs = OS::Linux::Ld.system_dirs
|
|
|
|
|
|
|
|
# If DF_1_NODEFLIB is set, exclude any library paths that are subdirectories
|
|
|
|
# of the system dirs
|
|
|
|
if nodeflib_flag
|
|
|
|
linker_library_paths = linker_library_paths.reject do |lib_path|
|
|
|
|
linker_system_dirs.any? { |system_dir| Utils::Path.child_of? system_dir, lib_path }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# If not found, search recursively in the paths listed in ld.so.conf (skipping
|
|
|
|
# paths that are subdirectories of the system dirs if DF_1_NODEFLIB is set)
|
|
|
|
linker_library_paths.each do |linker_library_path|
|
|
|
|
candidate = Pathname(linker_library_path)/basename
|
|
|
|
return candidate if candidate.exist? && candidate.elf?
|
|
|
|
end
|
|
|
|
|
|
|
|
# If not found, search in the system dirs, unless DF_1_NODEFLIB is set
|
|
|
|
unless nodeflib_flag
|
|
|
|
linker_system_dirs.each do |linker_system_dir|
|
|
|
|
candidate = Pathname(linker_system_dir)/basename
|
|
|
|
return candidate if candidate.exist? && candidate.elf?
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
basename
|
|
|
|
end
|
2017-12-01 16:29:50 -08:00
|
|
|
end
|
2020-08-25 00:11:59 +02:00
|
|
|
private_constant :Metadata
|
2017-12-01 16:29:50 -08:00
|
|
|
|
2020-08-05 20:32:37 +05:30
|
|
|
def save_using_patchelf_rb(new_interpreter, new_rpath)
|
|
|
|
patcher = patchelf_patcher
|
|
|
|
patcher.interpreter = new_interpreter if new_interpreter.present?
|
|
|
|
patcher.rpath = new_rpath if new_rpath.present?
|
|
|
|
patcher.save(patchelf_compatible: true)
|
|
|
|
end
|
|
|
|
|
2020-07-10 06:31:31 +05:30
|
|
|
def rpath_using_patchelf_rb
|
|
|
|
patchelf_patcher.runpath || patchelf_patcher.rpath
|
|
|
|
end
|
|
|
|
|
2020-06-02 18:36:13 +05:30
|
|
|
def patchelf_patcher
|
2020-07-28 23:09:13 +05:30
|
|
|
require "patchelf"
|
2020-08-05 20:32:37 +05:30
|
|
|
@patchelf_patcher ||= ::PatchELF::Patcher.new to_s, on_error: :silent
|
2020-06-02 18:36:13 +05:30
|
|
|
end
|
|
|
|
|
2025-07-11 08:02:27 +00:00
|
|
|
sig { returns(Metadata) }
|
2017-12-01 16:29:50 -08:00
|
|
|
def metadata
|
|
|
|
@metadata ||= Metadata.new(self)
|
|
|
|
end
|
2020-08-25 00:11:59 +02:00
|
|
|
private :metadata
|
2017-12-01 16:29:50 -08:00
|
|
|
|
|
|
|
def dylib_id
|
|
|
|
metadata.dylib_id
|
|
|
|
end
|
|
|
|
|
|
|
|
def dynamically_linked_libraries(*)
|
|
|
|
metadata.dylibs
|
|
|
|
end
|
|
|
|
end
|
2024-04-24 23:56:55 -04:00
|
|
|
|
|
|
|
module OS
|
|
|
|
module Linux
|
|
|
|
# Helper functions for working with ELF objects.
|
|
|
|
#
|
|
|
|
# @api private
|
|
|
|
module Elf
|
|
|
|
sig { params(str: String, ref: String, repl: T.any(String, Pathname)).returns(String) }
|
|
|
|
def self.expand_elf_dst(str, ref, repl)
|
|
|
|
# ELF gABI rules for DSTs:
|
|
|
|
# - Longest possible sequence using the rules (greedy).
|
|
|
|
# - Must start with a $ (enforced by caller).
|
|
|
|
# - Must follow $ with one underscore or ASCII [A-Za-z] (caller
|
|
|
|
# follows these rules for REF) or '{' (start curly quoted name).
|
|
|
|
# - Must follow first two characters with zero or more [A-Za-z0-9_]
|
|
|
|
# (enforced by caller) or '}' (end curly quoted name).
|
|
|
|
# (from https://github.com/bminor/glibc/blob/41903cb6f460d62ba6dd2f4883116e2a624ee6f8/elf/dl-load.c#L182-L228)
|
|
|
|
|
|
|
|
# In addition to capturing a token, also attempt to capture opening/closing braces and check that they are not
|
|
|
|
# mismatched before expanding.
|
|
|
|
str.gsub(/\$({?)([a-zA-Z_][a-zA-Z0-9_]*)(}?)/) do |orig_str|
|
|
|
|
has_opening_brace = ::Regexp.last_match(1).present?
|
|
|
|
matched_text = ::Regexp.last_match(2)
|
|
|
|
has_closing_brace = ::Regexp.last_match(3).present?
|
|
|
|
if (matched_text == ref) && (has_opening_brace == has_closing_brace)
|
|
|
|
repl
|
|
|
|
else
|
|
|
|
orig_str
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|