dynamic-loader-cache 0.2.3

Reader of the dynamic loader shared libraries cache
Documentation
// Copyright 2024-2025 Koutheir Attouchi.
// See the "LICENSE.txt" file at the top-level directory of this distribution.
//
// Licensed under the MIT license. This file may not be copied, modified,
// or distributed except according to those terms.

//! Cache of the OpenBSD or NetBSD dynamic loader.

#[cfg(test)]
mod tests;

use core::ffi::c_int;
use core::iter::FusedIterator;
use core::mem::{offset_of, size_of};
use std::path::{Path, PathBuf};

use nom::branch::alt as nom_alt;
use nom::bytes::complete::{tag as nom_tag, take as nom_take};
use nom::combinator::{into as nom_into, peek as nom_peek};
use nom::number::complete::{u32 as nom_u32, u64 as nom_u64};
use nom::number::Endianness;
use nom::sequence::terminated as nom_terminated;
use static_assertions::assert_eq_size;

use crate::utils::{map_file, CStringTable, HashTableIter};
use crate::{CacheProvider, DataModel, Error, Result};

static CACHE_FILE_PATH: &str = "/var/run/ld.so.hints";

const MAGIC: u32 = 0x4c_44_48_69_u32;
const MAGIC_LE32: [u8; 4] = MAGIC.to_le_bytes();
const MAGIC_BE32: [u8; 4] = MAGIC.to_be_bytes();
const MAGIC_LE64: [u8; 8] = (MAGIC as u64).to_le_bytes();
const MAGIC_BE64: [u8; 8] = (MAGIC as u64).to_be_bytes();

//const VERSION_1: u32 = 1; // We do not support this ancient version.

const VERSION_2: u32 = 2;
const VERSION_2_LE32: [u8; 4] = VERSION_2.to_le_bytes();
const VERSION_2_BE32: [u8; 4] = VERSION_2.to_be_bytes();
const VERSION_2_LE64: [u8; 8] = (VERSION_2 as u64).to_le_bytes();
const VERSION_2_BE64: [u8; 8] = (VERSION_2 as u64).to_be_bytes();

/// Maximum number of recognized shared object version numbers.
const MAX_DEWEY: usize = 8;

/*
/// Header of the hints file.
#[repr(C)]
struct Header {
    magic: c_long,
    /// Interface version number.
    version: c_long,
    /// Location of hash table.
    hash_table: c_long,
    /// Number of buckets in hash_table.
    bucket_count: c_long,
    /// Location of strings.
    string_table: c_long,
    /// Size of strings.
    string_table_size: c_long,
    /// End of hints (max offset in file).
    end_of_hints: c_long,
    /// Colon-separated list of search dirs.
    dir_list: c_long,
}
*/

/// Hash table element in hints file.
#[repr(C)]
struct Bucket {
    /// Index of the library name into the string table.
    name_index: c_int,
    /// Index of the full path into the string table.
    path_index: c_int,
    /// The versions.
    dewey: [c_int; MAX_DEWEY],
    /// Number of version numbers.
    dewey_count: c_int,
    /// Next in this bucket.
    next: c_int,
}

type ParsedFields = (u64, u64, u64, u64, u64, u64);

#[derive(Debug)]
struct ParsedHeader {
    byte_order: Endianness,
    hash_table: u64,
    bucket_count: u64,
    string_table: u64,
    string_table_size: u64,
    end_of_hints: u64,
    _dir_list: u64,
}

impl ParsedHeader {
    fn parse(path: &Path, bytes: &[u8]) -> Result<Self> {
        assert_eq_size!(u32, c_int);

        let (input, (data_model, byte_order)) =
            Self::parse_byte_order(bytes).map_err(|r| Error::from_nom_parse(r, bytes, path))?;

        let (_input, fields) = Self::parse_fields(input, data_model, byte_order)
            .map_err(|r| Error::from_nom_parse(r, input, path))?;

        let result = Self {
            byte_order,
            hash_table: fields.0,
            bucket_count: fields.1,
            string_table: fields.2,
            string_table_size: fields.3,
            end_of_hints: fields.4,
            _dir_list: fields.5,
        };

        result.validate(path, bytes).map(|()| result)
    }

    fn parse_byte_order(bytes: &[u8]) -> nom::IResult<&[u8], (DataModel, Endianness)> {
        use nom::Parser;

        nom_alt((
            nom_terminated(nom_tag(&MAGIC_LE64[..]), nom_tag(&VERSION_2_LE64[..]))
                .map(|_| (DataModel::LP64, Endianness::Little)),
            nom_terminated(nom_tag(&MAGIC_BE64[..]), nom_tag(&VERSION_2_BE64[..]))
                .map(|_| (DataModel::LP64, Endianness::Big)),
            nom_terminated(nom_tag(&MAGIC_LE32[..]), nom_tag(&VERSION_2_LE32[..]))
                .map(|_| (DataModel::ILP32, Endianness::Little)),
            nom_terminated(nom_tag(&MAGIC_BE32[..]), nom_tag(&VERSION_2_BE32[..]))
                .map(|_| (DataModel::ILP32, Endianness::Big)),
        ))
        .parse(bytes)
    }

