Skip to content

Commit

Permalink
os/linux/elf: avoid using ldd for listing dynamic dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
alebcay committed Mar 25, 2024
1 parent 92a4311 commit b692204
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 13 deletions.
6 changes: 6 additions & 0 deletions Library/Homebrew/extend/pathname.rb
Expand Up @@ -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

Check warning on line 497 in Library/Homebrew/extend/pathname.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/extend/pathname.rb#L497

Added line #L497 was not covered by tests
end
end

require "extend/os/pathname"
Expand Down
73 changes: 60 additions & 13 deletions 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
#
Expand Down Expand Up @@ -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
Expand All @@ -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

Check warning on line 181 in Library/Homebrew/os/linux/elf.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/os/linux/elf.rb#L181

Added line #L181 was not covered by tests

linker_system_dirs.any? do |linker_system_dir|
parent = Pathname(linker_system_dir).realdirpath
child.child_of? parent

Check warning on line 185 in Library/Homebrew/os/linux/elf.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/os/linux/elf.rb#L183-L185

Added lines #L183 - L185 were not covered by tests
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

Check warning on line 200 in Library/Homebrew/os/linux/elf.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/os/linux/elf.rb#L200

Added line #L200 was not covered by tests
return candidate if candidate.exist? && candidate.elf?
end
end

basename

Check warning on line 205 in Library/Homebrew/os/linux/elf.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/os/linux/elf.rb#L205

Added line #L205 was not covered by tests
end
end
private_constant :Metadata

Expand Down
77 changes: 77 additions & 0 deletions 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
9 changes: 9 additions & 0 deletions Library/Homebrew/os/linux/ld.rbi
@@ -0,0 +1,9 @@
# typed: strict

module OS
module Linux
module Ld
requires_ancestor { Pathname }
end
end
end

0 comments on commit b692204

Please sign in to comment.