Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

os/linux/elf: avoid using ldd for listing dynamic dependencies #16941

Merged
merged 3 commits into from Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
62 changes: 49 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 @@
@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,52 @@
patcher = path.patchelf_patcher
[patcher.soname, patcher.needed]
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
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 }

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L175 was not covered by tests
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 189 in Library/Homebrew/os/linux/elf.rb

View check run for this annotation

Codecov / codecov/patch

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

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

basename

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

View check run for this annotation

Codecov / codecov/patch

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

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

Expand Down
74 changes: 74 additions & 0 deletions Library/Homebrew/os/linux/ld.rb
@@ -0,0 +1,74 @@
# typed: strict
# frozen_string_literal: true

module OS
module Linux
# Helper functions for querying `ld` information.
#
# @api private
module Ld
sig { returns(String) }
def self.brewed_ld_so_diagnostics
brewed_ld_so = HOMEBREW_PREFIX/"lib/ld.so"
return "" unless brewed_ld_so.exist?

ld_so_output = Utils.popen_read(brewed_ld_so, "--list-diagnostics")
return "" unless $CHILD_STATUS.success?

ld_so_output
end

sig { returns(String) }
def self.sysconfdir
fallback_sysconfdir = "/etc"

match = brewed_ld_so_diagnostics.match(/path.sysconfdir="(.+)"/)
return fallback_sysconfdir unless match

match.captures.compact.first || fallback_sysconfdir
end

sig { returns(T::Array[String]) }
def self.system_dirs
dirs = []

brewed_ld_so_diagnostics.split("\n").each do |line|
match = line.match(/path.system_dirs\[0x.*\]="(.*)"/)
next unless match

dirs << match.captures.compact.first
end

dirs
end

sig { params(conf_path: T.any(Pathname, String)).returns(T::Array[String]) }
def self.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
47 changes: 47 additions & 0 deletions Library/Homebrew/test/os/linux/ld_spec.rb
@@ -0,0 +1,47 @@
# frozen_string_literal: true

require "os/linux/ld"
require "tmpdir"

RSpec.describe OS::Linux::Ld do
describe "::library_paths" do
ld_etc = Pathname("")
before do
ld_etc = Pathname(Dir.mktmpdir("homebrew-tests-ld-etc-", Dir.tmpdir))
FileUtils.mkdir [ld_etc/"subdir1", ld_etc/"subdir2"]
(ld_etc/"ld.so.conf").write <<~EOS
# This line is a comment

include #{ld_etc}/subdir1/*.conf # This is an end-of-line comment, should be ignored

# subdir2 is an empty directory
include #{ld_etc}/subdir2/*.conf

/a/b/c
/d/e/f # Indentation on this line should be ignored
/a/b/c # Duplicate entry should be ignored
EOS

(ld_etc/"subdir1/1-1.conf").write <<~EOS
/foo/bar
/baz/qux
EOS

(ld_etc/"subdir1/1-2.conf").write <<~EOS
/g/h/i
EOS

# Empty files (or files containing only whitespace) should be ignored
(ld_etc/"subdir1/1-3.conf").write "\n\t\n\t\n"
(ld_etc/"subdir1/1-4.conf").write ""
end

after do
FileUtils.rm_rf ld_etc
end

it "parses library paths successfully" do
expect(described_class.library_paths(ld_etc/"ld.so.conf")).to eq(%w[/foo/bar /baz/qux /g/h/i /a/b/c /d/e/f])
end
end
end
33 changes: 33 additions & 0 deletions Library/Homebrew/test/utils/path_spec.rb
@@ -0,0 +1,33 @@
# frozen_string_literal: true

require "utils/path"

RSpec.describe Utils::Path do
describe "::child_of?" do
it "recognizes a path as its own child" do
expect(described_class.child_of?("/foo/bar", "/foo/bar")).to be(true)
end

it "recognizes a path that is a child of the parent" do
expect(described_class.child_of?("/foo", "/foo/bar")).to be(true)
end

it "recognizes a path that is a grandchild of the parent" do
expect(described_class.child_of?("/foo", "/foo/bar/baz")).to be(true)
end

it "does not recognize a path that is not a child" do
expect(described_class.child_of?("/foo", "/bar/baz")).to be(false)
end

it "handles . and .. in paths correctly" do
expect(described_class.child_of?("/foo", "/foo/./bar")).to be(true)
expect(described_class.child_of?("/foo/bar", "/foo/../foo/bar/baz")).to be(true)
end

it "handles relative paths correctly" do
expect(described_class.child_of?("foo", "./bar/baz")).to be(false)
expect(described_class.child_of?("../foo", "./bar/baz/../../../foo/bar/baz")).to be(true)
end
end
end
14 changes: 14 additions & 0 deletions Library/Homebrew/utils/path.rb
@@ -0,0 +1,14 @@
# typed: strict
# frozen_string_literal: true

module Utils
module Path
sig { params(parent: T.any(Pathname, String), child: T.any(Pathname, String)).returns(T::Boolean) }
def self.child_of?(parent, child)
parent_pathname = Pathname(parent).expand_path
child_pathname = Pathname(child).expand_path
child_pathname.ascend { |p| return true if p == parent_pathname }
false
end
end
end