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 GNU/Linux old dynamic loader.

#[cfg(test)]
mod tests;

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

use nom::bytes::complete::{tag as nom_tag, take as nom_take};
use nom::combinator::peek as nom_peek;
use nom::number::complete::u32 as nom_u32;
use nom::number::Endianness;
use nom::sequence::preceded as nom_preceded;
use static_assertions::assert_eq_size;

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

static CACHE_FILE_PATH: &str = "/etc/ld.so.cache";

static MAGIC: &[u8] = b"ld.so-1.7.0";

#[repr(C)]
struct Header {
    magic: [u8; 11],
    padding: [u8; 1],
    lib_count: c_uint,
}

#[repr(C)]
struct Entry {
    flags: u32,
    key: u32,
    value: u32,
}

#[derive(Debug)]
struct ParsedHeader {
    lib_count: usize,
}

impl ParsedHeader {
    fn parse(path: &Path, bytes: &[u8]) -> Result<Self> {
        let (_input, lib_count) =
            Self::parse_fields(bytes).map_err(|r| Error::from_nom_parse(r, bytes, path))?;

        let result = Self {
            lib_count: usize::try_from(lib_count)?,
        };

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

    fn parse_fields(bytes: &[u8]) -> nom::IResult<&[u8], u32> {
        use nom::Parser;

        assert_eq_size!(u32, c_uint);

        nom_preceded(
            nom_preceded(
                nom_tag(MAGIC),
                nom_take(offset_of!(Header, lib_count).saturating_sub(offset_of!(Header, padding))),
            ),
            nom_u32(Endianness::Native),
        )
        .parse(bytes)
    }

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

        let max_lib_count = bytes.len().saturating_sub(size_of::<Header>()) / size_of::<Entry>();

        if self.lib_count > max_lib_count {
            let r = nom::error::make_error(bytes, nom::error::ErrorKind::TooLarge);
            return Err(Error::from_nom_parse(nom::Err::Error(r), bytes, path));
        }

        let min_size =
            size_of::<Header>().saturating_add(size_of::<Entry>().saturating_mul(self.lib_count));

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

/// Cache of the GNU/Linux old dynamic loader.
///
/// This loads a dynamic loader cache file (*e.g.*, `/etc/ld.so.cache`),
/// in the old `ld.so-1.7.0` format, 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,
    lib_count: usize,
}

impl Cache {
    /// Create a cache that loads the file `/etc/ld.so.cache`.
    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,
            lib_count: header.lib_count,
        })
    }

    /// Return an iterator that returns cache entries.
    pub fn iter(&self) -> Result<impl FusedIterator<Item = Result<crate::Entry<'_>>> + '_> {
        Ok(HashTableIter::<{ size_of::<Entry>() }, _>::new(
            &self.map[self.hash_table_range()],
            CStringTable::new(&self.map[self.string_table_range()], &self.path),
            self.next_hash_table_entry_parser(),
        ))
    }

    fn hash_table_range(&self) -> core::ops::Range<usize> {
        let size = size_of::<Entry>().saturating_mul(self.lib_count);
        size_of::<Header>()..size_of::<Header>().saturating_add(size)
    }

    fn string_table_range(&self) -> core::ops::Range<usize> {
        let size = size_of::<Entry>().saturating_mul(self.lib_count);
        size_of::<Header>().saturating_add(size)..self.map.len()
    }

    fn next_hash_table_entry_parser(
        &self,
    ) -> impl nom::Parser<&[u8], Output = (u32, u32), Error = nom::error::Error<&[u8]>> {
        (
            nom_preceded(
                nom_take(offset_of!(Entry, key)),
                nom_u32(Endianness::Native),
            ),
            nom_u32(Endianness::Native),
        )
    }
}

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))
    }
}