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, then
213/// the string table.
214///
215/// Unlike the new format, whose offsets are relative to the start of the image,
216/// these are relative to the string table itself — glibc reads them through
217/// `cache_data = (const char *) &cache->libs[cache->nlibs]`. The declared
218/// `nlibs` is what fixes that base, so it is used even where the entry loop
219/// reads fewer entries than the header claims.
220fn parse_old(bytes: &[u8], nlibs: usize) -> Vec<CacheRecord> {
221    let capacity = entry_capacity(bytes, 0, OLD_HEADER_LEN, OLD_ENTRY_LEN);
222    let count = nlibs.min(capacity).min(CACHE_ENTRIES_MAX);
223    let Some(strings) = nlibs
224        .checked_mul(OLD_ENTRY_LEN)
225        .and_then(|entries| entries.checked_add(OLD_HEADER_LEN))
226        .filter(|&base| base <= bytes.len())
227    else {
228        return Vec::new();
229    };
230    let mut out = Vec::with_capacity(count);
231    for index in 0..count {
232        let Some(offset) = index
233            .checked_mul(OLD_ENTRY_LEN)
234            .and_then(|at| at.checked_add(OLD_HEADER_LEN))
235        else {
236            break;
237        };
238        let (Some(flags), Some(key), Some(value)) = (
239            read_u32(bytes, offset),
240            read_u32(bytes, offset + 4),
241            read_u32(bytes, offset + 8),
242        ) else {
243            break;
244        };
245        if let (Some(soname), Some(path)) = (
246            read_string(bytes, strings, key),
247            read_string(bytes, strings, value),
248        ) {
249            out.push(CacheRecord {
250                soname,
251                path: PathBuf::from(path),
252                flags,
253                osversion: 0,
254                hwcap: 0,
255            });
256        }
257    }
258    out
259}
260
261fn parse_new(bytes: &[u8], base: usize) -> Vec<CacheRecord> {
262    if !starts_with(bytes, base + NEW_MAGIC.len(), NEW_VERSION) {
263        return Vec::new();
264    }
265    // The planner supports only little-endian target cache records. Refuse a
266    // different byte order rather than decoding fields incorrectly.
267    let Some(header_flags) = bytes.get(base + 28).copied() else {
268        return Vec::new();
269    };
270    // Old writers left this field zero. Otherwise, the low two bits encode
271    // byte order and must name little-endian for supported targets.
272    if header_flags != 0 && header_flags & 0x03 != FLAGS_ENDIAN_LITTLE {
273        return Vec::new();
274    }
275    let nlibs = match read_u32(bytes, base + 20) {
276        Some(n) => n as usize,
277        None => return Vec::new(),
278    };
279    let capacity = entry_capacity(bytes, base, NEW_HEADER_LEN, NEW_ENTRY_LEN);
280    let count = nlibs.min(capacity).min(CACHE_ENTRIES_MAX);
281    let mut out = Vec::with_capacity(count);
282    for index in 0..count {
283        let Some(offset) = base
284            .checked_add(NEW_HEADER_LEN)
285            .and_then(|start| index.checked_mul(NEW_ENTRY_LEN)?.checked_add(start))
286        else {
287            break;
288        };
289        let (Some(flags), Some(key), Some(value), Some(osversion)) = (
290            read_u32(bytes, offset),
291            read_u32(bytes, offset + 4),
292            read_u32(bytes, offset + 8),
293            read_u32(bytes, offset + 12),
294        ) else {
295            break;
296        };
297        let hwcap = bytes
298            .get(offset + 16..offset + NEW_ENTRY_LEN)
299            .and_then(|value| value.try_into().ok())
300            .map(u64::from_le_bytes);
301        if let (Some(soname), Some(path), Some(hwcap)) = (
302            read_string(bytes, base, key),
303            read_string(bytes, base, value),
304            hwcap,
305        ) {
306            out.push(CacheRecord {
307                soname,
308                path: PathBuf::from(path),
309                flags,
310                osversion,
311                hwcap,
312            });
313        }
314    }
315    out
316}
317
318/// One `soname -> path` mapping, as the loader inside the bundle will see it.
319#[derive(Debug, Clone, PartialEq, Eq)]
320pub struct CacheEntry {
321    pub soname: String,
322    /// Absolute path *inside the generated rootfs*.
323    pub path: PathBuf,
324}
325
326/// glibc's `_DL_CACHE_DEFAULT_ID` for the target.
327///
328/// `_dl_cache_check_flags` compares the entry flags against this value exactly,
329/// so an entry carrying anything else is silently ignored by the loader.
330fn entry_flags(architecture: &Architecture) -> Option<i32> {
331    const FLAG_ELF_LIBC6: i32 = 0x0003;
332    const FLAG_X8664_LIB64: i32 = 0x0300;
333    const FLAG_AARCH64_LIB64: i32 = 0x0a00;
334    match (architecture.machine, architecture.class) {
335        (Machine::X86_64, ElfClass::Elf64) => Some(FLAG_X8664_LIB64 | FLAG_ELF_LIBC6),
336        (Machine::Aarch64, ElfClass::Elf64) => Some(FLAG_AARCH64_LIB64 | FLAG_ELF_LIBC6),
337        _ => None,
338    }
339}
340
341/// `cache_file_new_flags_endian_little`. The header records the byte order the
342/// entries were written in, and glibc refuses a cache that disagrees with the
343/// architecture it is running on.
344const FLAGS_ENDIAN_LITTLE: u8 = 2;
345
346/// The string table of a cache image, and the `(soname, path)` offset pair each
347/// entry record points at.
348struct StringTable {
349    bytes: Vec<u8>,
350    offsets: Vec<(u32, u32)>,
351}
352
353/// Encode the strings of `entries`. Offsets are absolute within the image,
354/// hence `base`.
355///
356/// `None` for anything that cannot be encoded faithfully: a path that is not
357/// UTF-8, a NUL that would truncate a string, or an image too large to address
358/// with the `u32` offsets the format uses.
359fn encode_strings(entries: &[&CacheEntry], base: usize) -> Option<StringTable> {
360    let mut strings: Vec<u8> = Vec::new();
361    let mut offsets: Vec<(u32, u32)> = Vec::with_capacity(entries.len());
362
363    for entry in entries {
364        // A NUL in either string would truncate it; such a name cannot come
365        // from an ELF string table, but the file names could in principle.
366        let path = entry.path.to_str()?;
367        if entry.soname.contains('\0') || path.contains('\0') {
368            return None;
369        }
370        let key = u32::try_from(base + strings.len()).ok()?;
371        strings.extend_from_slice(entry.soname.as_bytes());
372        strings.push(0);
373        let value = u32::try_from(base + strings.len()).ok()?;
374        strings.extend_from_slice(path.as_bytes());
375        strings.push(0);
376        offsets.push((key, value));
377    }
378    Some(StringTable {
379        bytes: strings,
380        offsets,
381    })
382}
383
384/// Whether both of an entry's strings survive a round trip through the reader,
385/// which bounds every string it accepts.
386fn is_encodable(entry: &CacheEntry) -> bool {
387    entry.soname.len() <= CACHE_STRING_LEN_MAX
388        && entry
389            .path
390            .to_str()
391            .is_some_and(|path| path.len() <= CACHE_STRING_LEN_MAX)
392}
393
394/// Whether any soname has more alternatives than the reader keeps.
395///
396/// Counted by exact name over the whole set, which is how the reader groups
397/// them. Adjacency in the written order would not do: entries are sorted by
398/// [`libcmp`], which compares digit runs numerically, so `libx.so.1` and
399/// `libx.so.01` are `Equal` there and a run of one name can be split by the
400/// other while the reader still sees them as one oversized group.
401fn candidates_per_soname_exceeded(entries: &[&CacheEntry]) -> bool {
402    let mut counts: HashMap<&str, usize> = HashMap::new();
403    for entry in entries {
404        let count = counts.entry(entry.soname.as_str()).or_insert(0);
405        *count += 1;
406        if *count > CACHE_CANDIDATES_PER_SONAME_MAX {
407            return true;
408        }
409    }
410    false
411}
412
413/// Build a `glibc-ld.so.cache1.1` image for `entries`.
414///
415/// Returns `None` for a target this function cannot encode faithfully, so the
416/// caller can fall back to reporting the problem instead of writing a cache the
417/// loader would reject.
418pub fn build(architecture: &Architecture, entries: &[CacheEntry]) -> Option<Vec<u8>> {
419    if entries.iter().any(|entry| !entry.path.is_absolute()) {
420        return None;
421    }
422    // Sonames and paths come out of ELF files and the source filesystem, so
423    // they can be longer than the reader — and therefore the loader's own
424    // lookup — will accept. An image whose entries would be dropped on the way
425    // back in is not a cache this bundle can use.
426    if !entries.iter().all(is_encodable) {
427        return None;
428    }
429
430    let flags = entry_flags(architecture)?;
431    if architecture.endianness != Endianness::Little {
432        // The header records one byte order and glibc refuses a cache that
433        // disagrees with the architecture reading it.
434        return None;
435    }
436
437    let mut entries: Vec<&CacheEntry> = entries.iter().collect();
438    // glibc looks entries up with a binary search that walks *down* the table,
439    // so it has to be sorted in descending `_dl_cache_libcmp` order. Ascending
440    // order parses fine and then fails to resolve. The path breaks ties, purely
441    // so that the same plan always produces the same bytes.
442    entries.sort_by(|a, b| libcmp(&b.soname, &a.soname).then_with(|| a.path.cmp(&b.path)));
443    entries.dedup_by(|a, b| a.soname == b.soname && a.path == b.path);
444    assert!(
445        entries
446            .windows(2)
447            .all(|pair| libcmp(&pair[0].soname, &pair[1].soname) != Ordering::Less),
448        "the loader binary-searches downwards and needs descending order"
449    );
450
451    if entries.len() > CACHE_ENTRIES_MAX || candidates_per_soname_exceeded(&entries) {
452        return None;
453    }
454
455    let base = NEW_HEADER_LEN + entries.len() * NEW_ENTRY_LEN;
456    let StringTable {
457        bytes: strings,
458        offsets,
459    } = encode_strings(&entries, base)?;
460    let mut out = Vec::with_capacity(base + strings.len());
461    out.extend_from_slice(NEW_MAGIC);
462    out.extend_from_slice(NEW_VERSION);
463    out.extend_from_slice(&u32::try_from(entries.len()).ok()?.to_le_bytes());
464    out.extend_from_slice(&u32::try_from(strings.len()).ok()?.to_le_bytes());
465    out.push(FLAGS_ENDIAN_LITTLE);
466    out.extend_from_slice(&[0, 0, 0]); // `padding_unsed` (sic), reserved
467    out.extend_from_slice(&0u32.to_le_bytes()); // extension_offset: none
468    out.extend_from_slice(&[0u8; 12]); // `unused`, reserved
469    assert_eq!(out.len(), NEW_HEADER_LEN);
470
471    for (key, value) in offsets {
472        out.extend_from_slice(&flags.to_le_bytes());
473        out.extend_from_slice(&key.to_le_bytes());
474        out.extend_from_slice(&value.to_le_bytes());
475        out.extend_from_slice(&0u32.to_le_bytes()); // osversion, unused
476        out.extend_from_slice(&0u64.to_le_bytes()); // hwcap: none required
477    }
478    assert_eq!(out.len(), base);
479    out.extend_from_slice(&strings);
480    assert_eq!(out.len(), base + strings.len());
481    if u64::try_from(out.len()).is_ok_and(|len| len > CACHE_BYTES_MAX) {
482        return None;
483    }
484
485    // Read the image back with the same reader the loader's format is modelled
486    // on. A cache the bundle cannot use would be worse than none.
487    let written = LdCache::parse(&out);
488    assert_eq!(written.entry_count(), entries.len());
489    assert!(
490        entries
491            .iter()
492            .all(|entry| written.lookup(&entry.soname).contains(&entry.path))
493    );
494    Some(out)
495}
496
497/// glibc's `_dl_cache_libcmp`: like `strcmp`, except that runs of digits compare
498/// numerically, so `libfoo.so.9` sorts before `libfoo.so.10`.
499///
500/// Bytes are compared unsigned. glibc compares them as `char`, whose signedness
501/// is architecture-dependent, so the two can only disagree about non-ASCII
502/// sonames.
503fn libcmp(left: &str, right: &str) -> Ordering {
504    let (p1, p2) = (left.as_bytes(), right.as_bytes());
505    let (mut i, mut j) = (0usize, 0usize);
506    let at = |s: &[u8], k: usize| s.get(k).copied().unwrap_or(0);
507
508    while at(p1, i) != 0 {
509        let (c1, c2) = (at(p1, i), at(p2, j));
510        if c1.is_ascii_digit() {
511            if !c2.is_ascii_digit() {
512                return Ordering::Greater;
513            }
514            let mut v1 = 0u64;
515            while at(p1, i).is_ascii_digit() {
516                v1 = v1
517                    .saturating_mul(10)
518                    .saturating_add(u64::from(at(p1, i) - b'0'));
519                i += 1;
520            }
521            let mut v2 = 0u64;
522            while at(p2, j).is_ascii_digit() {
523                v2 = v2
524                    .saturating_mul(10)
525                    .saturating_add(u64::from(at(p2, j) - b'0'));
526                j += 1;
527            }
528            if v1 != v2 {
529                return v1.cmp(&v2);
530            }
531        } else if c2.is_ascii_digit() {
532            return Ordering::Less;
533        } else if c1 != c2 {
534            return c1.cmp(&c2);
535        } else {
536            i += 1;
537            j += 1;
538        }
539    }
540    at(p1, i).cmp(&at(p2, j))
541}
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546
547    fn little_endian_x86_64() -> Architecture {
548        Architecture {
549            machine: Machine::X86_64,
550            class: ElfClass::Elf64,
551            endianness: Endianness::Little,
552        }
553    }
554
555    /// `libcmp` compares digit runs numerically, so two spellings of one
556    /// version sort as equals while the reader still groups them by exact
557    /// name. Counting adjacent runs would miss the group that overflows.
558    #[test]
559    fn candidate_counting_uses_the_readers_grouping_not_the_written_order() {
560        let architecture = little_endian_x86_64();
561        let mut entries: Vec<CacheEntry> = (0..=CACHE_CANDIDATES_PER_SONAME_MAX)
562            .map(|index| CacheEntry {
563                soname: "libx.so.1".to_string(),
564                path: PathBuf::from(format!("/usr/lib/{index:04}/libx.so.1")),
565            })
566            .collect();
567        // Sorts as an equal of `libx.so.1`, into the middle of the run above.
568        entries.push(CacheEntry {
569            soname: "libx.so.01".to_string(),
570            path: PathBuf::from("/usr/lib/0128a/libx.so.01"),
571        });
572        assert!(build(&architecture, &entries).is_none());
573    }
574
575    /// Sonames come out of ELF string tables, so `build` is handed input it
576    /// cannot always encode. It has to answer `None`, never abort.
577    #[test]
578    fn unencodable_entries_are_refused_rather_than_asserted() {
579        let architecture = little_endian_x86_64();
580
581        let long_soname = CacheEntry {
582            soname: "a".repeat(CACHE_STRING_LEN_MAX + 1),
583            path: PathBuf::from("/usr/lib/libx.so.1"),
584        };
585        assert!(build(&architecture, std::slice::from_ref(&long_soname)).is_none());
586
587        let long_path = CacheEntry {
588            soname: "libx.so.1".to_string(),
589            path: PathBuf::from(format!("/usr/lib/{}", "b".repeat(CACHE_STRING_LEN_MAX))),
590        };
591        assert!(build(&architecture, &[long_path]).is_none());
592
593        // More alternatives for one soname than the reader keeps.
594        let crowded: Vec<CacheEntry> = (0..=CACHE_CANDIDATES_PER_SONAME_MAX)
595            .map(|index| CacheEntry {
596                soname: "libx.so.1".to_string(),
597                path: PathBuf::from(format!("/usr/lib/{index}/libx.so.1")),
598            })
599            .collect();
600        assert!(build(&architecture, &crowded).is_none());
601
602        // One below the cap still builds, so the bound is the only thing
603        // separating the two answers.
604        assert!(build(&architecture, &crowded[..CACHE_CANDIDATES_PER_SONAME_MAX]).is_some());
605    }
606
607    fn offset(value: usize) -> u32 {
608        u32::try_from(value).expect("fixture offsets fit in u32")
609    }
610
611    /// Build a `glibc-ld.so.cache1.1` image with the given (soname, path) pairs.
612    fn new_format(entries: &[(&str, &str)]) -> Vec<u8> {
613        let mut strings = Vec::new();
614        let mut offsets = Vec::new();
615        for (soname, path) in entries {
616            let key = offset(strings.len());
617            strings.extend_from_slice(soname.as_bytes());
618            strings.push(0);
619            let value = offset(strings.len());
620            strings.extend_from_slice(path.as_bytes());
621            strings.push(0);
622            offsets.push((key, value));
623        }
624        let header_len = NEW_HEADER_LEN + entries.len() * NEW_ENTRY_LEN;
625
626        let mut out = Vec::new();
627        out.extend_from_slice(NEW_MAGIC);
628        out.extend_from_slice(NEW_VERSION);
629        out.extend_from_slice(&offset(entries.len()).to_le_bytes());
630        out.extend_from_slice(&offset(strings.len()).to_le_bytes());
631        out.push(FLAGS_ENDIAN_LITTLE);
632        out.extend_from_slice(&[0, 0, 0]);
633        out.extend_from_slice(&0u32.to_le_bytes());
634        out.extend_from_slice(&[0u8; 12]);
635        assert_eq!(out.len(), NEW_HEADER_LEN);
636
637        for (key, value) in &offsets {
638            out.extend_from_slice(&0x0300_0003u32.to_le_bytes());
639            out.extend_from_slice(&(key + offset(header_len)).to_le_bytes());
640            out.extend_from_slice(&(value + offset(header_len)).to_le_bytes());
641            out.extend_from_slice(&0u32.to_le_bytes());
642            out.extend_from_slice(&0u64.to_le_bytes());
643        }
644        out.extend_from_slice(&strings);
645        out
646    }
647
648    #[test]
649    fn parses_the_new_format() {
650        let bytes = new_format(&[
651            ("libc.so.6", "/lib/x86_64-linux-gnu/libc.so.6"),
652            ("libm.so.6", "/lib/x86_64-linux-gnu/libm.so.6"),
653        ]);
654        let cache = LdCache::parse(&bytes);
655        assert_eq!(cache.entry_count(), 2);
656        assert_eq!(
657            cache.lookup("libc.so.6"),
658            [PathBuf::from("/lib/x86_64-linux-gnu/libc.so.6")]
659        );
660        assert!(cache.lookup("libnope.so.1").is_empty());
661    }
662
663    /// glibc reads an old-format cache's strings through
664    /// `&cache->libs[cache->nlibs]`, so its offsets are relative to the string
665    /// table, not to the start of the image like the new format's.
666    #[test]
667    fn old_format_string_offsets_are_relative_to_the_string_table() {
668        let entries = [
669            ("libz.so.1", "/usr/lib/libz.so.1"),
670            ("libc.so.6", "/lib/libc.so.6"),
671        ];
672        let nlibs = entries.len();
673        let mut strings: Vec<u8> = Vec::new();
674        let mut offsets = Vec::new();
675        for (soname, path) in entries {
676            let key = offset(strings.len());
677            strings.extend_from_slice(soname.as_bytes());
678            strings.push(0);
679            let value = offset(strings.len());
680            strings.extend_from_slice(path.as_bytes());
681            strings.push(0);
682            offsets.push((key, value));
683        }
684
685        let mut bytes = Vec::new();
686        bytes.extend_from_slice(OLD_MAGIC);
687        bytes.push(0); // padding to the u32 boundary
688        bytes.extend_from_slice(&offset(nlibs).to_le_bytes());
689        for (key, value) in &offsets {
690            bytes.extend_from_slice(&0u32.to_le_bytes()); // flags
691            bytes.extend_from_slice(&key.to_le_bytes());
692            bytes.extend_from_slice(&value.to_le_bytes());
693        }
694        assert_eq!(bytes.len(), OLD_HEADER_LEN + nlibs * OLD_ENTRY_LEN);
695        bytes.extend_from_slice(&strings);
696
697        let cache = LdCache::parse(&bytes);
698        assert_eq!(
699            cache.lookup("libz.so.1"),
700            [PathBuf::from("/usr/lib/libz.so.1")]
701        );
702        assert_eq!(cache.lookup("libc.so.6"), [PathBuf::from("/lib/libc.so.6")]);
703    }
704
705    #[test]
706    fn parses_an_old_format_header_with_an_appended_new_cache() {
707        let new = new_format(&[("libz.so.1", "/usr/lib/libz.so.1")]);
708        let nlibs = 1usize;
709        let mut bytes = Vec::new();
710        bytes.extend_from_slice(OLD_MAGIC);
711        bytes.push(0); // padding to the u32 boundary
712        bytes.extend_from_slice(&offset(nlibs).to_le_bytes());
713        // One old entry pointing at strings we never read.
714        bytes.extend_from_slice(&[0u8; OLD_ENTRY_LEN]);
715        while bytes.len() < align8(OLD_HEADER_LEN + nlibs * OLD_ENTRY_LEN) {
716            bytes.push(0);
717        }
718        bytes.extend_from_slice(&new);
719
720        let cache = LdCache::parse(&bytes);
721        assert_eq!(
722            cache.lookup("libz.so.1"),
723            [PathBuf::from("/usr/lib/libz.so.1")]
724        );
725    }
726
727    fn x86_64() -> Architecture {
728        Architecture {
729            machine: Machine::X86_64,
730            class: ElfClass::Elf64,
731            endianness: Endianness::Little,
732        }
733    }
734
735    fn entry(soname: &str, path: &str) -> CacheEntry {
736        CacheEntry {
737            soname: soname.to_string(),
738            path: PathBuf::from(path),
739        }
740    }
741
742    /// Read a generated image back the way glibc does: header fields at their
743    /// fixed offsets, entries in file order.
744    fn header(bytes: &[u8]) -> (u32, u32, u8, u32) {
745        (
746            read_u32(bytes, 20).unwrap(),
747            read_u32(bytes, 24).unwrap(),
748            bytes[28],
749            read_u32(bytes, 32).unwrap(),
750        )
751    }
752
753    fn keys_in_file_order(bytes: &[u8]) -> Vec<String> {
754        let nlibs = read_u32(bytes, 20).unwrap() as usize;
755        (0..nlibs)
756            .map(|index| {
757                let offset = NEW_HEADER_LEN + index * NEW_ENTRY_LEN;
758                read_string(bytes, 0, read_u32(bytes, offset + 4).unwrap()).unwrap()
759            })
760            .collect()
761    }
762
763    #[test]
764    fn a_generated_cache_round_trips_through_the_parser() {
765        let bytes = build(
766            &x86_64(),
767            &[
768                entry("libcached.so.1", "/opt/cached/libcached.so.1"),
769                entry("libc.so.6", "/usr/lib/x86_64-linux-gnu/libc.so.6"),
770            ],
771        )
772        .unwrap();
773
774        let (nlibs, len_strings, flags, extension_offset) = header(&bytes);
775        assert_eq!(nlibs, 2);
776        assert_eq!(flags, FLAGS_ENDIAN_LITTLE, "glibc checks the byte order");
777        assert_eq!(extension_offset, 0, "no extension section is written");
778        assert_eq!(
779            bytes.len(),
780            NEW_HEADER_LEN + 2 * NEW_ENTRY_LEN + len_strings as usize
781        );
782
783        let cache = LdCache::parse(&bytes);
784        assert_eq!(cache.entry_count(), 2);
785        assert_eq!(
786            cache.lookup("libcached.so.1"),
787            [PathBuf::from("/opt/cached/libcached.so.1")]
788        );
789        assert!(cache.lookup("libnope.so.1").is_empty());
790    }
791
792    /// The loader binary-searches downwards, so the table has to descend.
793    #[test]
794    fn entries_are_written_in_descending_libcmp_order() {
795        let bytes = build(
796            &x86_64(),
797            &[
798                entry("libaaa.so.1", "/a"),
799                entry("libzzz.so.1", "/z"),
800                entry("libmmm.so.9", "/m9"),
801                entry("libmmm.so.10", "/m10"),
802            ],
803        )
804        .unwrap();
805        assert_eq!(
806            keys_in_file_order(&bytes),
807            [
808                "libzzz.so.1",
809                // 10 is numerically greater than 9, which plain strcmp gets wrong.
810                "libmmm.so.10",
811                "libmmm.so.9",
812                "libaaa.so.1",
813            ]
814        );
815    }
816
817    #[test]
818    fn digit_runs_compare_numerically() {
819        assert_eq!(libcmp("libfoo.so.9", "libfoo.so.10"), Ordering::Less);
820        assert_eq!(libcmp("libfoo.so.10", "libfoo.so.9"), Ordering::Greater);
821        assert_eq!(libcmp("libfoo.so.1", "libfoo.so.1"), Ordering::Equal);
822        // A prefix is smaller than what extends it.
823        assert_eq!(libcmp("libfoo.so", "libfoo.so.1"), Ordering::Less);
824        // Digits sort after anything that is not a digit, as in glibc.
825        assert_eq!(libcmp("lib1", "liba"), Ordering::Greater);
826        assert_eq!(libcmp("liba", "lib1"), Ordering::Less);
827        // Absurd digit runs saturate rather than overflow.
828        let huge = format!("lib{}", "9".repeat(40));
829        assert_eq!(libcmp(&huge, &huge), Ordering::Equal);
830    }
831
832    #[test]
833    fn identical_entries_collapse_and_output_is_stable() {
834        let entries = [
835            entry("libc.so.6", "/lib/libc.so.6"),
836            entry("libc.so.6", "/lib/libc.so.6"),
837            entry("libc.so.6", "/other/libc.so.6"),
838        ];
839        let bytes = build(&x86_64(), &entries).unwrap();
840        assert_eq!(read_u32(&bytes, 20).unwrap(), 2, "duplicates collapse");
841
842        let mut shuffled = entries.to_vec();
843        shuffled.reverse();
844        assert_eq!(
845            build(&x86_64(), &shuffled).unwrap(),
846            bytes,
847            "input order must not change the image"
848        );
849    }
850
851    #[test]
852    fn an_architecture_without_a_known_cache_id_is_refused() {
853        let unsupported = Architecture {
854            machine: Machine::RiscV64,
855            ..x86_64()
856        };
857        assert!(build(&unsupported, &[entry("libc.so.6", "/lib/libc.so.6")]).is_none());
858        let big_endian = Architecture {
859            endianness: Endianness::Big,
860            ..x86_64()
861        };
862        assert!(build(&big_endian, &[entry("libc.so.6", "/lib/libc.so.6")]).is_none());
863        // An empty cache is still a valid cache.
864        assert!(build(&x86_64(), &[]).is_some());
865    }
866
867    #[test]
868    fn dropped_entries_are_not_counted() {
869        // A relative path is not something ldconfig writes, and resolving one
870        // would depend on this process's working directory, so it is dropped.
871        let bytes = new_format(&[
872            ("libc.so.6", "/lib/libc.so.6"),
873            ("librel.so.1", "relative/librel.so.1"),
874            ("libc.so.6", "/lib/libc.so.6"),
875        ]);
876        let cache = LdCache::parse(&bytes);
877        assert!(cache.lookup("librel.so.1").is_empty());
878        assert_eq!(
879            cache.entry_count(),
880            1,
881            "only what the cache kept is counted"
882        );
883    }
884
885    #[test]
886    fn candidates_per_soname_are_bounded_in_cache_order() {
887        let owned: Vec<(String, String)> = (0..=CACHE_CANDIDATES_PER_SONAME_MAX)
888            .map(|index| ("libmany.so".to_string(), format!("/lib/libmany-{index}.so")))
889            .collect();
890        let entries: Vec<(&str, &str)> = owned
891            .iter()
892            .map(|(soname, path)| (soname.as_str(), path.as_str()))
893            .collect();
894
895        let cache = LdCache::parse(&new_format(&entries));
896        let found = cache.lookup("libmany.so");
897        assert_eq!(found.len(), CACHE_CANDIDATES_PER_SONAME_MAX);
898        assert_eq!(found[0], Path::new("/lib/libmany-0.so"));
899        assert_eq!(
900            found.last(),
901            Some(&PathBuf::from(format!(
902                "/lib/libmany-{}.so",
903                CACHE_CANDIDATES_PER_SONAME_MAX - 1
904            )))
905        );
906    }
907
908    #[test]
909    fn compatible_lookup_rejects_foreign_abi_and_cpu_specific_entries() {
910        let mut bytes = new_format(&[("libpick.so", "/lib/libpick.so")]);
911        // First entry begins straight after the new-format header.
912        bytes[NEW_HEADER_LEN..NEW_HEADER_LEN + 4].copy_from_slice(&0x0000_0a03u32.to_le_bytes()); // aarch64 libc6
913        let cache = LdCache::parse(&bytes);
914        let x86 = Architecture {
915            machine: Machine::X86_64,
916            class: ElfClass::Elf64,
917            endianness: Endianness::Little,
918        };
919        assert!(cache.lookup_compatible("libpick.so", &x86).is_empty());
920
921        bytes[NEW_HEADER_LEN..NEW_HEADER_LEN + 4].copy_from_slice(&0x0000_0303u32.to_le_bytes());
922        bytes[NEW_HEADER_LEN + 16..NEW_HEADER_LEN + 24].copy_from_slice(&1u64.to_le_bytes());
923        let cache = LdCache::parse(&bytes);
924        assert!(cache.lookup_compatible("libpick.so", &x86).is_empty());
925    }
926
927    #[test]
928    fn garbage_degrades_to_an_empty_cache() {
929        assert!(LdCache::parse(b"not a cache at all").is_empty());
930        assert!(LdCache::parse(&[]).is_empty());
931    }
932
933    #[test]
934    fn an_absurd_entry_count_allocates_nothing() {
935        assert_eq!(
936            entry_capacity(&[0u8; 48], 0, NEW_HEADER_LEN, NEW_ENTRY_LEN),
937            0
938        );
939        assert_eq!(
940            entry_capacity(&[0u8; 48 + 24], 0, NEW_HEADER_LEN, NEW_ENTRY_LEN),
941            1
942        );
943        assert_eq!(entry_capacity(&[], 4096, NEW_HEADER_LEN, NEW_ENTRY_LEN), 0);
944
945        let mut bytes = new_format(&[("libc.so.6", "/lib/libc.so.6")]);
946        bytes[20..24].copy_from_slice(&u32::MAX.to_le_bytes());
947        let cache = LdCache::parse(&bytes);
948        assert_eq!(cache.lookup("libc.so.6"), [PathBuf::from("/lib/libc.so.6")]);
949        assert_eq!(
950            cache.entry_count(),
951            1,
952            "parsing stops at the end of the file"
953        );
954    }
955
956    #[test]
957    fn truncated_entries_do_not_panic() {
958        let mut bytes = new_format(&[("libc.so.6", "/lib/libc.so.6")]);
959        bytes.truncate(NEW_HEADER_LEN + 4);
960        assert!(LdCache::parse(&bytes).is_empty());
961    }
962}