From b69220447f2fd3accd0ca6345457edaff749da83 Mon Sep 17 00:00:00 2001 From: Caleb Xu Date: Fri, 22 Mar 2024 23:31:29 -0400 Subject: [PATCH] os/linux/elf: avoid using ldd for listing dynamic dependencies --- Library/Homebrew/extend/pathname.rb | 6 +++ Library/Homebrew/os/linux/elf.rb | 73 ++++++++++++++++++++++----- Library/Homebrew/os/linux/ld.rb | 77 +++++++++++++++++++++++++++++ Library/Homebrew/os/linux/ld.rbi | 9 ++++ 4 files changed, 152 insertions(+), 13 deletions(-) create mode 100644 Library/Homebrew/os/linux/ld.rb create mode 100644 Library/Homebrew/os/linux/ld.rbi diff --git a/Library/Homebrew/extend/pathname.rb b/Library/Homebrew/extend/pathname.rb index 5f363c088d194f..5e0bb9cea25b21 100644 --- a/Library/Homebrew/extend/pathname.rb +++ b/Library/Homebrew/extend/pathname.rb @@ -490,6 +490,12 @@ def zipinfo .encode(Encoding::UTF_8, invalid: :replace) .split("\n") end + + sig { params(parent: Pathname).returns(T::Boolean) } + def child_of?(parent) + ascend { |p| return true if p == parent } + false + end end require "extend/os/pathname" diff --git a/Library/Homebrew/os/linux/elf.rb b/Library/Homebrew/os/linux/elf.rb index efd855db952ca3..70e68ae7e58ad9 100644 --- a/Library/Homebrew/os/linux/elf.rb +++ b/Library/Homebrew/os/linux/elf.rb @@ -1,6 +1,8 @@ # typed: true # frozen_string_literal: true +require "os/linux/ld" + # {Pathname} extension for dealing with ELF files. # @see https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header # @@ -130,19 +132,7 @@ def initialize(path) @dylib_id, needed = needed_libraries path return if needed.empty? - ldd = DevelopmentTools.locate "ldd" - ldd_output = Utils.popen_read(ldd, path.expand_path.to_s).split("\n") - return unless $CHILD_STATUS.success? - - ldd_paths = ldd_output.filter_map do |line| - match = line.match(/\t.+ => (.+) \(.+\)|\t(.+) => not found/) - next unless match - - match.captures.compact.first - end - @dylibs = ldd_paths.select do |ldd_path| - needed.include? File.basename(ldd_path) - end + @dylibs = needed.map { |lib| find_full_lib_path(lib).to_s } end private @@ -157,6 +147,63 @@ def needed_libraries_using_patchelf_rb(path) patcher = path.patchelf_patcher [patcher.soname, patcher.needed] end + + def dt_flags_1(path) + readelf = DevelopmentTools.locate "readelf" + readelf_output = Utils.popen_read(readelf, "--dynamic", path.expand_path.to_s) + return [] unless $CHILD_STATUS.success? + + match = readelf_output.match(/^\s*0x0*6ffffffb\s+\(FLAGS_1\)\s+Flags:\s*(.*)$/) + return [] unless match + + match.captures.compact.first.split(/\s+/) + end + + 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| + candidate = Pathname(local_path)/basename + return candidate if candidate.exist? && candidate.elf? + end + + # Check if DF_1_NODEFLIB is set + nodeflib_flag = dt_flags_1(path).include?("NODEFLIB") + + 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 |linker_library_path| + child = Pathname(linker_library_path).realdirpath + + linker_system_dirs.any? do |linker_system_dir| + parent = Pathname(linker_system_dir).realdirpath + child.child_of? parent + end + 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 end private_constant :Metadata diff --git a/Library/Homebrew/os/linux/ld.rb b/Library/Homebrew/os/linux/ld.rb new file mode 100644 index 00000000000000..1d55c862c2e28d --- /dev/null +++ b/Library/Homebrew/os/linux/ld.rb @@ -0,0 +1,77 @@ +# typed: true +# frozen_string_literal: true + +module OS + module Linux + # Helper functions for querying `ld` information. + # + # @api private + module Ld + module_function + + sig { returns(String) } + def sysconfdir + fallback_sysconfdir = "/etc" + + brewed_ld_so = HOMEBREW_PREFIX/"lib/ld.so" + return fallback_sysconfdir unless brewed_ld_so.exist? + + ld_so_output = Utils.popen_read(brewed_ld_so, "--list-diagnostics") + return fallback_sysconfdir unless $CHILD_STATUS.success? + + match = ld_so_output.match(/path.sysconfdir="(.+)"/) + return fallback_sysconfdir unless match + + match.captures.compact.first + end + + sig { returns(T::Array[String]) } + def system_dirs + system_dirs = [] + + brewed_ld_so = HOMEBREW_PREFIX/"lib/ld.so" + return system_dirs unless brewed_ld_so.exist? + + ld_so_output = Utils.popen_read(brewed_ld_so, "--list-diagnostics").split("\n") + return system_dirs unless $CHILD_STATUS.success? + + ld_so_output.each do |line| + match = line.match(/path.system_dirs\[0x.*\]="(.*)"/) + next unless match + + system_dirs << match.captures.compact.first + end + + system_dirs + end + + sig { params(conf_path: T.any(String, Pathname)).returns(T::Array[String]) } + def library_paths(conf_path = Pathname(sysconfdir)/"ld.so.conf") + conf_file = Pathname(conf_path) + paths = Set.new + directory = conf_file.realpath.dirname + + conf_file.readlines.each do |line| + # Remove comments and leading/trailing whitespace + line.strip! + line.sub!(/\s*#.*$/, "") + + if line.start_with?(/\s*include\s+/) + include_path = Pathname(line.sub(/^\s*include\s+/, "")).expand_path + wildcard = include_path.absolute? ? include_path : directory/include_path + + Dir.glob(wildcard.to_s).each do |include_file| + paths += library_paths(include_file) + end + elsif line.empty? + next + else + paths << line + end + end + + paths.to_a + end + end + end +end diff --git a/Library/Homebrew/os/linux/ld.rbi b/Library/Homebrew/os/linux/ld.rbi new file mode 100644 index 00000000000000..2d517c0ebdedfe --- /dev/null +++ b/Library/Homebrew/os/linux/ld.rbi @@ -0,0 +1,9 @@ +# typed: strict + +module OS + module Linux + module Ld + requires_ancestor { Pathname } + end + end +end