syd 3.58.0

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/t/dl.rs: Safe interface to dynamic linking loader
//
// Copyright (c) 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

//! Safe interface to dynamic linking loader

#![expect(dead_code)]

use std::{
    error::Error,
    ffi::{CStr, CString},
    fmt,
    ptr::NonNull,
};

use libc::{c_int, c_void, RTLD_GLOBAL, RTLD_LAZY, RTLD_LOCAL, RTLD_NOLOAD, RTLD_NOW};
use syd::path::XPath;

/// Check whether getrandom(2) is in VDSO.
pub fn check_vdso_has_getrandom() -> bool {
    has_vdso_symbol(c"__vdso_getrandom")
}

/// Enumerate vDSO call names present in this process.
pub fn vdso_list_calls() -> Result<Vec<&'static CStr>, DlError> {
    const KERN_LEN: usize = 9; // "__kernel_"
    const VDSO_LEN: usize = 7; // "__vdso_"
    const VDSO_IDX: usize = 10; // index into array.
    const VDSO_CALL_NAMES: &[&CStr] = &[
        c"__kernel_clock_getres",
        c"__kernel_clock_getres_time64",
        c"__kernel_clock_gettime",
        c"__kernel_clock_gettime64",
        c"__kernel_getcpu",
        c"__kernel_getrandom",
        c"__kernel_gettimeofday",
        c"__kernel_get_tbfreq",    // powerpc
        c"__kernel_riscv_hwprobe", // riscv
        c"__kernel_time",
        c"__vdso_clock_getres",
        c"__vdso_clock_getres_time64",
        c"__vdso_clock_gettime",
        c"__vdso_clock_gettime64",
        c"__vdso_getcpu",
        c"__vdso_getrandom",
        c"__vdso_gettimeofday",
        c"__vdso_get_tbfreq",    // powerpc
        c"__vdso_riscv_hwprobe", // riscv
        c"__vdso_time",
    ];

    let vdso = vdso_open()?;
    let mut out = Vec::with_capacity(VDSO_CALL_NAMES.len());
    for (idx, &name) in VDSO_CALL_NAMES.iter().enumerate() {
        if vdso.has_symbol(name) {
            let plen = if idx < VDSO_IDX { KERN_LEN } else { VDSO_LEN };
            let bytes = name.to_bytes_with_nul();
            // SAFETY:
            // 1. All entries start with "__vdso_"
            // 2. We keep the trailing NUL, producing a valid CStr that
            //    borrows from the static symbol literal.
            out.push(unsafe { CStr::from_bytes_with_nul_unchecked(&bytes[plen..]) });
        }
    }
    Ok(out)
}

/// Probe vDSO for symbol.
pub fn has_vdso_symbol(sym: &CStr) -> bool {
    vdso_open()
        .map(|vdso| vdso.has_symbol(sym))
        .unwrap_or(false)
}

/// Open vDSO and return Library instance.
pub fn vdso_open() -> Result<Library, DlError> {
    vdso_open_by_name(c"linux-vdso.so.1").or_else(|_| vdso_open_by_name(c"linux-vdso64.so.1"))
}

/// Open vDSO by name and return Library instance.
pub fn vdso_open_by_name(name: &CStr) -> Result<Library, DlError> {
    Library::open(Some(name), DlFlags::NOLOAD | DlFlags::LOCAL | DlFlags::NOW)
}

/// Probes process link-map for a symbol by name.
pub fn has_symbol(sym: &CStr) -> bool {
    Library::this(DlFlags::NOW)
        .map(|lib| lib.has_symbol(sym))
        .unwrap_or(false)
}

/// Safe handle to a dynamically loaded object
pub struct Library {
    handle: NonNull<c_void>,
}

impl Library {
    /// Open the shared object `name` with given dlopen(3) `flags`.
    ///
    /// A `None` name yields a handle to global symbol scope.
    pub fn open(name: Option<&CStr>, flags: DlFlags) -> Result<Library, DlError> {
        clear_error();

        let name = name.map_or(std::ptr::null(), CStr::as_ptr);
        // SAFETY:
        // 1. Name is either NULL for global scope, or
        // 2. Name is a valid NUL-terminated string.
        // 3. flags are libc RTLD_* bits.
        let handle = unsafe { libc::dlopen(name, flags.bits()) };

        NonNull::new(handle)
            .map(|handle| Library { handle })
            .ok_or_else(DlError::last)
    }

    /// Return a handle to global symbol scope.
    pub fn this(flags: DlFlags) -> Result<Library, DlError> {
        Library::open(None, flags)
    }

    /// Look up `name` in this object and return its address.
    pub fn symbol(&self, name: &CStr) -> Result<NonNull<c_void>, DlError> {
        clear_error();

        // SAFETY:
        // 1. Handle is a dlopen(3) result.
        // 2. Name is a valid NUL-terminated symbol name.
        let addr = unsafe { libc::dlsym(self.handle.as_ptr(), name.as_ptr()) };
        if let Some(addr) = NonNull::new(addr) {
            return Ok(addr);
        }

        // SAFETY: dlerror(3) only reads loader's error state.
        if unsafe { libc::dlerror() }.is_null() {
            Err(DlError::msg(c"symbol resolved to a NULL address"))
        } else {
            Err(DlError::last())
        }
    }

    /// Return whether this object exports symbol `name`.
    pub fn has_symbol(&self, name: &CStr) -> bool {
        self.symbol(name).is_ok()
    }
}

impl Drop for Library {
    fn drop(&mut self) {
        // SAFETY: Handle is a dlopen(3) result, closed exactly once.
        let _ = unsafe { libc::dlclose(self.handle.as_ptr()) };
    }
}

