dynamic_loader_cache/
ld_so_hints.rs

1// Copyright 2024-2025 Koutheir Attouchi.
2// See the "LICENSE.txt" file at the top-level directory of this distribution.
3//
4// Licensed under the MIT license. This file may not be copied, modified,
5// or distributed except according to those terms.
6
7//! Cache of the OpenBSD or NetBSD dynamic loader.
8
9#[cfg(test)]
10mod tests;
11
12use core::ffi::c_int;
13use core::iter::FusedIterator;
14use core::mem::{offset_of, size_of};
15use std::path::{Path, PathBuf};
16
17use nom::branch::alt as nom_alt;
18use nom::bytes::complete::{tag as nom_tag, take as nom_take};
19use nom::combinator::{into as nom_into, peek as nom_peek};
20use nom::number::complete::{u32 as nom_u32, u64 as nom_u64};
21use nom::number::Endianness;
22use nom::sequence::terminated as nom_terminated;
23use static_assertions::assert_eq_size;
24
25use crate::utils::{map_file, CStringTable, HashTableIter};
26use crate::{CacheProvider, DataModel, Error, Result};
27
28static CACHE_FILE_PATH: &str = "/var/run/ld.so.hints";
29
30const MAGIC: u32 = 0x4c_44_48_69_u32;
31const MAGIC_LE32: [u8; 4] = MAGIC.to_le_bytes();
32const MAGIC_BE32: [u8; 4] = MAGIC.to_be_bytes();
33const MAGIC_LE64: [u8; 8] = (MAGIC as u64).to_le_bytes();
34const MAGIC_BE64: [u8; 8] = (MAGIC as u64).to_be_bytes();
35
36//const VERSION_1: u32 = 1; // We do not support this ancient version.
37
38const VERSION_2: u32 = 2;
39const VERSION_2_LE32: [u8; 4] = VERSION_2.to_le_bytes();
40const VERSION_2_BE32: [u8; 4] = VERSION_2.to_be_bytes();
41const VERSION_2_LE64: [u8; 8] = (VERSION_2 as u64).to_le_bytes();
42const VERSION_2_BE64: [u8; 8] = (VERSION_2 as u64).to_be_bytes();
43
44/// Maximum number of recognized shared object version numbers.
45const MAX_DEWEY: usize = 8;
46
47/*
48/// Header of the hints file.
49#[repr(C)]
50struct Header {
51    magic: c_long,
52    /// Interface version number.
53    version: c_long,
54    /// Location of hash table.
55    hash_table: c_long,
56    /// Number of buckets in hash_table.
57    bucket_count: c_long,
58    /// Location of strings.
59    string_table: c_long,
60    /// Size of strings.
61    string_table_size: c_long,
62    /// End of hints (max offset in file).
63    end_of_hints: c_long,
64    /// Colon-separated list of search dirs.
65    dir_list: c_long,
66}
67*/
68
69/// Hash table element in hints file.
70#[repr(C)]
71struct Bucket {
72    /// Index of the library name into the string table.
73    name_index: c_int,
74    /// Index of the full path into the string table.
75    path_index: c_int,
76    /// The versions.
77    dewey: [c_int; MAX_DEWEY],
78    /// Number of version numbers.
79    dewey_count: c_int,
80    /// Next in this bucket.
81    next: c_int,
82}
83
84type ParsedFields = (u64, u64, u64, u64, u64, u64);
85
86#[derive(Debug)]
87struct ParsedHeader {
88    byte_order: Endianness,
89    hash_table: u64,
90    bucket_count: u64,
91    string_table: u64,
92    string_table_size: u64,
93    end_of_hints: u64,
94    _dir_list: u64,
95}
96
97impl ParsedHeader {
98    fn parse(path: &Path, bytes: &[u8]) -> Result<Self> {
99        assert_eq_size!(u32, c_int);
100
101        let (input, (data_model, byte_order)) =
102            Self::parse_byte_order(bytes).map_err(|r| Error::from_nom_parse(r, bytes, path))?;
103
104        let (_input, fields) = Self::parse_fields(input, data_model, byte_order)
105            .map_err(|r| Error::from_nom_parse(r, input, path))?;
106
107        let result = Self {
108            byte_order,
109            hash_table: fields.0,
110            bucket_count: fields.1,
111            string_table: fields.2,
112            string_table_size: fields.3,
113            end_of_hints: fields.4,
114            _dir_list: fields.5,
115        };
116
117        result.validate(path, bytes).map(|()| result)
118    }
119
120    fn parse_byte_order(bytes: &[u8]) -> nom::IResult<&[u8], (DataModel, Endianness)> {
121        use nom::Parser;
122
123        nom_alt((
124            nom_terminated(nom_tag(&MAGIC_LE64[..]), nom_tag(&VERSION_2_LE64[..]))
125                .map(|_| (DataModel::LP64, Endianness::Little)),
126            nom_terminated(nom_tag(&MAGIC_BE64[..]), nom_tag(&VERSION_2_BE64[..]))
127                .map(|_| (DataModel::LP64, Endianness::Big)),
128            nom_terminated(nom_tag(&MAGIC_LE32[..]), nom_tag(&VERSION_2_LE32[..]))
129                .map(|_| (DataModel::ILP32, Endianness::Little)),
130            nom_terminated(nom_tag(&MAGIC_BE32[..]), nom_tag(&VERSION_2_BE32[..]))
131                .map(|_| (DataModel::ILP32, Endianness::Big)),
132        ))
133        .parse(bytes)
134    }
135
136    fn parse_fields(
137        input: &[u8],
138        data_model: DataModel,
139        byte_order: Endianness,
140    ) -> nom::IResult<&[u8], ParsedFields> {
141        use nom::Parser;
142
143        match data_model {
144            DataModel::ILP32 => {
145                let parse_u32 = nom_u32(byte_order);
146                let mut parser = (
147                    nom_into(&parse_u32),
148                    nom_into(&parse_u32),
149                    nom_into(&parse_u32),
150                    nom_into(&parse_u32),
151                    nom_into(&parse_u32),
152                    nom_into(&parse_u32),
153                );
154                parser.parse(input)
155            }
156            DataModel::LP64 => {
157                let parse_u64 = nom_u64(byte_order);
158                let mut parser = (
159                    &parse_u64, &parse_u64, &parse_u64, &parse_u64, &parse_u64, &parse_u64,
160                );
161                parser.parse(input)
162            }
163        }
164    }
165
166    fn validate(&self, path: &Path, bytes: &[u8]) -> Result<()> {
167        use nom::Parser;
168
169        let hash_table_size = self.bucket_count.saturating_mul(size_of::<Bucket>() as u64);
170        let hash_table_end = self.hash_table.saturating_add(hash_table_size);
171        let string_table_end = self.string_table.saturating_add(self.string_table_size);
172
173        let min_size = hash_table_end.max(string_table_end).max(self.end_of_hints);
174        let min_size = usize::try_from(min_size)?;
175
176        nom_peek(nom_take(min_size))
177            .parse(bytes)
178            .map(|_| ())
179            .map_err(|r| Error::from_nom_parse(r, bytes, path))
180    }
181}
182
183/// Cache of the OpenBSD or NetBSD dynamic loader.
184///
185/// This loads a dynamic loader cache file (*e.g.*, `/var/run/ld.so.hints`),
186/// for either 32-bits or 64-bits architectures, in either little-endian or big-endian byte order.
187#[derive(Debug)]
188pub struct Cache {
189    path: PathBuf,
190    map: memmap2::Mmap,
191    byte_order: Endianness,
192    hash_table: u64,
193    bucket_count: u64,
194    string_table: u64,
195    string_table_size: u64,
196}
197
198impl Cache {
199    /// Create a cache that loads the file `/var/run/ld.so.hints`.
200    pub fn load_default() -> Result<Self> {
201        Self::load(CACHE_FILE_PATH)
202    }
203
204    /// Create a cache that loads the specified cache file.
205    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
206        let path = path.as_ref();
207        let map = map_file(path)?;
208        let header = ParsedHeader::parse(path, &map)?;
209
210        Ok(Self {
211            path: path.into(),
212            map,
213            byte_order: header.byte_order,
214            hash_table: header.hash_table,
215            bucket_count: header.bucket_count,
216            string_table: header.string_table,
217            string_table_size: header.string_table_size,
218        })
219    }
220
221    /// Return an iterator that returns cache entries.
222    pub fn iter(&self) -> Result<impl FusedIterator<Item = Result<crate::Entry<'_>>> + '_> {
223        let hash_table_range = self.hash_table_range()?;
224        let string_table_range = self.string_table_range()?;
225
226        Ok(HashTableIter::<{ size_of::<Bucket>() }, _>::new(
227            &self.map[hash_table_range],
228            CStringTable::new(&self.map[string_table_range], &self.path),
229            self.next_hash_table_entry_parser(),
230        ))
231    }
232
233    fn hash_table_range(&self) -> Result<core::ops::Range<usize>> {
234        let start = usize::try_from(self.hash_table)?;
235        let size = self.bucket_count.saturating_mul(size_of::<Bucket>() as u64);
236        let end = usize::try_from(self.hash_table.saturating_add(size))?;
237        Ok(start..end)
238    }
239
240    fn string_table_range(&self) -> Result<core::ops::Range<usize>> {
241        let start = usize::try_from(self.string_table)?;
242        let end = usize::try_from(self.string_table.saturating_add(self.string_table_size))?;
243        Ok(start..end)
244    }
245
246    fn next_hash_table_entry_parser(
247        &self,
248    ) -> impl nom::Parser<&[u8], Output = (u32, u32), Error = nom::error::Error<&[u8]>> {
249        (
250            nom_u32(self.byte_order),
251            nom_terminated(
252                nom_u32(self.byte_order),
253                nom_take(size_of::<Bucket>().wrapping_sub(offset_of!(Bucket, dewey))),
254            ),
255        )
256    }
257}
258
259impl CacheProvider for Cache {
260    fn entries_iter<'cache>(
261        &'cache self,
262    ) -> Result<Box<dyn FusedIterator<Item = Result<crate::Entry<'cache>>> + 'cache>> {
263        let iter = self.iter()?;
264        Ok(Box::new(iter))
265    }
266}