dlsym 0.1.0

Memory-safe bindings around the platform's dynamic library loading primitives
Documentation
//! Loader features beyond basic lookups.
#![allow(clippy::multiple_unsafe_ops_per_block)]

use std::ffi::{CStr, c_char, c_int};

use dlsym::{Error, Library, library_filename};

#[cfg(not(windows))]
fn libc() -> Library {
    Library::this().unwrap()
}

#[cfg(windows)]
fn libc() -> Library {
    // SAFETY: msvcrt is a system library.
    unsafe { Library::open("msvcrt.dll") }.unwrap()
}

#[test]
fn variadic_call() {
    let lib = libc();
    // SAFETY: `sprintf` has this signature.
    let sprintf = unsafe {
        lib.get::<unsafe extern "C" fn(*mut c_char, *const c_char, ...) -> c_int>(c"sprintf")
    }
    .unwrap();
    let mut buf = [0 as c_char; 32];
    // SAFETY: the buffer is large enough and the format matches the
    // arguments; `f` does not escape the closure.
    let n = unsafe { sprintf.with(|f| f(buf.as_mut_ptr(), c"%d-%s".as_ptr(), 42, c"ok".as_ptr())) };
    assert_eq!(n, 5);
    // SAFETY: `sprintf` NUL-terminated the buffer.
    assert_eq!(unsafe { CStr::from_ptr(buf.as_ptr()) }, c"42-ok");
}

#[cfg(all(target_os = "linux", target_env = "gnu"))]
#[test]
fn data_symbol() {
    let lib = libc();
    // SAFETY: glibc exports `program_invocation_name` as a `char *`.
    let name = unsafe { lib.get::<*mut *const c_char>(c"program_invocation_name") }.unwrap();
    // SAFETY: glibc initialises it before `main` and never frees it.
    let name = unsafe { CStr::from_ptr(*name.as_ref()) };
    assert!(!name.to_bytes().is_empty());
}

#[cfg(all(target_os = "linux", target_env = "gnu", target_arch = "x86_64"))]
#[test]
fn versioned_symbol() {
    use dlsym::os::unix::LibraryExt;

    let lib = libc();
    // SAFETY: never called.
    let memcpy = unsafe { lib.get_versioned::<*const u8>(c"memcpy", c"GLIBC_2.14") }.unwrap();
    assert!(!memcpy.as_ptr().is_null());
    // SAFETY: never called.
    let err = unsafe { lib.get_versioned::<*const u8>(c"memcpy", c"DLSYM_9.9") }.unwrap_err();
    assert!(matches!(err, Error::SymbolNotFound { .. }), "{err:?}");
}

#[cfg(target_os = "linux")]
#[test]
fn open_options() {
    use dlsym::os::unix::OpenOptions;

    // SAFETY: libm has no unsound initialisers.
    let libm = unsafe { OpenOptions::new().lazy(true).global(true).open("libm.so.6") }.unwrap();
    // `no_load` only succeeds because `libm` above keeps it loaded.
    // SAFETY: as above.
    let again = unsafe { OpenOptions::new().no_load(true).open("libm.so.6") }.unwrap();
    drop(again);
    drop(libm);
    // SAFETY: nothing is loaded.
    let missing = unsafe { OpenOptions::new().no_load(true).open("libdlsym-missing.so") };
    assert!(missing.is_err());
}

#[cfg(windows)]
#[test]
fn already_loaded() {
    use dlsym::os::windows::LibraryExt;

    let kernel32 = Library::open_already_loaded("kernel32.dll").unwrap();
    // SAFETY: never called.
    let f = unsafe { kernel32.get::<*const u8>(c"GetLastError") }.unwrap();
    assert!(!f.as_ptr().is_null());
    assert!(Library::open_already_loaded("dlsym-definitely-missing.dll").is_err());
}

#[test]
fn open_first_returns_first_loadable() {
    let candidates = [
        "libdlsym-missing-1",
        #[cfg(any(target_os = "linux", target_os = "fullrust"))]
        "libm.so.6",
        #[cfg(target_vendor = "apple")]
        "/usr/lib/libm.dylib",
        #[cfg(windows)]
        "ucrtbase.dll",
        #[cfg(target_os = "freebsd")]
        "libm.so.5",
    ];
    // SAFETY: only system libraries.
    unsafe { Library::open_first(candidates) }.unwrap();
    // SAFETY: nothing loads.
    let err =
        unsafe { Library::open_first(["libdlsym-missing-1", "libdlsym-missing-2"]) }.unwrap_err();
    match err {
        Error::Open { path, .. } => assert_eq!(path, "libdlsym-missing-2"),
        e => panic!("{e:?}"),
    }
    // SAFETY: nothing to load.
    assert!(unsafe { Library::open_first(Vec::<&str>::new()) }.is_err());
}

#[test]
fn leaked_symbols_are_static() {
    let lib: &'static Library = libc().leak();
    // SAFETY: `strlen` has this signature.
    let strlen: dlsym::Symbol<'static, unsafe extern "C" fn(*const c_char) -> usize> =
        unsafe { lib.get(c"strlen") }.unwrap();
    // SAFETY: the argument is a valid C string.
    assert_eq!(unsafe { strlen.call((c"four".as_ptr(),)) }, 4);
}

