os/linux/elf: fix file descriptor leak

On Linux, we occasionally see `EMFILE` ("too many open files") errors
especially when installing a large formula like `llvm`. Currently, this
can be reliably reproduced in a Homebrew/brew GitHub codespace (where
`ulimit -n` seems to be 1024 by default) with `brew install geeqie`,
with the following error message:

    Error: Too many open files @ rb_sysopen - /home/linuxbrew/.linuxbrew/Cellar/llvm/20.1.8/bin/tblgen-lsp-server

The reason is that each instance of `PatchELF::Patcher` keeps the ELF
file open. We prepend the `ELFShim` module to the `Pathname` class and
cache the patcher as an instance variable, which means that the ELF file
remains open so long as the `Pathname` instance is still alive even if
we don't need to access the ELF metadata anymore. When performing
certain checks (e.g., linkage), we also store these `Pathname`
instances, so the number of open file descriptors simply keeps
increasing.

We can fix that by not caching the patcher and only use it when
necessary. We create a patcher instance whenever we need to read or
write ELF metadata, and reading of metadata is consolidated into the
existing `ELFShim::Metadata` class so that we don't repeatedly create
patcher instances.

A fix for a file descriptor leak issue in patchelf.rb has been submitted
at https://github.com/david942j/patchelf.rb/pull/48. Together with that,
this fixes #19177, #19866, #20223, #20302.
This commit is contained in:
Ruoyu Zhong 2025-08-11 18:01:32 +08:00
parent 2992b7f519
commit 66737b5e82
No known key found for this signature in database

View File

@ -52,7 +52,6 @@ module ELFShim
@interpreter = T.let(nil, T.nilable(String)) @interpreter = T.let(nil, T.nilable(String))
@dynamic_elf = T.let(nil, T.nilable(T::Boolean)) @dynamic_elf = T.let(nil, T.nilable(T::Boolean))
@metadata = T.let(nil, T.nilable(Metadata)) @metadata = T.let(nil, T.nilable(Metadata))
@patchelf_patcher = nil
super super
end end
@ -123,7 +122,7 @@ module ELFShim
# "/lib:/usr/lib:/usr/local/lib" # "/lib:/usr/lib:/usr/local/lib"
sig { returns(T.nilable(String)) } sig { returns(T.nilable(String)) }
def rpath def rpath
@rpath ||= rpath_using_patchelf_rb metadata.rpath
end end
# An array of runtime search path entries, such as: # An array of runtime search path entries, such as:
@ -134,7 +133,7 @@ module ELFShim
sig { returns(T.nilable(String)) } sig { returns(T.nilable(String)) }
def interpreter def interpreter
@interpreter ||= patchelf_patcher.interpreter metadata.interpreter
end end
def patch!(interpreter: nil, rpath: nil) def patch!(interpreter: nil, rpath: nil)
@ -149,11 +148,12 @@ module ELFShim
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def dynamic_elf? def dynamic_elf?
@dynamic_elf ||= elf_parser.segment_by_type(:DYNAMIC).present? metadata.dynamic_elf?
end end
sig { returns(T::Array[String]) }
def section_names def section_names
@section_names ||= elf_parser.sections.map(&:name).compact_blank metadata.section_names
end end
# Helper class for reading metadata from an ELF file. # Helper class for reading metadata from an ELF file.
@ -164,35 +164,53 @@ module ELFShim
sig { returns(T.nilable(String)) } sig { returns(T.nilable(String)) }
attr_reader :dylib_id attr_reader :dylib_id
sig { returns(T::Boolean) }
def dynamic_elf?
@dynamic_elf
end
sig { returns(T.nilable(String)) }
attr_reader :interpreter
sig { returns(T.nilable(String)) }
attr_reader :rpath
sig { returns(T::Array[String]) } sig { returns(T::Array[String]) }
attr_reader :dylibs attr_reader :section_names
sig { params(path: ELFShim).void } sig { params(path: ELFShim).void }
def initialize(path) def initialize(path)
@path = T.let(path, ELFShim) require "patchelf"
@dylibs = T.let([], T::Array[String]) patcher = path.patchelf_patcher
@dylib_id = T.let(nil, T.nilable(String))
@dylib_id, needed = needed_libraries path
@dylibs = needed.map { |lib| find_full_lib_path(lib).to_s } if needed.present?
@metadata = T.let(nil, T.nilable(T::Hash[String, T.untyped])) @path = T.let(path, ELFShim)
@dylibs = T.let(nil, T.nilable(T::Array[String]))
@dylib_id = T.let(nil, T.nilable(String))
dynamic_segment = patcher.elf.segment_by_type(:dynamic)
@dynamic_elf = dynamic_segment.present?
@dylib_id, @needed = if @dynamic_elf
[patcher.soname, patcher.needed]
else
[nil, []]
end
@interpreter = patcher.interpreter
@rpath = patcher.rpath || patcher.runpath
@section_names = patcher.elf.sections.map(&:name).compact_blank
@dt_flags_1 = dynamic_segment&.tag_by_type(:flags_1)&.value
end
sig { returns(T::Array[String]) }
def dylibs
@dylibs ||= @needed.map { |lib| find_full_lib_path(lib).to_s }
end end
private private
def needed_libraries(path)
return [nil, []] unless path.dynamic_elf?
needed_libraries_using_patchelf_rb path
end
def needed_libraries_using_patchelf_rb(path)
patcher = path.patchelf_patcher
[patcher.soname, patcher.needed]
end
def find_full_lib_path(basename) def find_full_lib_path(basename)
local_paths = (path.patchelf_patcher.runpath || path.patchelf_patcher.rpath)&.split(":") local_paths = rpath&.split(":")
# Search for dependencies in the runpath/rpath first # Search for dependencies in the runpath/rpath first
local_paths&.each do |local_path| local_paths&.each do |local_path|
@ -202,11 +220,10 @@ module ELFShim
end end
# Check if DF_1_NODEFLIB is set # Check if DF_1_NODEFLIB is set
dt_flags_1 = path.elf_parser.segment_by_type(:dynamic)&.tag_by_type(:flags_1) nodeflib_flag = if @dt_flags_1.nil?
nodeflib_flag = if dt_flags_1.nil?
false false
else else
dt_flags_1.value & ELFTools::Constants::DF::DF_1_NODEFLIB != 0 @dt_flags_1 & ELFTools::Constants::DF::DF_1_NODEFLIB != 0
end end
linker_library_paths = OS::Linux::Ld.library_paths linker_library_paths = OS::Linux::Ld.library_paths
@ -247,13 +264,12 @@ module ELFShim
patcher.save(patchelf_compatible: true) patcher.save(patchelf_compatible: true)
end end
def rpath_using_patchelf_rb # Don't cache the patcher; it keeps the ELF file open so long as it is alive.
patchelf_patcher.runpath || patchelf_patcher.rpath # Instead, for read-only access to the ELF file's metadata, fetch it and cache
end # it with {Metadata}.
def patchelf_patcher def patchelf_patcher
require "patchelf" require "patchelf"
@patchelf_patcher ||= ::PatchELF::Patcher.new to_s, on_error: :silent ::PatchELF::Patcher.new to_s, on_error: :silent
end end
sig { returns(Metadata) } sig { returns(Metadata) }