Mike McQuaid 23971854b0
Fix file descriptor leak in Linux LD library path parsing
The library_paths method was using readlines which could leave file
descriptors open due to Ruby's garbage collection behavior. When
processing many packages during 'brew upgrade' or 'brew linkage',
this caused "Too many open files" errors on Linux systems.

Changes:
- Replace readlines with explicit file.open block to ensure proper closure
- Add caching to avoid repeatedly reading /etc/ld.so.conf during a session
- Cache included files as well to optimize recursive include processing

Fixes: #19866, #20302, #19177, #20223

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-05 15:19:07 +01:00

88 lines
2.6 KiB
Ruby

# typed: strict
# frozen_string_literal: true
module OS
module Linux
# Helper functions for querying `ld` information.
module Ld
sig { returns(String) }
def self.brewed_ld_so_diagnostics
@brewed_ld_so_diagnostics ||= T.let({}, T.nilable(T::Hash[Pathname, String]))
brewed_ld_so = HOMEBREW_PREFIX/"lib/ld.so"
return "" unless brewed_ld_so.exist?
brewed_ld_so_target = brewed_ld_so.readlink
@brewed_ld_so_diagnostics[brewed_ld_so_target] ||= begin
ld_so_output = Utils.popen_read(brewed_ld_so, "--list-diagnostics")
ld_so_output if $CHILD_STATUS.success?
end
@brewed_ld_so_diagnostics[brewed_ld_so_target].to_s
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)
return [] unless conf_file.exist?
@library_paths_cache ||= T.let({}, T.nilable(T::Hash[String, T::Array[String]]))
cache_key = conf_file.to_s
if (cached_library_path_contents = @library_paths_cache[cache_key])
return cached_library_path_contents
end
paths = Set.new
directory = conf_file.realpath.dirname
conf_file.open("r") do |file|
file.each_line 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
end
@library_paths_cache[cache_key] = paths.to_a
end
end
end
end