bitflags::bitflags! {
    /// Flags for dlopen(3).
    #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
    pub struct DlFlags: c_int {
        /// Resolve undefined symbols lazily.
        const LAZY = RTLD_LAZY;
        /// Resolve all undefined symbols immediately.
        const NOW = RTLD_NOW;
        /// Make this object's symbols available to later loads.
        const GLOBAL = RTLD_GLOBAL;
        /// Keep this object's symbols local to it.
        const LOCAL = RTLD_LOCAL;
        /// Only return a handle if object is already loaded.
        const NOLOAD = RTLD_NOLOAD;
    }
}

fn clear_error() {
    // SAFETY: dlerror only reads and resets loader's error state.
    let _ = unsafe { libc::dlerror() };
}

/// Error reported by dynamic linker.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DlError {
    message: CString,
}

impl DlError {
    fn msg(message: &CStr) -> DlError {
        let bytes = message.to_bytes_with_nul();
        let mut buf = Vec::new();
        if buf.try_reserve_exact(bytes.len()).is_ok() {
            buf.extend_from_slice(bytes);
        }
        let message = CString::from_vec_with_nul(buf).unwrap_or_default();
        DlError { message }
    }

    fn last() -> DlError {
        // SAFETY:
        // 1. dlerror(3) takes no arguments.
        // 2. dlerror(3) returns a pointer to a NUL-terminated string, or
        // 3. dlerror(3) returns NULL when no error is pending.
        let ptr = unsafe { libc::dlerror() };
        if ptr.is_null() {
            return DlError::msg(c"unknown dynamic linker error");
        }
        // SAFETY: ptr is a NUL-terminated string owned by loader.
        let message = unsafe { CStr::from_ptr(ptr) };
        DlError::msg(message)
    }

    /// Return dlerror(3) message as `CStr`.
    pub fn message(&self) -> &CStr {
        &self.message
    }

    /// Return dlerror(3) message as `XPath`.
    pub fn as_xpath(&self) -> &XPath {
        XPath::from_bytes(self.message.to_bytes())
    }
}

impl fmt::Display for DlError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // SAFETY: Mask control characters in path.
        fmt::Display::fmt(self.as_xpath(), f)
    }
}

impl Error for DlError {}

#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
    use super::*;

    #[test]
    fn test_dlerror_1() {
        let e = DlError::msg(c"boom");
        assert_eq!(e.message().to_bytes(), b"boom");
        assert_eq!(e.as_xpath().as_bytes(), b"boom");
        assert_eq!(format!("{e}"), "boom");
    }

    #[test]
    fn test_dlerror_2() {
        clear_error();
        let e = DlError::last();
        assert_eq!(e.message().to_bytes(), b"unknown dynamic linker error");
    }

    #[test]
    fn test_dlerror_3() {
        let e = DlError::msg(c"x");
        assert_eq!(e, DlError::msg(c"x"));
        assert_ne!(e, DlError::msg(c"y"));
        assert!(format!("{e:?}").contains("DlError"));
    }

    #[cfg(not(target_env = "musl"))]
    #[test]
    fn test_library_1() {
        let lib = Library::open(Some(c"libc.so.6"), DlFlags::NOW | DlFlags::LOCAL).unwrap();
        assert!(lib.symbol(c"malloc").is_ok());
        assert!(lib.has_symbol(c"malloc"));
    }

    #[test]
    fn test_library_2() {
        let e = Library::open(Some(c"syd-no-such-lib-xyz.so"), DlFlags::NOW)
            .err()
            .unwrap();
        assert!(!e.message().to_bytes().is_empty());
    }

    #[cfg(not(target_env = "musl"))]
    #[test]
    fn test_library_3() {
        let lib = Library::this(DlFlags::NOW).unwrap();
        if Library::open(
            Some(c"libc.so.6"),
            DlFlags::NOLOAD | DlFlags::NOW | DlFlags::LOCAL,
        )
        .is_ok()
        {
            assert!(lib.has_symbol(c"malloc"));
        }
        assert!(!lib.has_symbol(c"syd_no_such_symbol_xyz"));
        assert!(lib.symbol(c"syd_no_such_symbol_xyz").is_err());
    }

    #[cfg(not(target_env = "musl"))]
    #[test]
    fn test_vdso_open_1() {
        assert!(vdso_open().is_ok());
    }

    #[cfg(not(target_env = "musl"))]
    #[test]
    fn test_vdso_open_by_name_1() {
        assert!(vdso_open_by_name(c"linux-vdso.so.1").is_ok());
        assert!(vdso_open_by_name(c"syd-no-such-vdso.so").is_err());
    }

    #[cfg(not(target_env = "musl"))]
    #[test]
    fn test_vdso_list_calls_1() {
        assert!(!vdso_list_calls().unwrap().is_empty());
    }

    #[cfg(not(target_env = "musl"))]
    #[test]
    fn test_has_vdso_symbol_1() {
        assert!(has_vdso_symbol(c"__vdso_gettimeofday"));
        assert!(!has_vdso_symbol(c"__vdso_syd_no_such"));
    }

    #[test]
    fn test_check_vdso_has_getrandom_1() {
        assert_eq!(
            check_vdso_has_getrandom(),
            has_vdso_symbol(c"__vdso_getrandom")
        );
    }

    #[cfg(not(target_env = "musl"))]
    #[test]
    fn test_has_symbol_1() {
        if Library::open(
            Some(c"libc.so.6"),
            DlFlags::NOLOAD | DlFlags::NOW | DlFlags::LOCAL,
        )
        .is_ok()
        {
            assert!(has_symbol(c"malloc"));
        }
        assert!(!has_symbol(c"syd_no_such_symbol_xyz"));
    }
}