1use 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
23const OLD_HEADER_LEN: usize = 16;
25const OLD_ENTRY_LEN: usize = 12;
26const NEW_HEADER_LEN: usize = 48;
28const NEW_ENTRY_LEN: usize = 24;
29
30const CACHE_ENTRIES_MAX: usize = 65_536;
35const CACHE_BYTES_MAX: u64 = 16 * 1024 * 1024;
38const CACHE_STRING_LEN_MAX: usize = 4096;
41const CACHE_CANDIDATES_PER_SONAME_MAX: usize = 256;
44
45#[derive(Debug, Clone, Default)]
46pub struct LdCache {
47 entries: HashMap<String, Vec<PathBuf>>,
49 records: HashMap<String, Vec<CacheRecord>>,
51 len: usize,
54}
55
56#[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 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 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 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 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
173fn 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
187fn 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
201fn 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
212fn 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 let Some(header_flags) = bytes.get(base + 28).copied() else {
268 return Vec::new();
269 };
270 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#[derive(Debug, Clone, PartialEq, Eq)]
320pub struct CacheEntry {
321 pub soname: String,
322 pub path: PathBuf,
324}
325
326fn 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
341const FLAGS_ENDIAN_LITTLE: u8 = 2;
345
346struct StringTable {
349 bytes: Vec<u8>,
350 offsets: Vec<(u32, u32)>,
351}
352
353fn 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 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
384fn 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
394fn 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
413pub fn build(architecture: &Architecture, entries: &[CacheEntry]) -> Option<Vec<u8>> {
419 if entries.iter().any(|entry| !entry.path.is_absolute()) {
420 return None;
421 }
422 if !entries.iter().all(is_encodable) {
427 return None;
428 }
429
430 let flags = entry_flags(architecture)?;
431 if architecture.endianness != Endianness::Little {
432 return None;
435 }
436
437 let mut entries: Vec<&CacheEntry> = entries.iter().collect();
438 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]); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&[0u8; 12]); 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()); out.extend_from_slice(&0u64.to_le_bytes()); }
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 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
497fn 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 #[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 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 #[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 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 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 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 #[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); bytes.extend_from_slice(&offset(nlibs).to_le_bytes());
689 for (key, value) in &offsets {
690 bytes.extend_from_slice(&0u32.to_le_bytes()); 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); bytes.extend_from_slice(&offset(nlibs).to_le_bytes());
713 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 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 #[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 "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 assert_eq!(libcmp("libfoo.so", "libfoo.so.1"), Ordering::Less);
824 assert_eq!(libcmp("lib1", "liba"), Ordering::Greater);
826 assert_eq!(libcmp("liba", "lib1"), Ordering::Less);
827 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 assert!(build(&x86_64(), &[]).is_some());
865 }
866
867 #[test]
868 fn dropped_entries_are_not_counted() {
869 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 bytes[NEW_HEADER_LEN..NEW_HEADER_LEN + 4].copy_from_slice(&0x0000_0a03u32.to_le_bytes()); 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}