From 4b0e950736b7394f9b80c3780c4881d6dfa9fd2f Mon Sep 17 00:00:00 2001 From: Caleb Xu Date: Mon, 25 Mar 2024 10:55:40 -0400 Subject: [PATCH 1/3] utils/path: add child_of? method --- Library/Homebrew/test/utils/path_spec.rb | 33 ++++++++++++++++++++++++ Library/Homebrew/utils/path.rb | 14 ++++++++++ 2 files changed, 47 insertions(+) create mode 100644 Library/Homebrew/test/utils/path_spec.rb create mode 100644 Library/Homebrew/utils/path.rb diff --git a/Library/Homebrew/test/utils/path_spec.rb b/Library/Homebrew/test/utils/path_spec.rb new file mode 100644 index 0000000000000..ba7f27436cd1f --- /dev/null +++ b/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 diff --git a/Library/Homebrew/utils/path.rb b/Library/Homebrew/utils/path.rb new file mode 100644 index 0000000000000..b2f69504f6fd2 --- /dev/null +++ b/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 From c6b98d0b8cc5b4af4f935291294598c69737d85c Mon Sep 17 00:00:00 2001 From: Caleb Xu Date: Mon, 25 Mar 2024 10:56:21 -0400 Subject: [PATCH 2/3] os/linux/ld: add functions for querying dynamic linker --- Library/Homebrew/os/linux/ld.rb | 74 +++++++++++++++++++++++ Library/Homebrew/test/os/linux/ld_spec.rb | 47 ++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 Library/Homebrew/os/linux/ld.rb create mode 100644 Library/Homebrew/test/os/linux/ld_spec.rb diff --git a/Library/Homebrew/os/linux/ld.rb b/Library/Homebrew/os/linux/ld.rb new file mode 100644 index 0000000000000..1ff2bd05f83e7 --- /dev/null +++ b/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 diff --git a/Library/Homebrew/test/os/linux/ld_spec.rb b/Library/Homebrew/test/os/linux/ld_spec.rb new file mode 100644 index 0000000000000..b738bbe2537e6 --- /dev/null +++ b/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 From a4e0ccc1f6b387dacbd78add2744ad8aa7d55829 Mon Sep 17 00:00:00 2001 From: Caleb Xu Date: Mon, 25 Mar 2024 10:56:40 -0400 Subject: [PATCH 3/3] os/linux/elf: avoid using ldd for listing dynamic dependencies --- Library/Homebrew/os/linux/elf.rb | 62 +++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/Library/Homebrew/os/linux/elf.rb b/Library/Homebrew/os/linux/elf.rb index efd855db952ca..49d0b80c63d53 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,52 @@ def needed_libraries_using_patchelf_rb(path) 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 } + 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