Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions src/libs/syscall/src/dlfcn/syscall/dlopen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ pub fn dlopen(filename: &str) -> Result<DlHandle, Error> {
// 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<DlHandle, Arc<Mutex<DynamicLibrary>>>> =
DYNAMIC_LIBRARY_REGISTRY.lock();
Expand Down Expand Up @@ -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={:?})",
Expand All @@ -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<Mutex<DynamicLibrary>> = Arc::new(Mutex::new(dep_dlfile));

Expand Down
77 changes: 77 additions & 0 deletions src/libs/syscall/src/dlfcn/syscall/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use ::alloc::{
collections::btree_map::BTreeMap,
string::String,
sync::Arc,
vec::Vec,
};
use ::elf::{
StringTable,
Expand Down Expand Up @@ -58,9 +59,85 @@ static DYNAMIC_LIBRARY_REGISTRY: Lazy<Mutex<BTreeMap<DlHandle, Arc<Mutex<Dynamic
static GLOBAL_SYMBOL_TABLE: Lazy<Mutex<BTreeMap<String, usize>>> =
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<Mutex<Vec<String>>> =
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<String> = 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
Expand Down