From 0d0d6fa346b614db949ba026e865fa92077dae4f Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Sat, 18 Apr 2026 22:31:04 -0700 Subject: [PATCH] fix(dlfcn): add library search path for DT_NEEDED resolution (#2076, #2122) Add resolve_library_path() that searches configured directories (default: lib/) for bare library names from DT_NEEDED entries. Paths are normalized by stripping leading / to ensure /lib/libc.so and lib/libc.so are treated as the same library. Changes: - Add LIBRARY_SEARCH_PATHS static with default ["lib/"]. - Add resolve_library_path() with leading-/ stripping and file probing. - dlopen() calls resolve_library_path() at entry. - load_all_dependencies() resolves bare dep names and matches against both bare and resolved names. Closes #2076 Closes #2122 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/libs/syscall/src/dlfcn/syscall/dlopen.rs | 22 +++++- src/libs/syscall/src/dlfcn/syscall/mod.rs | 77 ++++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/src/libs/syscall/src/dlfcn/syscall/dlopen.rs b/src/libs/syscall/src/dlfcn/syscall/dlopen.rs index c6302d6d98..65e86147f0 100644 --- a/src/libs/syscall/src/dlfcn/syscall/dlopen.rs +++ b/src/libs/syscall/src/dlfcn/syscall/dlopen.rs @@ -42,7 +42,12 @@ pub fn dlopen(filename: &str) -> Result { // calls are a no-op. super::dlinit(); - // TODO: Normalize filename. + // Resolve bare library names (e.g., "libfoo.so") to a canonical path + // (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); + let filename: &str = resolved.as_str(); let mut registry: MutexGuard<'_, BTreeMap>>> = DYNAMIC_LIBRARY_REGISTRY.lock(); @@ -118,13 +123,21 @@ fn load_all_dependencies( // Bind to already loaded dependencies and remove them from the list. 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); for (dlhandle, dlfile) in dlfiles.iter() { // Check if need to skip the dynamic library itself. if dlhandle == new_dlhandle { continue; } - if dlfile.lock().name() == dependency.as_str() { + let is_match: bool = { + let loaded_file: spin::MutexGuard<'_, DynamicLibrary> = dlfile.lock(); + let loaded_name: &str = loaded_file.name(); + loaded_name == dependency.as_str() || loaded_name == resolved_dep + }; + if is_match { ::syslog::debug!( "load_all_dependencies_recursive(): already loaded dependency '{}' \ (handle={:?})", @@ -147,8 +160,11 @@ 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); + // Open and pre-load the dynamic library file. - let dep_dlfile: DynamicLibrary = DynamicLibrary::open(&dependency)?; + let dep_dlfile: DynamicLibrary = DynamicLibrary::open(&resolved_dep)?; let handle: DlHandle = dep_dlfile.handle(); let dep_dlfile: Arc> = Arc::new(Mutex::new(dep_dlfile)); diff --git a/src/libs/syscall/src/dlfcn/syscall/mod.rs b/src/libs/syscall/src/dlfcn/syscall/mod.rs index acd345edd7..53789beacc 100644 --- a/src/libs/syscall/src/dlfcn/syscall/mod.rs +++ b/src/libs/syscall/src/dlfcn/syscall/mod.rs @@ -29,6 +29,7 @@ use ::alloc::{ collections::btree_map::BTreeMap, string::String, sync::Arc, + vec::Vec, }; use ::elf::{ StringTable, @@ -58,9 +59,85 @@ static DYNAMIC_LIBRARY_REGISTRY: Lazy>> = Lazy::new(|| Mutex::new(BTreeMap::new())); +/// Default directories searched when a `DT_NEEDED` entry contains a bare +/// library name (no path separator). Modelled after Linux's default search +/// paths (`/lib`, `/usr/lib`), but simplified to the single `lib/` directory +/// that Nanvix uses for shared libraries. +static LIBRARY_SEARCH_PATHS: Lazy>> = + Lazy::new(|| Mutex::new(alloc::vec!["lib/".into()])); + /// Ensures `dlinit` runs at most once. static DLINIT_ONCE: Once = Once::new(); +/// Resolves a library filename to a canonical path. +/// +/// If `filename` contains a path separator (`/`), it is treated as an explicit +/// path (absolute or relative with directory component). The path is normalized +/// by stripping leading `/` and `./` to produce a canonical form, but no +/// search-path probing is performed. This matches Linux behavior where absolute +/// 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. +/// +/// All paths are normalized to a consistent form without leading `/` or `./`, +/// matching the convention used throughout the Nanvix dlfcn layer and test code +/// (e.g., `"lib/libmul.so"`). The VFS accepts both relative and absolute paths +/// and normalizes internally, so stripping the leading `/` is safe and ensures +/// that `"/lib/libc.so"`, `"./lib/libc.so"`, `"lib/libc.so"`, and the bare +/// DT_NEEDED name `"libc.so"` all resolve to the same canonical path +/// `"lib/libc.so"`. +/// +/// NOTE: The probe opens and immediately closes a file descriptor per +/// 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 { + // 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 + // where absolute/relative paths bypass LD_LIBRARY_PATH. + if filename.contains('/') { + // Strip leading "./" and "/" for canonical form. + let normalized: &str = filename + .strip_prefix("./") + .unwrap_or(filename) + .trim_start_matches('/'); + // Guard against pathological input like "/" or "./" becoming empty. + if normalized.is_empty() { + return String::from(filename); + } + return String::from(normalized); + } + + // Bare library name (no path separator) — search configured directories. + // 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; + } + } + + // Fall back to the original name (will produce a clear error at open time). + ::syslog::debug!("resolve_library_path(): no match for '{}', using as-is", filename); + String::from(filename) +} + /// 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