From 9495ff08d41bbfea9bbd9e804881732291ab32c8 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Wed, 3 Jun 2026 18:51:37 -0700 Subject: [PATCH] [syscall] E: Run dlopen ctors/dtors and DT_RUNPATH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three closely related capabilities to the user-space dynamic loader that close long-standing System V ABI gaps and unblock shared libraries with C/C++ global constructors: 1. `.init_array` invocation. Each `DynamicLibrary` now records the loaded address and length of its `.init_array` section. After `resolve_all_symbols` completes the topological relocation pass, `dlopen` drops the registry lock and invokes every function pointer in dependency order (leaves first), exactly as required for shared-object constructor execution. The registry lock is dropped first so a constructor may legally call `dlsym` without deadlocking. Sentinel values 0 and -1 are skipped to match the glibc loader behaviour. 2. `.fini_array` invocation. `dlclose` invokes destructors in reverse order on each library that is about to be unmapped, before the underlying memory segments are dropped. The BFS-from-root traversal already in place naturally yields the "dependents fini before dependencies" order required by the ABI. 3. `DT_RUNPATH` consultation. `DynamicLibrary::open` parses the library's `DT_RUNPATH` entries (already split out by goblin), `:`-splits each one, and stores the resulting directory list. `resolve_library_path` accepts an optional runpath slice and probes those directories ahead of the default `lib/` search path. A bare-bones `$ORIGIN` substitution (-> `.`) is included so toolchain-default runpaths do not silently miss. `DT_RPATH` is intentionally not consulted: it is deprecated by the System V gABI and modern toolchains emit `DT_RUNPATH` instead. The changes are purely additive — existing dlopen behaviour for libraries without `.init_array`, `.fini_array`, or `DT_RUNPATH` is unchanged. The accompanying posix-tests suite `dlfcn-init-runpath-c` (separate PR) exercises all three features end-to-end on a standalone Nanvix VM. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/libs/syscall/src/dlfcn/syscall/dlclose.rs | 32 ++++ src/libs/syscall/src/dlfcn/syscall/dlopen.rs | 120 ++++++++---- src/libs/syscall/src/dlfcn/syscall/dynlib.rs | 171 ++++++++++++++++++ src/libs/syscall/src/dlfcn/syscall/mod.rs | 120 ++++++++++-- 4 files changed, 387 insertions(+), 56 deletions(-) diff --git a/src/libs/syscall/src/dlfcn/syscall/dlclose.rs b/src/libs/syscall/src/dlfcn/syscall/dlclose.rs index 94b6308a9a..864efe6b2c 100644 --- a/src/libs/syscall/src/dlfcn/syscall/dlclose.rs +++ b/src/libs/syscall/src/dlfcn/syscall/dlclose.rs @@ -12,6 +12,7 @@ use crate::dlfcn::syscall::{ }; use ::alloc::{ collections::btree_map::BTreeMap, + string::String, sync::Arc, vec::Vec, }; @@ -70,6 +71,25 @@ pub fn dlclose(handle: &DlHandle) -> Result<(), Error> { // Collect all dependencies of the dynamic library file being closed. let mut dep_dlfiles: Vec>> = Vec::new(); if let Some((_, dep_dlfile)) = dlfile.pop() { + // Snapshot the `.fini_array` descriptor and name under a short + // per-library lock, then drop the lock and invoke destructors. + // Holding the per-library lock during destructor execution + // would deadlock any destructor that calls + // `dlsym(self_handle, ...)`. (The outer registry lock is still + // held, so destructors that call `dlopen` / `dlclose` / cross- + // library `dlsym` are not yet supported; see the loader docs.) + let (descriptor, name): (Option<(usize, usize)>, String) = { + let lib = dep_dlfile.lock(); + (lib.fini_array_descriptor(), String::from(lib.name())) + }; + // SAFETY: `descriptor` was produced under the per-library lock + // for a library still alive via `dep_dlfile`; the per-library + // mutex is released before invocation so destructors that call + // `dlsym(self_handle, ...)` do not deadlock. + unsafe { + DynamicLibrary::invoke_fini_array(descriptor, &name); + } + let mut dep_dlfile = dep_dlfile.lock(); dep_dlfile .take_dependencies() @@ -105,6 +125,18 @@ pub fn dlclose(handle: &DlHandle) -> Result<(), Error> { // Collect all dependencies of the dynamic library file. if let Some((_, dep_dlfile)) = dep_dlfile.pop() { + // Run `.fini_array` destructors before this dependency is + // dropped. See the comment above on lock handling. + let (descriptor, name): (Option<(usize, usize)>, String) = { + let lib = dep_dlfile.lock(); + (lib.fini_array_descriptor(), String::from(lib.name())) + }; + // SAFETY: see the comment on the first `invoke_fini_array` + // call above. + unsafe { + DynamicLibrary::invoke_fini_array(descriptor, &name); + } + let mut dep_dlfile = dep_dlfile.lock(); dep_dlfile .take_dependencies() diff --git a/src/libs/syscall/src/dlfcn/syscall/dlopen.rs b/src/libs/syscall/src/dlfcn/syscall/dlopen.rs index 6f27902bcc..0281624206 100644 --- a/src/libs/syscall/src/dlfcn/syscall/dlopen.rs +++ b/src/libs/syscall/src/dlfcn/syscall/dlopen.rs @@ -46,7 +46,10 @@ pub fn dlopen(filename: &str, global: bool) -> Result { // (e.g., "lib/libfoo.so") using the configured search directories. // Paths with leading "/" are normalized to strip it, ensuring // "/lib/libc.so" and "lib/libc.so" are treated as the same library. - let resolved: String = super::resolve_library_path(filename); + // The top-level dlopen call has no parent runpaths to honour, so pass + // `None`; bare-name probes against `DT_RUNPATH` only happen for + // transitive `DT_NEEDED` dependencies of an already-loaded library. + let resolved: String = super::resolve_library_path(filename, None); let filename: &str = resolved.as_str(); let mut registry: MutexGuard<'_, BTreeMap>>> = @@ -80,37 +83,68 @@ pub fn dlopen(filename: &str, global: bool) -> Result { // Load dependencies and resolve symbols. If either step fails, remove all // entries that were added during this call (the library itself and any // transitive dependencies) so subsequent dlopen calls start fresh. - match load_all_dependencies(&mut registry, new_dlfile) - .and_then(|_| resolve_all_symbols(&mut registry, &handles_before)) - { - Ok(()) => { - // If RTLD_GLOBAL was requested, publish the library's exported - // symbols into the global symbol table so subsequently loaded - // libraries can resolve them. - if global { - if let Some(dlfile) = registry.get(&handle) { - super::register_library_in_global_scope(dlfile); + let init_order: Vec>> = + match load_all_dependencies(&mut registry, new_dlfile) + .and_then(|_| resolve_all_symbols(&mut registry, &handles_before)) + { + Ok(order) => { + // If RTLD_GLOBAL was requested, publish the library's exported + // symbols into the global symbol table so subsequently loaded + // libraries can resolve them. + if global { + if let Some(dlfile) = registry.get(&handle) { + super::register_library_in_global_scope(dlfile); + } } - } - Ok(handle) - }, - Err(e) => { - let new_handles: Vec = registry - .keys() - .filter(|h| !handles_before.contains(h)) - .copied() - .collect(); - ::syslog::warn!( - "dlopen(): rolling back {} entries after failure (error={:?})", - new_handles.len(), - e - ); - for h in new_handles { - registry.remove(&h); - } - Err(e) - }, + order + }, + Err(e) => { + let new_handles: Vec = registry + .keys() + .filter(|h| !handles_before.contains(h)) + .copied() + .collect(); + ::syslog::warn!( + "dlopen(): rolling back {} entries after failure (error={:?})", + new_handles.len(), + e + ); + for h in new_handles { + registry.remove(&h); + } + return Err(e); + }, + }; + + // Drop the registry lock before invoking `.init_array` constructors so a + // constructor may legally call `dlsym` (and, in a future relaxation, + // `dlopen`) without deadlocking on `DYNAMIC_LIBRARY_REGISTRY`. + drop(registry); + + // Invoke `.init_array` constructors in dependency order (leaves first). + // The Arc list was built while the registry was locked, so each entry is + // guaranteed to still point to a loaded library. + // + // Snapshot the constructor descriptor and library name under a short + // per-library lock, then drop the lock before invoking constructors. + // Holding the per-library lock during constructor execution would + // deadlock any constructor that calls `dlsym(self_handle, ...)`, because + // `dlsym` re-locks the same library to look up symbols. + for dlfile in init_order.iter() { + let (descriptor, name): (Option<(usize, usize)>, String) = { + let lib: MutexGuard<'_, DynamicLibrary> = dlfile.lock(); + (lib.init_array_descriptor(), String::from(lib.name())) + }; + // SAFETY: `descriptor` was produced under the per-library lock for + // a library still held alive by `init_order`'s `Arc`; relocations + // have been applied by `resolve_all_symbols`; no dlfcn locks are + // held across this call. + unsafe { + DynamicLibrary::invoke_init_array(descriptor, &name); + } } + + Ok(handle) } /// Recursively loads all transitive dependencies of a newly opened library. @@ -129,6 +163,12 @@ fn load_all_dependencies( new_dlhandle: &DlHandle, new_dlfile: &mut MutexGuard<'_, DynamicLibrary>, ) -> Result<(), Error> { + // Snapshot the loader's `DT_RUNPATH` entries so they are visible to + // `resolve_library_path` while probing every dependency below. The + // `DynamicLibrary` mutex is borrowed throughout, so this avoids + // re-borrowing it per call. + let runpaths: Vec = new_dlfile.runpaths().to_vec(); + // Collect the name of all dependencies. let mut dependencies: Vec = new_dlfile .dependencies() @@ -140,7 +180,7 @@ fn load_all_dependencies( dependencies.retain(|dependency| { // Resolve bare name so we can match against loaded libraries // that were opened with a full path. - let resolved_dep: String = super::resolve_library_path(dependency); + let resolved_dep: String = super::resolve_library_path(dependency, Some(&runpaths)); for (dlhandle, dlfile) in dlfiles.iter() { // Check if need to skip the dynamic library itself. if dlhandle == new_dlhandle { @@ -176,7 +216,7 @@ fn load_all_dependencies( // Load remaining dependencies. while let Some(dependency) = dependencies.pop() { // Resolve bare library names to full paths using search directories. - let resolved_dep: String = super::resolve_library_path(&dependency); + let resolved_dep: String = super::resolve_library_path(&dependency, Some(&runpaths)); // Open and pre-load the dynamic library file. let dep_dlfile: DynamicLibrary = DynamicLibrary::open(&resolved_dep)?; @@ -205,15 +245,17 @@ fn load_all_dependencies( Ok(()) } -/// Resolves all relocations for libraries added during this `dlopen` call. +/// Resolves all relocations for libraries added during this `dlopen` call and +/// returns them in dependency order (leaves first, root last) so the caller +/// can invoke `.init_array` constructors in the correct sequence. /// -/// Libraries are resolved in dependency order (leaves first, root last). -/// This ensures that when a library's relocations reference symbols from -/// its dependencies, those dependencies are already fully resolved. +/// Libraries are resolved in dependency order. This ensures that when a +/// library's relocations reference symbols from its dependencies, those +/// dependencies are already fully resolved. fn resolve_all_symbols( dlfiles: &mut MutexGuard<'_, BTreeMap>>>, handles_before: &BTreeSet, -) -> Result<(), Error> { +) -> Result>>, Error> { ::syslog::trace!("resolve_all_symbols()"); // Collect all newly added libraries. @@ -291,11 +333,13 @@ fn resolve_all_symbols( } // Resolve in dependency order (leaves first). + let mut init_order: Vec>> = Vec::with_capacity(ordered.len()); for handle in ordered { if let Some(dlfile) = dlfiles.get(&handle) { dlfile.lock().resolve_all()?; + init_order.push(dlfile.clone()); } } - Ok(()) + Ok(init_order) } diff --git a/src/libs/syscall/src/dlfcn/syscall/dynlib.rs b/src/libs/syscall/src/dlfcn/syscall/dynlib.rs index b07c89eb0c..1289d2401e 100644 --- a/src/libs/syscall/src/dlfcn/syscall/dynlib.rs +++ b/src/libs/syscall/src/dlfcn/syscall/dynlib.rs @@ -123,6 +123,12 @@ pub struct DynamicLibrary { dynplt: Option, /// Relocation table for global variables. dynrel: Option, + /// Absolute address of the `.init_array` section and the number of entries. + init_array: Option<(usize, usize)>, + /// Absolute address of the `.fini_array` section and the number of entries. + fini_array: Option<(usize, usize)>, + /// `DT_RUNPATH` directories of this library, already split on `:`. + runpaths: Vec, } impl DynamicLibrary { @@ -290,6 +296,23 @@ impl DynamicLibrary { Self::get_dynplt(§ion_headers, load_address); let dynrel: Option = Self::get_dynrel(§ion_headers, load_address); + let init_array: Option<(usize, usize)> = + Self::get_init_array(§ion_headers, load_address); + let fini_array: Option<(usize, usize)> = + Self::get_fini_array(§ion_headers, load_address); + + // Collect `DT_RUNPATH` entries (goblin exposes them already + // resolved against `.dynstr`). Each entry may be a colon- + // separated list of directories; split here so the search + // path probe can iterate them directly. + let mut runpaths: Vec = Vec::new(); + for raw in elf.runpaths.iter() { + for component in raw.split(':') { + if !component.is_empty() { + runpaths.push(component.to_string()); + } + } + } Ok(DynamicLibrary { filename, @@ -301,6 +324,9 @@ impl DynamicLibrary { dynstr, dynplt, dynrel, + init_array, + fini_array, + runpaths, }) }, Err(error) => { @@ -444,6 +470,44 @@ impl DynamicLibrary { } } + /// Looks up a function-pointer array section (`.init_array` / `.fini_array`) + /// by name, returning the absolute address of the first entry and the + /// number of `usize`-sized entries it contains. + /// + /// Returns `None` when the section is missing or empty. Entries are + /// always 4 bytes on i386; if `sh_size` is not a multiple of `usize` + /// the trailing bytes are silently truncated (a well-formed ELF should + /// never trigger this). + fn get_function_pointer_array( + section_headers: &BTreeMap, + load_address: VirtualAddress, + section_name: &str, + ) -> Option<(usize, usize)> { + let header: &SectionHeader = section_headers.get(section_name)?; + let count: usize = (header.sh_size as usize) / mem::size_of::(); + if count == 0 { + return None; + } + let base: usize = load_address.into_raw_value() + header.sh_addr as usize; + Some((base, count)) + } + + /// Gets the `.init_array` section descriptor, if present. + fn get_init_array( + section_headers: &BTreeMap, + load_address: VirtualAddress, + ) -> Option<(usize, usize)> { + Self::get_function_pointer_array(section_headers, load_address, ".init_array") + } + + /// Gets the `.fini_array` section descriptor, if present. + fn get_fini_array( + section_headers: &BTreeMap, + load_address: VirtualAddress, + ) -> Option<(usize, usize)> { + Self::get_function_pointer_array(section_headers, load_address, ".fini_array") + } + /// Finds a symbol in the dynamic library. fn find(&self, symbol_name: &str) -> Option<&Symbol> { ::syslog::trace!("find(): symbol={} in dlname={:?}", symbol_name, self.filename); @@ -937,6 +1001,113 @@ impl DynamicLibrary { } dependencies } + + /// Returns the `DT_RUNPATH` directories of the library, split on `:`. + pub fn runpaths(&self) -> &[String] { + &self.runpaths + } + + /// Returns the loaded `.init_array` descriptor as `(base_address, + /// entry_count)`, or `None` if the library has no constructors. + /// + /// Callers should snapshot this under the library's mutex, drop the + /// mutex, and then invoke the entries via [`invoke_init_array`]. The + /// descriptor remains valid for as long as the owning + /// `Arc>` is alive. + pub fn init_array_descriptor(&self) -> Option<(usize, usize)> { + self.init_array + } + + /// Returns the loaded `.fini_array` descriptor as `(base_address, + /// entry_count)`, or `None` if the library has no destructors. + /// + /// See [`init_array_descriptor`](Self::init_array_descriptor) for the + /// expected lock-handling pattern. + pub fn fini_array_descriptor(&self) -> Option<(usize, usize)> { + self.fini_array + } + + /// Invokes every function pointer in the supplied `.init_array` + /// descriptor in order, as required by the System V ABI for shared- + /// object constructor execution. + /// + /// `name` is purely for diagnostic logging. + /// + /// # Locking + /// + /// This function must be called with **no** dlfcn locks held — neither + /// `DYNAMIC_LIBRARY_REGISTRY` nor the per-library mutex. Constructors + /// may legally call `dlsym` (and, in a future relaxation, `dlopen`), + /// both of which would otherwise re-enter the same locks and deadlock. + /// + /// # Safety + /// + /// The caller must guarantee that `descriptor` was produced by + /// [`init_array_descriptor`](Self::init_array_descriptor) from a + /// `DynamicLibrary` whose memory segments are still mapped, that + /// `resolve_all` has already applied any `R_386_RELATIVE` patches to + /// the section, and that the holding `Arc` lives until this call + /// returns. Entry values equal to `0` or `usize::MAX` are treated as + /// sentinels and skipped, matching the glibc loader behaviour. + pub unsafe fn invoke_init_array(descriptor: Option<(usize, usize)>, name: &str) { + let (base, count): (usize, usize) = match descriptor { + Some(range) => range, + None => return, + }; + ::syslog::debug!("invoke_init_array(): library={:?} entries={}", name, count); + for index in 0..count { + // SAFETY: `base` points to the loaded `.init_array` section of + // the originating library and `count` is the number of + // `usize`-sized entries it contains. + let entry_ptr: *const usize = (base + index * mem::size_of::()) as *const usize; + let entry: usize = unsafe { entry_ptr.read_unaligned() }; + if entry == 0 || entry == usize::MAX { + continue; + } + // SAFETY: the .init_array entry is a function pointer with C + // calling convention and no arguments per the System V ABI. + let func: extern "C" fn() = unsafe { mem::transmute::(entry) }; + func(); + } + } + + /// Invokes every function pointer in the supplied `.fini_array` + /// descriptor in reverse order, as required by the System V ABI for + /// shared-object destructor execution. Must be called before the + /// library's memory segments are unmapped. + /// + /// `name` is purely for diagnostic logging. + /// + /// # Locking + /// + /// The per-library mutex of the originating library must **not** be + /// held while invoking destructors, so a destructor calling + /// `dlsym(self, ...)` does not deadlock. The current `dlclose` + /// implementation still holds `DYNAMIC_LIBRARY_REGISTRY`, so + /// destructors that call `dlopen` / `dlclose` (or `dlsym` on a + /// different library) are not yet supported; see the loader docs for + /// the full reentrancy contract. + /// + /// # Safety + /// + /// See [`invoke_init_array`](Self::invoke_init_array). + pub unsafe fn invoke_fini_array(descriptor: Option<(usize, usize)>, name: &str) { + let (base, count): (usize, usize) = match descriptor { + Some(range) => range, + None => return, + }; + ::syslog::debug!("invoke_fini_array(): library={:?} entries={}", name, count); + for index in (0..count).rev() { + // SAFETY: see `invoke_init_array`. + let entry_ptr: *const usize = (base + index * mem::size_of::()) as *const usize; + let entry: usize = unsafe { entry_ptr.read_unaligned() }; + if entry == 0 || entry == usize::MAX { + continue; + } + let func: extern "C" fn() = unsafe { mem::transmute::(entry) }; + func(); + } + } } impl fmt::Debug for DynamicLibrary { diff --git a/src/libs/syscall/src/dlfcn/syscall/mod.rs b/src/libs/syscall/src/dlfcn/syscall/mod.rs index 65f362aa5e..44b50ad25a 100644 --- a/src/libs/syscall/src/dlfcn/syscall/mod.rs +++ b/src/libs/syscall/src/dlfcn/syscall/mod.rs @@ -90,9 +90,18 @@ static DLINIT_ONCE: Once = Once::new(); /// and relative paths bypass `LD_LIBRARY_PATH`. /// /// If `filename` is a bare name (no `/`), the function tries each directory in -/// [`LIBRARY_SEARCH_PATHS`] in order, returning the first path for which the -/// file exists. If no match is found the bare name is returned so that the -/// subsequent `open_regular_file` call produces the appropriate error. +/// the following order, returning the first path for which the file exists: +/// +/// 1. The directories listed in `runpaths` (caller-supplied, typically the +/// `DT_RUNPATH` entries of the library whose dependency is being resolved). +/// `$ORIGIN` inside an entry is substituted with `"."` (the current +/// working directory). This is a temporary approximation; the System V +/// `ld.so` convention is to substitute the directory of the loading +/// library, which Nanvix does not yet track per-library. +/// 2. The directories in [`LIBRARY_SEARCH_PATHS`] (currently just `"lib/"`). +/// +/// If no match is found the bare name is returned so that the subsequent +/// `open_regular_file` call produces the appropriate error. /// /// All paths are normalized to a consistent form without leading `/` or `./`, /// matching the convention used throughout the Nanvix dlfcn layer and test code @@ -106,7 +115,11 @@ static DLINIT_ONCE: Once = Once::new(); /// candidate path. The matched file is re-opened by `DynamicLibrary::open()`. /// This double-open is accepted for simplicity; a stat-based probe would /// avoid it but is not currently available in the Nanvix VFS API. -pub(super) fn resolve_library_path(filename: &str) -> String { +/// +/// `DT_RPATH` (the predecessor to `DT_RUNPATH`) is intentionally not consulted: +/// it is deprecated by the System V gABI and modern toolchains emit +/// `DT_RUNPATH` instead. +pub(super) fn resolve_library_path(filename: &str, runpaths: Option<&[String]>) -> String { // If the original filename contains a path separator, the caller provided // an explicit path (absolute or relative with directory). Normalize it // but do NOT search configured directories — matching Linux behavior @@ -124,24 +137,40 @@ pub(super) fn resolve_library_path(filename: &str) -> String { return String::from(normalized); } - // Bare library name (no path separator) — search configured directories. + // Bare library name (no path separator) — search runpaths first, then + // the configured default directories. + if let Some(runpaths) = runpaths { + for dir in runpaths.iter() { + let dir: String = substitute_origin(dir); + let candidate: String = join_dir(&dir, filename); + if probe_exists(&candidate) { + // Apply the same canonicalization as the explicit-path + // branch above so callers that compare against already- + // loaded library names (which are stored canonically) + // don't see duplicates like `"./libfoo.so"` vs + // `"libfoo.so"`. Strip a single leading "./" and any + // leading "/" so DT_RUNPATH entries like `"."`, `"./"`, + // or absolute paths still yield a canonical result. + let canonical: String = canonicalize(&candidate); + ::syslog::debug!( + "resolve_library_path(): resolved '{}' via DT_RUNPATH -> '{}'", + filename, + canonical + ); + return canonical; + } + } + } + // Clone the path list under the lock, then release it before probing // the filesystem (which involves I/O and should not hold a spinlock). let search_paths: Vec = LIBRARY_SEARCH_PATHS.lock().clone(); for dir in search_paths.iter() { - let candidate: String = alloc::format!("{}{}", dir, filename); - // Probe whether the file exists by attempting to open it. - if let Ok(_fd) = crate::safe::FileSystem::open_regular_file( - // FileSystemPath::new can fail for invalid names; skip on error. - &match crate::safe::FileSystemPath::new(&candidate) { - Ok(p) => p, - Err(_) => continue, - }, - &crate::safe::RegularFileOpenFlags::read_only(), - None, - ) { - ::syslog::debug!("resolve_library_path(): resolved '{}' -> '{}'", filename, candidate); - return candidate; + let candidate: String = join_dir(dir, filename); + if probe_exists(&candidate) { + let canonical: String = canonicalize(&candidate); + ::syslog::debug!("resolve_library_path(): resolved '{}' -> '{}'", filename, canonical); + return canonical; } } @@ -150,6 +179,61 @@ pub(super) fn resolve_library_path(filename: &str) -> String { String::from(filename) } +/// Strips a single leading `./` and any leading `/` from `path`, matching the +/// canonical form used throughout the dlfcn layer (e.g. `"lib/libc.so"`). +/// Returns the input unchanged if normalization would yield an empty string. +fn canonicalize(path: &str) -> String { + let stripped: &str = path + .strip_prefix("./") + .unwrap_or(path) + .trim_start_matches('/'); + if stripped.is_empty() { + String::from(path) + } else { + String::from(stripped) + } +} + +/// Joins a directory and filename without introducing duplicate separators. +fn join_dir(dir: &str, filename: &str) -> String { + if dir.is_empty() { + return String::from(filename); + } + if dir.ends_with('/') { + alloc::format!("{}{}", dir, filename) + } else { + alloc::format!("{}/{}", dir, filename) + } +} + +/// Substitutes `$ORIGIN` in a runpath entry. Nanvix has no per-process loader +/// directory concept yet, so `$ORIGIN` is currently expanded to `"."` (the +/// current working directory). This is a temporary approximation: the System V +/// `ld.so` convention is to expand `$ORIGIN` to the directory containing the +/// loading library, which Nanvix does not yet track per-library. Keeping a +/// well-formed substitution (rather than the empty string) avoids producing +/// invalid paths like `"//lib"` from entries such as `"$ORIGIN/lib"`. +fn substitute_origin(entry: &str) -> String { + if !entry.contains("$ORIGIN") { + return String::from(entry); + } + entry.replace("$ORIGIN", ".") +} + +/// Returns `true` if a regular file exists at `candidate`. +fn probe_exists(candidate: &str) -> bool { + let path: crate::safe::FileSystemPath = match crate::safe::FileSystemPath::new(candidate) { + Ok(p) => p, + Err(_) => return false, + }; + crate::safe::FileSystem::open_regular_file( + &path, + &crate::safe::RegularFileOpenFlags::read_only(), + None, + ) + .is_ok() +} + /// Populates `GLOBAL_SYMBOL_TABLE` from the executable's `.dynsym`/`.dynstr` /// sections. The linker script emits `__dynsym_start/__dynsym_end` and /// `__dynstr_start/__dynstr_end` boundary symbols around these sections when