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 FreeBSD dynamic loader.

#[cfg(test)]
mod tests;

use alloc::borrow::Cow;
use alloc::rc::Rc;
use core::iter::FusedIterator;
use core::mem::{offset_of, size_of};
use std::fs::read_dir;
use std::path::Path;

use nom::branch::alt as nom_alt;
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, terminated as nom_terminated};

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

pub(crate) static CACHE_FILE_PATHS: &[&str] =
    &["/var/run/ld-elf.so.hints", "/var/run/ld-elf32.so.hints"];

const MAGIC: u32 = 0x74_6e_68_45;
const MAGIC_LE32: [u8; 4] = MAGIC.to_le_bytes();
const MAGIC_BE32: [u8; 4] = MAGIC.to_be_bytes();

const VERSION: u32 = 1_u32;

#[repr(C)]
struct Header {
    /// Magic number.
    magic: u32,
    /// File version (1).
    version: u32,
    /// Offset of string table in file.
    string_table_offset: u32,
    /// Size of string table.
    string_table_size: u32,
    /// Offset of directory list in string table.
    dir_list_offset: u32,
    /// strlen(dir_list).
    dir_list_size: u32,
    /// Room for expansion.
    spare: [u32; 26],
}

#[derive(Debug)]
struct ParsedHeader {
    string_table_offset: usize,
    string_table_size: usize,
    dir_list_offset: usize,
    dir_list_size: usize,
}

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

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

        let result = Self {
            string_table_offset: usize::try_from(fields.0)?,
            string_table_size: usize::try_from(fields.1)?,
            dir_list_offset: usize::try_from(fields.2)?,
            dir_list_size: usize::try_from(fields.3)?,
        };

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

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

        nom_alt((
            nom_tag(&MAGIC_LE32[..]).map(|_| Endianness::Little),
            nom_tag(&MAGIC_BE32[..]).map(|_| Endianness::Big),
        ))
        .parse(bytes)
    }

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

        let version_bytes = match byte_order {
            Endianness::Big => VERSION.to_be_bytes(),
            Endianness::Little => VERSION.to_le_bytes(),
            Endianness::Native => VERSION.to_ne_bytes(),
        };

        let mut parser = (
            nom_preceded(nom_tag(&version_bytes[..]), nom_u32(byte_order)),
            nom_u32(byte_order),
            nom_u32(byte_order),
            nom_terminated(
                nom_u32(byte_order),
                nom_take(size_of::<Header>().saturating_sub(offset_of!(Header, spare))),
            ),
        );

        parser.parse(bytes)
    }

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

        let size_after_string_table = bytes.len().saturating_sub(self.string_table_offset);
        let max_dir_list_size = size_after_string_table
            .saturating_sub(self.dir_list_offset)
            .saturating_sub(1);

        if self.string_table_size > size_after_string_table
            || self.dir_list_offset > size_after_string_table
            || self.dir_list_size > max_dir_list_size
        {
            let r = nom::error::make_error(bytes, nom::error::ErrorKind::TooLarge);
            return Err(Error::from_nom_parse(nom::Err::Error(r), bytes, path));
        }

        let string_table_end = self
            .string_table_offset
            .saturating_add(self.string_table_size);
        let dir_list_end = self
            .string_table_offset
            .saturating_add(self.dir_list_offset)
            .saturating_add(self.dir_list_size)
            .saturating_add(1);
        let min_size = string_table_end.max(dir_list_end);

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

/// Cache of the FreeBSD dynamic loader.
///
/// This loads a dynamic loader cache file
/// (*e.g.*, `/var/run/ld-elf.so.hints`, `/var/run/ld-elf32.so.hints`),
/// for either 32-bits or 64-bits architectures, in either little-endian or big-endian byte order.
#[derive(Debug)]
pub struct Cache {
    map: memmap2::Mmap,
    dir_list_offset: usize,
    dir_list_size: usize,
}

impl Cache {
    /// 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)?;

        let dir_list_offset = header
            .string_table_offset
            .saturating_add(header.dir_list_offset);

        Ok(Self {
            map,
            dir_list_offset,
            dir_list_size: header.dir_list_size,
        })
    }

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

        let iter = bytes
            .split(|&b| b == b':')
            .map(path_from_bytes)
            .filter_map(Result::ok)
            .map(Rc::new)
            .filter_map(|path| {
                read_dir(path.as_ref().as_ref())
                    .ok()
                    .map(move |dirs| dirs.map(move |entries| (Rc::clone(&path), entries)))
            })
            .flatten()
            .map(|(path, entry)| match entry {
                Ok(entry) => Ok(crate::Entry {
                    file_name: Cow::Owned(entry.file_name()),
                    full_path: Cow::Owned(entry.path()),
                }),

                Err(source) => {
                    let path = path.as_ref().as_ref().into();
                    Err(Error::ReadDir { path, source })
                }
            });

        Ok(iter)
    }

    fn dir_list_range(&self) -> core::ops::Range<usize> {
        let end = self.dir_list_offset.saturating_add(self.dir_list_size);
        self.dir_list_offset..end
    }
}

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