    fn parse_fields(
        input: &[u8],
        data_model: DataModel,
        byte_order: Endianness,
    ) -> nom::IResult<&[u8], ParsedFields> {
        use nom::Parser;

        match data_model {
            DataModel::ILP32 => {
                let parse_u32 = nom_u32(byte_order);
                let mut parser = (
                    nom_into(&parse_u32),
                    nom_into(&parse_u32),
                    nom_into(&parse_u32),
                    nom_into(&parse_u32),
                    nom_into(&parse_u32),
                    nom_into(&parse_u32),
                );
                parser.parse(input)
            }
            DataModel::LP64 => {
                let parse_u64 = nom_u64(byte_order);
                let mut parser = (
                    &parse_u64, &parse_u64, &parse_u64, &parse_u64, &parse_u64, &parse_u64,
                );
                parser.parse(input)
            }
        }
    }

    fn validate(&self, path: &Path, bytes: &[u8]) -> Result<()> {
        use nom::Parser;

        let hash_table_size = self.bucket_count.saturating_mul(size_of::<Bucket>() as u64);
        let hash_table_end = self.hash_table.saturating_add(hash_table_size);
        let string_table_end = self.string_table.saturating_add(self.string_table_size);

        let min_size = hash_table_end.max(string_table_end).max(self.end_of_hints);
        let min_size = usize::try_from(min_size)?;

        nom_peek(nom_take(min_size))
            .parse(bytes)
            .map(|_| ())
            .map_err(|r| Error::from_nom_parse(r, bytes, path))
    }
}

/// Cache of the OpenBSD or NetBSD dynamic loader.
///
/// This loads a dynamic loader cache file (*e.g.*, `/var/run/ld.so.hints`),
/// for either 32-bits or 64-bits architectures, in either little-endian or big-endian byte order.
#[derive(Debug)]
pub struct Cache {
    path: PathBuf,
    map: memmap2::Mmap,
    byte_order: Endianness,
    hash_table: u64,
    bucket_count: u64,
    string_table: u64,
    string_table_size: u64,
}

impl Cache {
    /// Create a cache that loads the file `/var/run/ld.so.hints`.
    pub fn load_default() -> Result<Self> {
        Self::load(CACHE_FILE_PATH)
    }

    /// Create a cache that loads the specified cache file.
    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
        let path = path.as_ref();
        let map = map_file(path)?;
        let header = ParsedHeader::parse(path, &map)?;

        Ok(Self {
            path: path.into(),
            map,
            byte_order: header.byte_order,
            hash_table: header.hash_table,
            bucket_count: header.bucket_count,
            string_table: header.string_table,
            string_table_size: header.string_table_size,
        })
    }

    /// Return an iterator that returns cache entries.
    pub fn iter(&self) -> Result<impl FusedIterator<Item = Result<crate::Entry<'_>>> + '_> {
        let hash_table_range = self.hash_table_range()?;
        let string_table_range = self.string_table_range()?;

        Ok(HashTableIter::<{ size_of::<Bucket>() }, _>::new(
            &self.map[hash_table_range],
            CStringTable::new(&self.map[string_table_range], &self.path),
            self.next_hash_table_entry_parser(),
        ))
    }

    fn hash_table_range(&self) -> Result<core::ops::Range<usize>> {
        let start = usize::try_from(self.hash_table)?;
        let size = self.bucket_count.saturating_mul(size_of::<Bucket>() as u64);
        let end = usize::try_from(self.hash_table.saturating_add(size))?;
        Ok(start..end)
    }

    fn string_table_range(&self) -> Result<core::ops::Range<usize>> {
        let start = usize::try_from(self.string_table)?;
        let end = usize::try_from(self.string_table.saturating_add(self.string_table_size))?;
        Ok(start..end)
    }

    fn next_hash_table_entry_parser(
        &self,
    ) -> impl nom::Parser<&[u8], Output = (u32, u32), Error = nom::error::Error<&[u8]>> {
        (
            nom_u32(self.byte_order),
            nom_terminated(
                nom_u32(self.byte_order),
                nom_take(size_of::<Bucket>().wrapping_sub(offset_of!(Bucket, dewey))),
            ),
        )
    }
}

impl CacheProvider for Cache {
    fn entries_iter<'cache>(
        &'cache self,
    ) -> Result<Box<dyn FusedIterator<Item = Result<crate::Entry<'cache>>> + 'cache>> {
        let iter = self.iter()?;
        Ok(Box::new(iter))
    }
}