Skip to main content

elfpak_core/resolver/
cache.rs

1//! Direct `/etc/ld.so.cache` reading and writing.
2//!
3//! `ldconfig` is never invoked. Both the historical `ld.so-1.7.0` layout and the
4//! current `glibc-ld.so.cache1.1` layout are understood on the way in, including
5//! the common case where a new-format cache is appended after an old-format one.
6//!
7//! On the way out, [`build`] emits a `glibc-ld.so.cache1.1` image for the
8//! bundle. Without one, a packaged application cannot find a library outside
9//! the directories the loader searches by default: the rootfs carries no
10//! `ldconfig`, and the build host's cache describes the host's filesystem.
11
12use crate::elf::{Architecture, ElfClass, Endianness, Machine};
13use std::{
14    cmp::Ordering,
15    collections::HashMap,
16    path::{Path, PathBuf},
17};
18
19const OLD_MAGIC: &[u8] = b"ld.so-1.7.0";
20const NEW_MAGIC: &[u8] = b"glibc-ld.so.cache";
21const NEW_VERSION: &[u8] = b"1.1";
22
23/// Header size of `struct cache_file` including alignment padding.
24const OLD_HEADER_LEN: usize = 16;
25const OLD_ENTRY_LEN: usize = 12;
26/// Header size of `struct cache_file_new`.
27const NEW_HEADER_LEN: usize = 48;
28const NEW_ENTRY_LEN: usize = 24;
29
30/// Upper bound on the entries taken from a cache image.
31///
32/// A distribution cache holds a few thousand libraries. `nlibs` comes out of
33/// the file and is never trusted, so this bounds what is read.
34const CACHE_ENTRIES_MAX: usize = 65_536;
35/// A cache is a small index, not an arbitrary data container. This prevents a
36/// hostile sysroot from making planning allocate or repeatedly scan huge data.
37const CACHE_BYTES_MAX: u64 = 16 * 1024 * 1024;
38/// Both ELF sonames and filesystem components are far shorter in practice;
39/// this also bounds malformed unterminated-string scans.
40const CACHE_STRING_LEN_MAX: usize = 4096;
41/// A loader lookup should not turn one malicious soname into tens of thousands
42/// of filesystem probes. Normal caches have only a handful of alternatives.
43const CACHE_CANDIDATES_PER_SONAME_MAX: usize = 256;
44
45#[derive(Debug, Clone, Default)]
46pub struct LdCache {
47    /// soname -> candidate absolute paths, in cache order.
48    entries: HashMap<String, Vec<PathBuf>>,
49    /// Same candidates with loader-selection metadata retained.
50    records: HashMap<String, Vec<CacheRecord>>,
51    /// Candidates kept, i.e. the total length of the lists above. Relative and
52    /// duplicate entries are dropped on the way in and are not counted.
53    len: usize,
54}
55
56/// One raw cache entry. The loader checks its ABI and hardware requirements
57/// before considering the pathname, so preserving this metadata is essential
58/// when planning from a foreign sysroot.
59#[derive(Debug, Clone)]
60struct CacheRecord {
61    soname: String,
62    path: PathBuf,
63    flags: u32,
64    osversion: u32,
65    hwcap: u64,
66}
67
68impl LdCache {
69    /// Parse a cache image. A malformed cache yields no entries instead of
70    /// failing the build; the cache is a hint and the search paths remain.
71    pub fn parse(bytes: &[u8]) -> LdCache {
72        let mut cache = LdCache::default();
73        if u64::try_from(bytes.len())
74            .ok()
75            .is_none_or(|len| len > CACHE_BYTES_MAX)
76        {
77            return cache;
78        }
79        let pairs = if starts_with(bytes, 0, NEW_MAGIC) {
80            parse_new(bytes, 0)
81        } else if starts_with(bytes, 0, OLD_MAGIC) {
82            let nlibs = match read_u32(bytes, 12) {
83                Some(n) => n as usize,
84                None => return cache,
85            };
86            let new_offset = align8(
87                nlibs
88                    .saturating_mul(OLD_ENTRY_LEN)
89                    .saturating_add(OLD_HEADER_LEN),
90            );
91            if starts_with(bytes, new_offset, NEW_MAGIC) {
92                parse_new(bytes, new_offset)
93            } else {
94                parse_old(bytes, nlibs)
95            }
96        } else {
97            Vec::new()
98        };
99
100        for record in pairs {
101            // ldconfig only records absolute paths. A relative one would be
102            // resolved against this process's working directory downstream.
103            if !record.path.is_absolute() {
104                continue;
105            }
106            let list = cache.entries.entry(record.soname.clone()).or_default();
107            if list.len() < CACHE_CANDIDATES_PER_SONAME_MAX && !list.contains(&record.path) {
108                list.push(record.path.clone());
109                cache
110                    .records
111                    .entry(record.soname.clone())
112                    .or_default()
113                    .push(record);
114                cache.len += 1;
115            }
116        }
117        cache
118    }
119
120    pub fn load(path: &Path) -> Option<LdCache> {
121        if std::fs::metadata(path).ok()?.len() > CACHE_BYTES_MAX {
122            return None;
123        }
124        let bytes = std::fs::read(path).ok()?;
125        let cache = LdCache::parse(&bytes);
126        if cache.entries.is_empty() {
127            None
128        } else {
129            Some(cache)
130        }
131    }
132
133    pub fn lookup(&self, soname: &str) -> &[PathBuf] {
134        self.entries.get(soname).map(Vec::as_slice).unwrap_or(&[])
135    }
136
137    /// Candidates usable by a portable bundle for `architecture`.
138    ///
139    /// Cache flags encode the ELF ABI. Entries requiring an OS version or CPU
140    /// hwcap are intentionally skipped because a sysroot does not establish a
141    /// deployment kernel or CPU baseline.
142    pub fn lookup_compatible(&self, soname: &str, architecture: &Architecture) -> Vec<PathBuf> {
143        let Some(expected_flags) =
144            entry_flags(architecture).and_then(|flags| u32::try_from(flags).ok())
145        else {
146            return Vec::new();
147        };
148        self.records
149            .get(soname)
150            .into_iter()
151            .flatten()
152            .filter(|entry| {
153                entry.flags == expected_flags && entry.osversion == 0 && entry.hwcap == 0
154            })
155            .map(|entry| entry.path.clone())
156            .collect()
157    }
158
159    /// Candidates the cache holds, counting a soname once per distinct path.
160    pub fn entry_count(&self) -> usize {
161        self.len
162    }
163
164    pub fn is_empty(&self) -> bool {
165        self.entries.is_empty()
166    }
167}
168
169fn align8(value: usize) -> usize {
170    value.saturating_add(7) & !7
171}
172
173/// `offset` comes out of the file by way of [`align8`], so the addition is
174/// checked rather than assumed to fit.
175fn starts_with(bytes: &[u8], offset: usize, magic: &[u8]) -> bool {
176    let Some(end) = offset.checked_add(magic.len()) else {
177        return false;
178    };
179    bytes.get(offset..end) == Some(magic)
180}
181
182fn read_u32(bytes: &[u8], offset: usize) -> Option<u32> {
183    let slice = bytes.get(offset..offset.checked_add(4)?)?;
184    Some(u32::from_le_bytes(slice.try_into().ok()?))
185}
186
187/// Strings are NUL terminated and addressed relative to `base`.
188fn read_string(bytes: &[u8], base: usize, offset: u32) -> Option<String> {
189    let start = base.checked_add(offset as usize)?;
190    let rest = bytes.get(start..)?;
191    let end = rest
192        .iter()
193        .take(CACHE_STRING_LEN_MAX + 1)
194        .position(|&b| b == 0)?;
195    if end > CACHE_STRING_LEN_MAX {
196        return None;
197    }
198    std::str::from_utf8(&rest[..end]).ok().map(str::to_string)
199}
200
201/// How many entries `bytes` can actually hold from `base` on.
202///
203/// `nlibs` comes straight out of the file, so it is never trusted for sizing;
204/// the size of the image is the only bound worth allocating against.
205fn entry_capacity(bytes: &[u8], base: usize, header: usize, entry: usize) -> usize {
206    bytes
207        .len()
208        .saturating_sub(base.saturating_add(header))
209        .saturating_div(entry)
210}
211
212/// `struct cache_file`: a header followed by `nlibs` fixed-size entries whose
213/// string offsets are relative to the start of the image.
214fn parse_old(bytes: &[u8], nlibs: usize) -> Vec<CacheRecord> {
215    let capacity = entry_capacity(bytes, 0, OLD_HEADER_LEN, OLD_ENTRY_LEN);
216    let count = nlibs.min(capacity).min(CACHE_ENTRIES_MAX);
217    let mut out = Vec::with_capacity(count);
218    for index in 0..count {
219        let Some(offset) = index
220            .checked_mul(OLD_ENTRY_LEN)
221            .and_then(|at| at.checked_add(OLD_HEADER_LEN))
222        else {
223            break;
224        };
225        let (Some(flags), Some(key), Some(value)) = (
226            read_u32(bytes, offset),
227            read_u32(bytes, offset + 4),
228            read_u32(bytes, offset + 8),
229        ) else {
230            break;
231        };
232        if let (Some(soname), Some(path)) =
233            (read_string(bytes, 0, key), read_string(bytes, 0, value))
234        {
235            out.push(CacheRecord {
236                soname,
237                path: PathBuf::from(path),
238                flags,
239                osversion: 0,
240                hwcap: 0,
241            });
242        }
243    }
244    out
245}
246
247fn parse_new(bytes: &[u8], base: usize) -> Vec<CacheRecord> {
248    if !starts_with(bytes, base + NEW_MAGIC.len(), NEW_VERSION) {
249        return Vec::new();
250    }
251    // The planner supports only little-endian target cache records. Refuse a
252    // different byte order rather than decoding fields incorrectly.
253    let Some(header_flags) = bytes.get(base + 28).copied() else {
254        return Vec::new();
255    };
256    // Old writers left this field zero. Otherwise, the low two bits encode
257    // byte order and must name little-endian for supported targets.
258    if header_flags != 0 && header_flags & 0x03 != FLAGS_ENDIAN_LITTLE {
259        return Vec::new();
260    }
261    let nlibs = match read_u32(bytes, base + 20) {
262        Some(n) => n as usize,
263        None => return Vec::new(),
264    };
265    let capacity = entry_capacity(bytes, base, NEW_HEADER_LEN, NEW_ENTRY_LEN);
266    let count = nlibs.min(capacity).min(CACHE_ENTRIES_MAX);
267    let mut out = Vec::with_capacity(count);
268    for index in 0..count {
269        let Some(offset) = base
270            .checked_add(NEW_HEADER_LEN)
271            .and_then(|start| index.checked_mul(NEW_ENTRY_LEN)?.checked_add(start))
272        else {
273            break;
274        };
275        let (Some(flags), Some(key), Some(value), Some(osversion)) = (
276            read_u32(bytes, offset),
277            read_u32(bytes, offset + 4),
278            read_u32(bytes, offset + 8),
279            read_u32(bytes, offset + 12),
280        ) else {
281            break;
282        };
283        let hwcap = bytes
284            .get(offset + 16..offset + NEW_ENTRY_LEN)
285            .and_then(|value| value.try_into().ok())
286            .map(u64::from_le_bytes);
287        if let (Some(soname), Some(path), Some(hwcap)) = (
288            read_string(bytes, base, key),
289            read_string(bytes, base, value),
290            hwcap,
291        ) {
292            out.push(CacheRecord {
293                soname,
294                path: PathBuf::from(path),
295                flags,
296                osversion,
297                hwcap,
298            });
299        }
300    }
301    out
302}
303
304/// One `soname -> path` mapping, as the loader inside the bundle will see it.
305#[derive(Debug, Clone, PartialEq, Eq)]
306pub struct CacheEntry {
307    pub soname: String,
308    /// Absolute path *inside the generated rootfs*.
309    pub path: PathBuf,
310}
311
312/// glibc's `_DL_CACHE_DEFAULT_ID` for the target.
313///
314/// `_dl_cache_check_flags` compares the entry flags against this value exactly,
315/// so an entry carrying anything else is silently ignored by the loader.
316fn entry_flags(architecture: &Architecture) -> Option<i32> {
317    const FLAG_ELF_LIBC6: i32 = 0x0003;
318    const FLAG_X8664_LIB64: i32 = 0x0300;
319    const FLAG_AARCH64_LIB64: i32 = 0x0a00;
320    match (architecture.machine, architecture.class) {
321        (Machine::X86_64, ElfClass::Elf64) => Some(FLAG_X8664_LIB64 | FLAG_ELF_LIBC6),
322        (Machine::Aarch64, ElfClass::Elf64) => Some(FLAG_AARCH64_LIB64 | FLAG_ELF_LIBC6),
323        _ => None,
324    }
325}
326
327/// `cache_file_new_flags_endian_little`. The header records the byte order the
328/// entries were written in, and glibc refuses a cache that disagrees with the
329/// architecture it is running on.
330const FLAGS_ENDIAN_LITTLE: u8 = 2;
331
332/// The string table of a cache image, and the `(soname, path)` offset pair each
333/// entry record points at.
334struct StringTable {
335    bytes: Vec<u8>,
336    offsets: Vec<(u32, u32)>,
337}
338
339/// Encode the strings of `entries`. Offsets are absolute within the image,
340/// hence `base`.
341///
342/// `None` for anything that cannot be encoded faithfully: a path that is not
343/// UTF-8, a NUL that would truncate a string, or an image too large to address
344/// with the `u32` offsets the format uses.
345fn encode_strings(entries: &[&CacheEntry], base: usize) -> Option<StringTable> {
346    let mut strings: Vec<u8> = Vec::new();
347    let mut offsets: Vec<(u32, u32)> = Vec::with_capacity(entries.len());
348
349    for entry in entries {
350        // A NUL in either string would truncate it; such a name cannot come
351        // from an ELF string table, but the file names could in principle.
352        let path = entry.path.to_str()?;
353        if entry.soname.contains('\0') || path.contains('\0') {
354            return None;
355        }
356        let key = u32::try_from(base + strings.len()).ok()?;
357        strings.extend_from_slice(entry.soname.as_bytes());
358        strings.push(0);
359        let value = u32::try_from(base + strings.len()).ok()?;
360        strings.extend_from_slice(path.as_bytes());
361        strings.push(0);
362        offsets.push((key, value));
363    }
364    Some(StringTable {
365        bytes: strings,
366        offsets,
367    })
368}
369
370/// Build a `glibc-ld.so.cache1.1` image for `entries`.
371///
372/// Returns `None` for a target this function cannot encode faithfully, so the
373/// caller can fall back to reporting the problem instead of writing a cache the
374/// loader would reject.
375pub fn build(architecture: &Architecture, entries: &[CacheEntry]) -> Option<Vec<u8>> {
376    if entries.iter().any(|entry| !entry.path.is_absolute()) {
377        return None;
378    }
379
380    let flags = entry_flags(architecture)?;
381    if architecture.endianness != Endianness::Little {
382        // The header records one byte order and glibc refuses a cache that
383        // disagrees with the architecture reading it.
384        return None;
385    }
386
387    let mut entries: Vec<&CacheEntry> = entries.iter().collect();
388    // glibc looks entries up with a binary search that walks *down* the table,
389    // so it has to be sorted in descending `_dl_cache_libcmp` order. Ascending
390    // order parses fine and then fails to resolve. The path breaks ties, purely
391    // so that the same plan always produces the same bytes.
392    entries.sort_by(|a, b| libcmp(&b.soname, &a.soname).then_with(|| a.path.cmp(&b.path)));
393    entries.dedup_by(|a, b| a.soname == b.soname && a.path == b.path);
394    assert!(
395        entries
396            .windows(2)
397            .all(|pair| libcmp(&pair[0].soname, &pair[1].soname) != Ordering::Less),
398        "the loader binary-searches downwards and needs descending order"
399    );
400
401    let base = NEW_HEADER_LEN + entries.len() * NEW_ENTRY_LEN;
402    let StringTable {
403        bytes: strings,
404        offsets,
405    } = encode_strings(&entries, base)?;
406    let mut out = Vec::with_capacity(base + strings.len());
407    out.extend_from_slice(NEW_MAGIC);
408    out.extend_from_slice(NEW_VERSION);
409    out.extend_from_slice(&u32::try_from(entries.len()).ok()?.to_le_bytes());
410    out.extend_from_slice(&u32::try_from(strings.len()).ok()?.to_le_bytes());
411    out.push(FLAGS_ENDIAN_LITTLE);
412    out.extend_from_slice(&[0, 0, 0]); // `padding_unsed` (sic), reserved
413    out.extend_from_slice(&0u32.to_le_bytes()); // extension_offset: none
414    out.extend_from_slice(&[0u8; 12]); // `unused`, reserved
415    assert_eq!(out.len(), NEW_HEADER_LEN);
416
417    for (key, value) in offsets {
418        out.extend_from_slice(&flags.to_le_bytes());
419        out.extend_from_slice(&key.to_le_bytes());
420        out.extend_from_slice(&value.to_le_bytes());
421        out.extend_from_slice(&0u32.to_le_bytes()); // osversion, unused
422        out.extend_from_slice(&0u64.to_le_bytes()); // hwcap: none required
423    }
424    assert_eq!(out.len(), base);
425    out.extend_from_slice(&strings);
426    assert_eq!(out.len(), base + strings.len());
427
428    // Read the image back with the same reader the loader's format is modelled
429    // on. A cache the bundle cannot use would be worse than none.
430    let written = LdCache::parse(&out);
431    assert_eq!(written.entry_count(), entries.len());
432    assert!(
433        entries
434            .iter()
435            .all(|entry| written.lookup(&entry.soname).contains(&entry.path))
436    );
437    Some(out)
438}
439
440/// glibc's `_dl_cache_libcmp`: like `strcmp`, except that runs of digits compare
441/// numerically, so `libfoo.so.9` sorts before `libfoo.so.10`.
442///
443/// Bytes are compared unsigned. glibc compares them as `char`, whose signedness
444/// is architecture-dependent, so the two can only disagree about non-ASCII
445/// sonames.
446fn libcmp(left: &str, right: &str) -> Ordering {
447    let (p1, p2) = (left.as_bytes(), right.as_bytes());
448    let (mut i, mut j) = (0usize, 0usize);
449    let at = |s: &[u8], k: usize| s.get(k).copied().unwrap_or(0);
450
451    while at(p1, i) != 0 {
452        let (c1, c2) = (at(p1, i), at(p2, j));
453        if c1.is_ascii_digit() {
454            if !c2.is_ascii_digit() {
455                return Ordering::Greater;
456            }
457            let mut v1 = 0u64;
458            while at(p1, i).is_ascii_digit() {
459                v1 = v1
460                    .saturating_mul(10)
461                    .saturating_add(u64::from(at(p1, i) - b'0'));
462                i += 1;
463            }
464            let mut v2 = 0u64;
465            while at(p2, j).is_ascii_digit() {
466                v2 = v2
467                    .saturating_mul(10)
468                    .saturating_add(u64::from(at(p2, j) - b'0'));
469                j += 1;
470            }
471            if v1 != v2 {
472                return v1.cmp(&v2);
473            }
474        } else if c2.is_ascii_digit() {
475            return Ordering::Less;
476        } else if c1 != c2 {
477            return c1.cmp(&c2);
478        } else {
479            i += 1;
480            j += 1;
481        }
482    }
483    at(p1, i).cmp(&at(p2, j))
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489
490    fn offset(value: usize) -> u32 {
491        u32::try_from(value).expect("fixture offsets fit in u32")
492    }
493
494    /// Build a `glibc-ld.so.cache1.1` image with the given (soname, path) pairs.
495    fn new_format(entries: &[(&str, &str)]) -> Vec<u8> {
496        let mut strings = Vec::new();
497        let mut offsets = Vec::new();
498        for (soname, path) in entries {
499            let key = offset(strings.len());
500            strings.extend_from_slice(soname.as_bytes());
501            strings.push(0);
502            let value = offset(strings.len());
503            strings.extend_from_slice(path.as_bytes());
504            strings.push(0);
505            offsets.push((key, value));
506        }
507        let header_len = NEW_HEADER_LEN + entries.len() * NEW_ENTRY_LEN;
508
509        let mut out = Vec::new();
510        out.extend_from_slice(NEW_MAGIC);
511        out.extend_from_slice(NEW_VERSION);
512        out.extend_from_slice(&offset(entries.len()).to_le_bytes());
513        out.extend_from_slice(&offset(strings.len()).to_le_bytes());
514        out.push(FLAGS_ENDIAN_LITTLE);
515        out.extend_from_slice(&[0, 0, 0]);
516        out.extend_from_slice(&0u32.to_le_bytes());
517        out.extend_from_slice(&[0u8; 12]);
518        assert_eq!(out.len(), NEW_HEADER_LEN);
519
520        for (key, value) in &offsets {
521            out.extend_from_slice(&0x0300_0003u32.to_le_bytes());
522            out.extend_from_slice(&(key + offset(header_len)).to_le_bytes());
523            out.extend_from_slice(&(value + offset(header_len)).to_le_bytes());
524            out.extend_from_slice(&0u32.to_le_bytes());
525            out.extend_from_slice(&0u64.to_le_bytes());
526        }
527        out.extend_from_slice(&strings);
528        out
529    }
530
531    #[test]
532    fn parses_the_new_format() {
533        let bytes = new_format(&[
534            ("libc.so.6", "/lib/x86_64-linux-gnu/libc.so.6"),
535            ("libm.so.6", "/lib/x86_64-linux-gnu/libm.so.6"),
536        ]);
537        let cache = LdCache::parse(&bytes);
538        assert_eq!(cache.entry_count(), 2);
539        assert_eq!(
540            cache.lookup("libc.so.6"),
541            [PathBuf::from("/lib/x86_64-linux-gnu/libc.so.6")]
542        );
543        assert!(cache.lookup("libnope.so.1").is_empty());
544    }
545
546    #[test]
547    fn parses_an_old_format_header_with_an_appended_new_cache() {
548        let new = new_format(&[("libz.so.1", "/usr/lib/libz.so.1")]);
549        let nlibs = 1usize;
550        let mut bytes = Vec::new();
551        bytes.extend_from_slice(OLD_MAGIC);
552        bytes.push(0); // padding to the u32 boundary
553        bytes.extend_from_slice(&offset(nlibs).to_le_bytes());
554        // One old entry pointing at strings we never read.
555        bytes.extend_from_slice(&[0u8; OLD_ENTRY_LEN]);
556        while bytes.len() < align8(OLD_HEADER_LEN + nlibs * OLD_ENTRY_LEN) {
557            bytes.push(0);
558        }
559        bytes.extend_from_slice(&new);
560
561        let cache = LdCache::parse(&bytes);
562        assert_eq!(
563            cache.lookup("libz.so.1"),
564            [PathBuf::from("/usr/lib/libz.so.1")]
565        );
566    }
567
568    fn x86_64() -> Architecture {
569        Architecture {
570            machine: Machine::X86_64,
571            class: ElfClass::Elf64,
572            endianness: Endianness::Little,
573        }
574    }
575
576    fn entry(soname: &str, path: &str) -> CacheEntry {
577        CacheEntry {
578            soname: soname.to_string(),
579            path: PathBuf::from(path),
580        }
581    }
582
583    /// Read a generated image back the way glibc does: header fields at their
584    /// fixed offsets, entries in file order.
585    fn header(bytes: &[u8]) -> (u32, u32, u8, u32) {
586        (
587            read_u32(bytes, 20).unwrap(),
588            read_u32(bytes, 24).unwrap(),
589            bytes[28],
590            read_u32(bytes, 32).unwrap(),
591        )
592    }
593
594    fn keys_in_file_order(bytes: &[u8]) -> Vec<String> {
595        let nlibs = read_u32(bytes, 20).unwrap() as usize;
596        (0..nlibs)
597            .map(|index| {
598                let offset = NEW_HEADER_LEN + index * NEW_ENTRY_LEN;
599                read_string(bytes, 0, read_u32(bytes, offset + 4).unwrap()).unwrap()
600            })
601            .collect()
602    }
603
604    #[test]
605    fn a_generated_cache_round_trips_through_the_parser() {
606        let bytes = build(
607            &x86_64(),
608            &[
609                entry("libcached.so.1", "/opt/cached/libcached.so.1"),
610                entry("libc.so.6", "/usr/lib/x86_64-linux-gnu/libc.so.6"),
611            ],
612        )
613        .unwrap();
614
615        let (nlibs, len_strings, flags, extension_offset) = header(&bytes);
616        assert_eq!(nlibs, 2);
617        assert_eq!(flags, FLAGS_ENDIAN_LITTLE, "glibc checks the byte order");
618        assert_eq!(extension_offset, 0, "no extension section is written");
619        assert_eq!(
620            bytes.len(),
621            NEW_HEADER_LEN + 2 * NEW_ENTRY_LEN + len_strings as usize
622        );
623
624        let cache = LdCache::parse(&bytes);
625        assert_eq!(cache.entry_count(), 2);
626        assert_eq!(
627            cache.lookup("libcached.so.1"),
628            [PathBuf::from("/opt/cached/libcached.so.1")]
629        );
630        assert!(cache.lookup("libnope.so.1").is_empty());
631    }
632
633    /// The loader binary-searches downwards, so the table has to descend.
634    #[test]
635    fn entries_are_written_in_descending_libcmp_order() {
636        let bytes = build(
637            &x86_64(),
638            &[
639                entry("libaaa.so.1", "/a"),
640                entry("libzzz.so.1", "/z"),
641                entry("libmmm.so.9", "/m9"),
642                entry("libmmm.so.10", "/m10"),
643            ],
644        )
645        .unwrap();
646        assert_eq!(
647            keys_in_file_order(&bytes),
648            [
649                "libzzz.so.1",
650                // 10 is numerically greater than 9, which plain strcmp gets wrong.
651                "libmmm.so.10",
652                "libmmm.so.9",
653                "libaaa.so.1",
654            ]
655        );
656    }
657
658    #[test]
659    fn digit_runs_compare_numerically() {
660        assert_eq!(libcmp("libfoo.so.9", "libfoo.so.10"), Ordering::Less);
661        assert_eq!(libcmp("libfoo.so.10", "libfoo.so.9"), Ordering::Greater);
662        assert_eq!(libcmp("libfoo.so.1", "libfoo.so.1"), Ordering::Equal);
663        // A prefix is smaller than what extends it.
664        assert_eq!(libcmp("libfoo.so", "libfoo.so.1"), Ordering::Less);
665        // Digits sort after anything that is not a digit, as in glibc.
666        assert_eq!(libcmp("lib1", "liba"), Ordering::Greater);
667        assert_eq!(libcmp("liba", "lib1"), Ordering::Less);
668        // Absurd digit runs saturate rather than overflow.
669        let huge = format!("lib{}", "9".repeat(40));
670        assert_eq!(libcmp(&huge, &huge), Ordering::Equal);
671    }
672
673    #[test]
674    fn identical_entries_collapse_and_output_is_stable() {
675        let entries = [
676            entry("libc.so.6", "/lib/libc.so.6"),
677            entry("libc.so.6", "/lib/libc.so.6"),
678            entry("libc.so.6", "/other/libc.so.6"),
679        ];
680        let bytes = build(&x86_64(), &entries).unwrap();
681        assert_eq!(read_u32(&bytes, 20).unwrap(), 2, "duplicates collapse");
682
683        let mut shuffled = entries.to_vec();
684        shuffled.reverse();
685        assert_eq!(
686            build(&x86_64(), &shuffled).unwrap(),
687            bytes,
688            "input order must not change the image"
689        );
690    }
691
692    #[test]
693    fn an_architecture_without_a_known_cache_id_is_refused() {
694        let unsupported = Architecture {
695            machine: Machine::RiscV64,
696            ..x86_64()
697        };
698        assert!(build(&unsupported, &[entry("libc.so.6", "/lib/libc.so.6")]).is_none());
699        let big_endian = Architecture {
700            endianness: Endianness::Big,
701            ..x86_64()
702        };
703        assert!(build(&big_endian, &[entry("libc.so.6", "/lib/libc.so.6")]).is_none());
704        // An empty cache is still a valid cache.
705        assert!(build(&x86_64(), &[]).is_some());
706    }
707
708    #[test]
709    fn dropped_entries_are_not_counted() {
710        // A relative path is not something ldconfig writes, and resolving one
711        // would depend on this process's working directory, so it is dropped.
712        let bytes = new_format(&[
713            ("libc.so.6", "/lib/libc.so.6"),
714            ("librel.so.1", "relative/librel.so.1"),
715            ("libc.so.6", "/lib/libc.so.6"),
716        ]);
717        let cache = LdCache::parse(&bytes);
718        assert!(cache.lookup("librel.so.1").is_empty());
719        assert_eq!(
720            cache.entry_count(),
721            1,
722            "only what the cache kept is counted"
723        );
724    }
725
726    #[test]
727    fn candidates_per_soname_are_bounded_in_cache_order() {
728        let owned: Vec<(String, String)> = (0..=CACHE_CANDIDATES_PER_SONAME_MAX)
729            .map(|index| ("libmany.so".to_string(), format!("/lib/libmany-{index}.so")))
730            .collect();
731        let entries: Vec<(&str, &str)> = owned
732            .iter()
733            .map(|(soname, path)| (soname.as_str(), path.as_str()))
734            .collect();
735
736        let cache = LdCache::parse(&new_format(&entries));
737        let found = cache.lookup("libmany.so");
738        assert_eq!(found.len(), CACHE_CANDIDATES_PER_SONAME_MAX);
739        assert_eq!(found[0], Path::new("/lib/libmany-0.so"));
740        assert_eq!(
741            found.last(),
742            Some(&PathBuf::from(format!(
743                "/lib/libmany-{}.so",
744                CACHE_CANDIDATES_PER_SONAME_MAX - 1
745            )))
746        );
747    }
748
749    #[test]
750    fn compatible_lookup_rejects_foreign_abi_and_cpu_specific_entries() {
751        let mut bytes = new_format(&[("libpick.so", "/lib/libpick.so")]);
752        // First entry begins straight after the new-format header.
753        bytes[NEW_HEADER_LEN..NEW_HEADER_LEN + 4].copy_from_slice(&0x0000_0a03u32.to_le_bytes()); // aarch64 libc6
754        let cache = LdCache::parse(&bytes);
755        let x86 = Architecture {
756            machine: Machine::X86_64,
757            class: ElfClass::Elf64,
758            endianness: Endianness::Little,
759        };
760        assert!(cache.lookup_compatible("libpick.so", &x86).is_empty());
761
762        bytes[NEW_HEADER_LEN..NEW_HEADER_LEN + 4].copy_from_slice(&0x0000_0303u32.to_le_bytes());
763        bytes[NEW_HEADER_LEN + 16..NEW_HEADER_LEN + 24].copy_from_slice(&1u64.to_le_bytes());
764        let cache = LdCache::parse(&bytes);
765        assert!(cache.lookup_compatible("libpick.so", &x86).is_empty());
766    }
767
768    #[test]
769    fn garbage_degrades_to_an_empty_cache() {
770        assert!(LdCache::parse(b"not a cache at all").is_empty());
771        assert!(LdCache::parse(&[]).is_empty());
772    }
773
774    #[test]
775    fn an_absurd_entry_count_allocates_nothing() {
776        assert_eq!(
777            entry_capacity(&[0u8; 48], 0, NEW_HEADER_LEN, NEW_ENTRY_LEN),
778            0
779        );
780        assert_eq!(
781            entry_capacity(&[0u8; 48 + 24], 0, NEW_HEADER_LEN, NEW_ENTRY_LEN),
782            1
783        );
784        assert_eq!(entry_capacity(&[], 4096, NEW_HEADER_LEN, NEW_ENTRY_LEN), 0);
785
786        let mut bytes = new_format(&[("libc.so.6", "/lib/libc.so.6")]);
787        bytes[20..24].copy_from_slice(&u32::MAX.to_le_bytes());
788        let cache = LdCache::parse(&bytes);
789        assert_eq!(cache.lookup("libc.so.6"), [PathBuf::from("/lib/libc.so.6")]);
790        assert_eq!(
791            cache.entry_count(),
792            1,
793            "parsing stops at the end of the file"
794        );
795    }
796
797    #[test]
798    fn truncated_entries_do_not_panic() {
799        let mut bytes = new_format(&[("libc.so.6", "/lib/libc.so.6")]);
800        bytes.truncate(NEW_HEADER_LEN + 4);
801        assert!(LdCache::parse(&bytes).is_empty());
802    }
803}