#[test]
fn filenames() {
    let name = library_filename("z");
    if cfg!(windows) {
        assert_eq!(name, "z.dll");
    } else if cfg!(target_vendor = "apple") {
        assert_eq!(name, "libz.dylib");
    } else {
        assert_eq!(name, "libz.so");
    }
}

#[cfg(not(target_os = "fullrust"))]
#[test]
fn paths_and_address_info() {
    #[cfg(target_os = "linux")]
    let name = "libm.so.6";
    #[cfg(target_vendor = "apple")]
    let name = "/usr/lib/libm.dylib";
    #[cfg(windows)]
    let name = "ucrtbase.dll";
    #[cfg(target_os = "freebsd")]
    let name = "libm.so.5";
    #[cfg(not(any(
        target_os = "linux",
        target_os = "freebsd",
        target_vendor = "apple",
        windows
    )))]
    let name = "libm.so";
    // SAFETY: libm is a system library.
    let lib = unsafe { Library::open(name) }.unwrap();
    // SAFETY: never called.
    let cos = unsafe { lib.get::<*const u8>(c"cos") }.unwrap();
    let info = cos.info().expect("address info");
    let file = info
        .library
        .file_name()
        .unwrap()
        .to_string_lossy()
        .to_lowercase();
    assert!(
        // musl serves libm from its dynamic linker (ld-musl-*.so.1).
        ["libm", "ucrtbase", "libsystem", "musl"]
            .iter()
            .any(|n| file.contains(n)),
        "{info:?}"
    );
    // glibc's `cos` is an IFUNC resolving to a non-exported implementation,
    // so check symbol names on `dlsym` itself. (On FreeBSD, `dlsym` lives in
    // the runtime linker, which `dladdr` does not report.)
    #[cfg(any(target_os = "linux", target_vendor = "apple"))]
    {
        let this = Library::this().unwrap();
        // SAFETY: never called.
        let f = unsafe { this.get::<*const u8>(c"dlsym") }.unwrap();
        let info = f.info().expect("address info");
        assert_eq!(info.symbol.as_deref(), Some(c"dlsym"), "{info:?}");
        assert_eq!(info.symbol_address, f.as_ptr());
    }
    if cfg!(any(
        all(target_os = "linux", target_env = "gnu"),
        target_vendor = "apple",
        windows
    )) {
        let path = lib.path().expect("library path");
        assert!(
            path.is_absolute() || cfg!(target_vendor = "apple"),
            "{path:?}"
        );
        assert!(Library::this().unwrap().path().is_some());
    }
    assert!(dlsym::AddressInfo::of(std::ptr::null()).is_none());
}

/// fullrust loads through dldota, which has no `dladdr` or link-map access.
#[cfg(target_os = "fullrust")]
#[test]
fn no_address_info() {
    // SAFETY: libm is a system library.
    let lib = unsafe { Library::open("libm.so.6") }.unwrap();
    // SAFETY: never called.
    let cos = unsafe { lib.get::<*const u8>(c"cos") }.unwrap();
    assert!(cos.info().is_none());
    assert!(lib.path().is_none());
}

/// Data symbols are not wrapped in trampolines: `environ` points at the
/// foreign libc's environment, which dldota copies from the host's (adding
/// `GLIBC_TUNABLES`).
#[cfg(target_os = "fullrust")]
#[test]
fn foreign_data_symbol() {
    use std::os::unix::ffi::OsStrExt;

    let (key, value) = std::env::vars_os().next().expect("an environment variable");
    let mut wanted = key.as_bytes().to_vec();
    wanted.push(b'=');
    wanted.extend_from_slice(value.as_bytes());

    let lib = libc();
    // SAFETY: glibc and musl both export `environ` as a `char **`.
    let environ = unsafe { lib.get::<*mut *const *const c_char>(c"environ") }.unwrap();
    // SAFETY: the foreign libc initialised it during boot.
    let mut entry = unsafe { *environ.as_ref() };
    assert!(!entry.is_null());
    let mut found = false;
    // SAFETY: a NULL-terminated array of C strings.
    while let Some(s) = unsafe { entry.as_ref() }.filter(|s| !s.is_null()) {
        // SAFETY: each entry is a valid C string.
        found |= unsafe { CStr::from_ptr(*s) }.to_bytes() == wanted;
        // SAFETY: not past the terminating NULL.
        entry = unsafe { entry.add(1) };
    }
    assert!(found, "{key:?} missing from the foreign environment");
}

/// `ret=errno` wrappers read the foreign libc's `errno`, not the host's.
#[cfg(target_os = "fullrust")]
#[test]
fn foreign_errno() {
    let lib = libc();
    // SAFETY: `close` has this signature.
    let close = unsafe { lib.get::<unsafe extern "C" fn(c_int) -> c_int>(c"close") }.unwrap();
    // SAFETY: closing an invalid descriptor only sets `errno`.
    assert_eq!(unsafe { close.call((-1,)) }, -1);
    const EBADF: i32 = 9;
    assert_eq!(dlsym::__private::errno_error().raw_os_error(), Some(EBADF));
}