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:
parent
2992b7f519
commit
66737b5e82
@ -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) }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user