1use std::collections::BTreeSet;
17use std::fs::File;
18use std::io::{self, Read, Seek, SeekFrom};
19use std::path::Path;
20
21pub type VersionSet = BTreeSet<String>;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Choice {
28 Host,
31 Bundle,
33 Incomparable,
36}
37
38pub fn compare(bundle: &VersionSet, host: &VersionSet) -> Choice {
41 if host.is_subset(bundle) {
42 Choice::Bundle
43 } else if bundle.is_subset(host) {
44 Choice::Host
45 } else {
46 Choice::Incomparable
47 }
48}
49
50const SHT_DYNAMIC: u32 = 6;
51const SHT_GNU_VERDEF: u32 = 0x6fff_fffd;
52const VER_FLG_BASE: u16 = 0x1;
53const VERDEF_ENTRY: usize = 20;
54const VERDAUX_ENTRY: usize = 8;
55const DT_NULL: u64 = 0;
56const DT_NEEDED: u64 = 1;
57
58const MAX_SHDR_TABLE: usize = 1 << 20;
61const MAX_SECTION: usize = 1 << 20;
64const MAX_STRTAB: usize = 64 << 20;
65
66fn invalid(msg: &str) -> io::Error {
67 io::Error::new(io::ErrorKind::InvalidData, msg)
68}
69
70pub fn read(path: &Path) -> io::Result<VersionSet> {
77 let mut object = Object::open(path)?;
78 let Some((defs, strings)) = object.section_with_strings(SHT_GNU_VERDEF)? else {
79 return Ok(VersionSet::new());
80 };
81 parse_verdef(&defs, &strings)
82}
83
84pub fn read_needed(path: &Path) -> io::Result<Vec<String>> {
87 let mut object = Object::open(path)?;
88 let Some((dynamic, strings)) = object.section_with_strings(SHT_DYNAMIC)? else {
89 return Ok(Vec::new());
90 };
91 parse_needed(&dynamic, &strings, object.layout.class64)
92}
93
94struct Object {
96 file: File,
97 file_len: u64,
98 layout: Layout,
99 table: Vec<u8>,
100}
101
102impl Object {
103 fn open(path: &Path) -> io::Result<Self> {
104 let mut file = File::open(path)?;
105 let file_len = file.metadata()?.len();
106 let mut header = [0u8; 64];
107 let n = file.read(&mut header)?;
108 let layout = Layout::parse(&header[..n])?;
109
110 let table_len = layout
111 .shnum
112 .checked_mul(layout.shentsize)
113 .ok_or_else(|| invalid("section header table overflows"))?;
114 if table_len > MAX_SHDR_TABLE {
115 return Err(invalid("section header table is implausibly large"));
116 }
117 let table = read_at(&mut file, file_len, layout.shoff, table_len)?;
118 Ok(Object {
119 file,
120 file_len,
121 layout,
122 table,
123 })
124 }
125
126 fn section_with_strings(&mut self, kind: u32) -> io::Result<Option<(Vec<u8>, Vec<u8>)>> {
129 let Some(section) = self.layout.find_section(&self.table, kind)? else {
130 return Ok(None);
131 };
132 if section.size > MAX_SECTION {
133 return Err(invalid("section is implausibly large"));
134 }
135 let strtab = self.layout.section(&self.table, section.link)?;
136 if strtab.size > MAX_STRTAB {
137 return Err(invalid("string table is implausibly large"));
138 }
139 let body = read_at(&mut self.file, self.file_len, section.offset, section.size)?;
140 let strings = read_at(&mut self.file, self.file_len, strtab.offset, strtab.size)?;
141 Ok(Some((body, strings)))
142 }
143}
144
145fn read_at(file: &mut File, file_len: u64, offset: u64, len: usize) -> io::Result<Vec<u8>> {
148 let end = offset
149 .checked_add(len as u64)
150 .ok_or_else(|| invalid("section range overflows"))?;
151 if end > file_len {
152 return Err(invalid("section lies outside the file"));
153 }
154 file.seek(SeekFrom::Start(offset))?;
155 let mut buf = vec![0u8; len];
156 file.read_exact(&mut buf)?;
157 Ok(buf)
158}
159
160struct Section {
161 offset: u64,
162 size: usize,
163 link: usize,
164}
165
166struct Layout {
169 class64: bool,
170 shoff: u64,
171 shentsize: usize,
172 shnum: usize,
173}
174
175impl Layout {
176 fn parse(header: &[u8]) -> io::Result<Self> {
177 if header.len() < 64 || header[0..4] != *b"\x7fELF" {
178 return Err(invalid("not an ELF object"));
179 }
180 if header[5] != 1 {
181 return Err(invalid("big-endian ELF is not supported"));
182 }
183 let layout = match header[4] {
184 2 => Layout {
185 class64: true,
186 shoff: u64::from_le_bytes(header[40..48].try_into().unwrap()),
187 shentsize: u16::from_le_bytes(header[58..60].try_into().unwrap()) as usize,
188 shnum: u16::from_le_bytes(header[60..62].try_into().unwrap()) as usize,
189 },
190 1 => Layout {
191 class64: false,
192 shoff: u32::from_le_bytes(header[32..36].try_into().unwrap()) as u64,
193 shentsize: u16::from_le_bytes(header[46..48].try_into().unwrap()) as usize,
194 shnum: u16::from_le_bytes(header[48..50].try_into().unwrap()) as usize,
195 },
196 _ => return Err(invalid("unknown ELF class")),
197 };
198 let min_entry = if layout.class64 { 64 } else { 40 };
199 if layout.shnum > 0 && layout.shentsize < min_entry {
200 return Err(invalid("section header entries are too small"));
201 }
202 Ok(layout)
203 }
204
205 fn section(&self, table: &[u8], index: usize) -> io::Result<Section> {
206 let start = index
207 .checked_mul(self.shentsize)
208 .ok_or_else(|| invalid("section index overflows"))?;
209 let entry = table
210 .get(start..start + self.shentsize)
211 .ok_or_else(|| invalid("section index outside the table"))?;
212 let (offset, size, link) = if self.class64 {
213 (
214 u64::from_le_bytes(entry[24..32].try_into().unwrap()),
215 u64::from_le_bytes(entry[32..40].try_into().unwrap()),
216 u32::from_le_bytes(entry[40..44].try_into().unwrap()),
217 )
218 } else {
219 (
220 u32::from_le_bytes(entry[16..20].try_into().unwrap()) as u64,
221 u32::from_le_bytes(entry[20..24].try_into().unwrap()) as u64,
222 u32::from_le_bytes(entry[24..28].try_into().unwrap()),
223 )
224 };
225 let size = usize::try_from(size).map_err(|_| invalid("section size overflows"))?;
226 Ok(Section {
227 offset,
228 size,
229 link: link as usize,
230 })
231 }
232
233 fn find_section(&self, table: &[u8], kind: u32) -> io::Result<Option<Section>> {
234 for i in 0..self.shnum {
235 let start = i * self.shentsize;
236 let entry = &table[start..start + self.shentsize];
237 let sh_type = u32::from_le_bytes(entry[4..8].try_into().unwrap());
238 if sh_type == kind {
239 return self.section(table, i).map(Some);
240 }
241 }
242 Ok(None)
243 }
244}
245
246fn parse_verdef(defs: &[u8], strings: &[u8]) -> io::Result<VersionSet> {
249 let mut out = VersionSet::new();
250 if defs.is_empty() {
251 return Ok(out);
252 }
253 let mut pos = 0usize;
254 for _ in 0..=defs.len() / VERDEF_ENTRY {
257 let entry = defs
258 .get(pos..pos + VERDEF_ENTRY)
259 .ok_or_else(|| invalid("version definition outside its section"))?;
260 let flags = u16::from_le_bytes(entry[2..4].try_into().unwrap());
261 let aux = u32::from_le_bytes(entry[12..16].try_into().unwrap()) as usize;
262 let next = u32::from_le_bytes(entry[16..20].try_into().unwrap()) as usize;
263
264 if flags & VER_FLG_BASE == 0 {
265 let aux_at = pos
266 .checked_add(aux)
267 .ok_or_else(|| invalid("version auxiliary offset overflows"))?;
268 let vda = defs
269 .get(aux_at..aux_at + VERDAUX_ENTRY)
270 .ok_or_else(|| invalid("version name entry outside its section"))?;
271 let name_at = u32::from_le_bytes(vda[0..4].try_into().unwrap()) as usize;
272 out.insert(string_at(strings, name_at)?);
273 }
274
275 if next == 0 {
276 return Ok(out);
277 }
278 pos = pos
279 .checked_add(next)
280 .ok_or_else(|| invalid("version definition chain overflows"))?;
281 }
282 Err(invalid("version definition chain does not terminate"))
283}
284
285fn parse_needed(dynamic: &[u8], strings: &[u8], class64: bool) -> io::Result<Vec<String>> {
287 let entry_len = if class64 { 16 } else { 8 };
288 let mut out = Vec::new();
289 for entry in dynamic.chunks_exact(entry_len) {
290 let (tag, value) = if class64 {
291 (
292 u64::from_le_bytes(entry[0..8].try_into().unwrap()),
293 u64::from_le_bytes(entry[8..16].try_into().unwrap()),
294 )
295 } else {
296 (
297 u32::from_le_bytes(entry[0..4].try_into().unwrap()) as u64,
298 u32::from_le_bytes(entry[4..8].try_into().unwrap()) as u64,
299 )
300 };
301 match tag {
302 DT_NULL => break,
303 DT_NEEDED => {
304 let at = usize::try_from(value).map_err(|_| invalid("needed name overflows"))?;
305 out.push(string_at(strings, at)?);
306 }
307 _ => {}
308 }
309 }
310 Ok(out)
311}
312
313fn string_at(strings: &[u8], at: usize) -> io::Result<String> {
314 let rest = strings
315 .get(at..)
316 .ok_or_else(|| invalid("name outside the string table"))?;
317 let end = rest
318 .iter()
319 .position(|&b| b == 0)
320 .ok_or_else(|| invalid("name is not terminated"))?;
321 std::str::from_utf8(&rest[..end])
322 .map(String::from)
323 .map_err(|_| invalid("name is not UTF-8"))
324}
325
326#[cfg(test)]
328pub(crate) mod fixture {
329 use super::*;
330
331 pub(crate) fn shared_object(defs: &[(u16, &str)], needed: &[&str]) -> Vec<u8> {
336 shared_object_with_interp(defs, needed, None)
337 }
338
339 pub(crate) fn shared_object_with_interp(
342 defs: &[(u16, &str)],
343 needed: &[&str],
344 interp: Option<&str>,
345 ) -> Vec<u8> {
346 let mut strings: Vec<u8> = vec![0];
347 let mut intern = |s: &str| {
348 let at = strings.len() as u32;
349 strings.extend_from_slice(s.as_bytes());
350 strings.push(0);
351 at
352 };
353 let name_offsets: Vec<u32> = defs.iter().map(|(_, n)| intern(n)).collect();
354 let needed_offsets: Vec<u32> = needed.iter().map(|n| intern(n)).collect();
355
356 let mut verdef = Vec::new();
357 for (i, (flags, _)) in defs.iter().enumerate() {
358 let last = i + 1 == defs.len();
359 verdef.extend_from_slice(&1u16.to_le_bytes());
360 verdef.extend_from_slice(&flags.to_le_bytes());
361 verdef.extend_from_slice(&(i as u16).to_le_bytes());
362 verdef.extend_from_slice(&1u16.to_le_bytes());
363 verdef.extend_from_slice(&0u32.to_le_bytes());
364 verdef.extend_from_slice(&(VERDEF_ENTRY as u32).to_le_bytes());
365 let next = if last {
366 0
367 } else {
368 (VERDEF_ENTRY + VERDAUX_ENTRY) as u32
369 };
370 verdef.extend_from_slice(&next.to_le_bytes());
371 verdef.extend_from_slice(&name_offsets[i].to_le_bytes());
372 verdef.extend_from_slice(&0u32.to_le_bytes());
373 }
374
375 let mut dynamic = Vec::new();
376 for off in needed_offsets {
377 dynamic.extend_from_slice(&DT_NEEDED.to_le_bytes());
378 dynamic.extend_from_slice(&(off as u64).to_le_bytes());
379 }
380 dynamic.extend_from_slice(&DT_NULL.to_le_bytes());
381 dynamic.extend_from_slice(&0u64.to_le_bytes());
382
383 let mut elf = build_elf(&strings, &verdef, &dynamic);
384 if let Some(interp) = interp {
385 let phoff = elf.len();
389 let interp_at = phoff + 56;
390 elf.resize(interp_at + interp.len() + 1, 0);
391 elf[32..40].copy_from_slice(&(phoff as u64).to_le_bytes());
392 elf[54..56].copy_from_slice(&56u16.to_le_bytes());
393 elf[56..58].copy_from_slice(&1u16.to_le_bytes());
394 elf[phoff..phoff + 4].copy_from_slice(&3u32.to_le_bytes());
395 elf[phoff + 8..phoff + 16].copy_from_slice(&(interp_at as u64).to_le_bytes());
396 elf[phoff + 32..phoff + 40].copy_from_slice(&((interp.len() + 1) as u64).to_le_bytes());
397 elf[interp_at..interp_at + interp.len()].copy_from_slice(interp.as_bytes());
398 }
399 elf
400 }
401
402 pub(crate) fn build_elf(strings: &[u8], verdef: &[u8], dynamic: &[u8]) -> Vec<u8> {
403 let shoff = 64usize;
404 let shentsize = 64usize;
405 let shnum = 4usize;
406 let strings_at = shoff + shentsize * shnum;
407 let verdef_at = strings_at + strings.len();
408 let dynamic_at = verdef_at + verdef.len();
409 let mut v = vec![0u8; dynamic_at + dynamic.len()];
410 v[0..4].copy_from_slice(b"\x7fELF");
411 v[4] = 2;
412 v[5] = 1;
413 v[16..18].copy_from_slice(&3u16.to_le_bytes()); v[40..48].copy_from_slice(&(shoff as u64).to_le_bytes());
415 v[58..60].copy_from_slice(&(shentsize as u16).to_le_bytes());
416 v[60..62].copy_from_slice(&(shnum as u16).to_le_bytes());
417 let mut shdr = |index: usize, sh_type: u32, offset: usize, size: usize, link: u32| {
418 let at = shoff + index * shentsize;
419 v[at + 4..at + 8].copy_from_slice(&sh_type.to_le_bytes());
420 v[at + 24..at + 32].copy_from_slice(&(offset as u64).to_le_bytes());
421 v[at + 32..at + 40].copy_from_slice(&(size as u64).to_le_bytes());
422 v[at + 40..at + 44].copy_from_slice(&link.to_le_bytes());
423 };
424 shdr(1, 3, strings_at, strings.len(), 0);
425 shdr(2, SHT_GNU_VERDEF, verdef_at, verdef.len(), 1);
426 shdr(3, SHT_DYNAMIC, dynamic_at, dynamic.len(), 1);
427 v[strings_at..strings_at + strings.len()].copy_from_slice(strings);
428 v[verdef_at..verdef_at + verdef.len()].copy_from_slice(verdef);
429 v[dynamic_at..dynamic_at + dynamic.len()].copy_from_slice(dynamic);
430 v
431 }
432
433 pub(crate) fn temp_dir(tag: &str) -> std::path::PathBuf {
435 let dir = std::env::temp_dir().join(format!(
436 "onelf-{tag}-{}-{}",
437 std::process::id(),
438 std::time::SystemTime::now()
439 .duration_since(std::time::UNIX_EPOCH)
440 .unwrap()
441 .as_nanos()
442 ));
443 std::fs::create_dir_all(&dir).unwrap();
444 dir
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::fixture::{build_elf, shared_object, temp_dir};
451 use super::*;
452
453 fn set(names: &[&str]) -> VersionSet {
454 names.iter().map(|s| s.to_string()).collect()
455 }
456
457 fn write_temp(name: &str, bytes: &[u8]) -> std::path::PathBuf {
458 let path = temp_dir("verdef").join(name);
459 std::fs::write(&path, bytes).unwrap();
460 path
461 }
462
463 #[test]
464 fn compare_orders_by_superset() {
465 let old = set(&["GLIBC_2.2.5", "GLIBC_2.17"]);
466 let new = set(&["GLIBC_2.2.5", "GLIBC_2.17", "GLIBC_2.34"]);
467 assert_eq!(compare(&old, &new), Choice::Host);
468 assert_eq!(compare(&new, &old), Choice::Bundle);
469 assert_eq!(compare(&new, &new), Choice::Bundle);
470 assert_eq!(compare(&set(&[]), &set(&[])), Choice::Bundle);
471 assert_eq!(compare(&old, &set(&[])), Choice::Bundle);
473 assert_eq!(compare(&set(&[]), &old), Choice::Host);
476 let fork = set(&["GLIBC_2.2.5", "VENDOR_1.0"]);
477 assert_eq!(compare(&old, &fork), Choice::Incomparable);
478 }
479
480 #[test]
481 fn reads_definitions_and_skips_the_base_version() {
482 let elf = shared_object(
483 &[
484 (VER_FLG_BASE, "libfoo.so.1"),
485 (0, "FOO_1.0"),
486 (0, "FOO_1.2"),
487 ],
488 &[],
489 );
490 let path = write_temp("libfoo.so.1", &elf);
491 assert_eq!(read(&path).unwrap(), set(&["FOO_1.0", "FOO_1.2"]));
492 }
493
494 #[test]
495 fn reads_needed_sonames_in_order() {
496 let elf = shared_object(&[], &["libc.so.6", "libm.so.6"]);
497 let path = write_temp("libbar.so.1", &elf);
498 assert_eq!(read_needed(&path).unwrap(), ["libc.so.6", "libm.so.6"]);
499 assert!(read(&path).unwrap().is_empty());
500 }
501
502 #[test]
503 fn no_sections_means_nothing_defined_or_needed() {
504 let mut v = vec![0u8; 64];
505 v[0..4].copy_from_slice(b"\x7fELF");
506 v[4] = 2;
507 v[5] = 1;
508 let path = write_temp("plain.so", &v);
509 assert!(read(&path).unwrap().is_empty());
510 assert!(read_needed(&path).unwrap().is_empty());
511 }
512
513 #[test]
514 fn not_an_elf_is_an_error() {
515 let path = write_temp("text", b"#!/bin/sh\n");
516 assert!(read(&path).is_err());
517 assert!(read_needed(&path).is_err());
518 }
519
520 #[test]
521 fn truncated_section_is_an_error_not_a_panic() {
522 let elf = shared_object(&[(0, "FOO_1.0")], &["libc.so.6"]);
523 let path = write_temp("cut.so", &elf[..elf.len() - 4]);
524 assert!(read_needed(&path).is_err());
525 for cut in 0..elf.len() {
526 let p = write_temp("cut.so", &elf[..cut]);
527 let _ = read(&p);
528 let _ = read_needed(&p);
529 }
530 }
531
532 #[test]
533 fn name_past_the_string_table_is_an_error() {
534 let strings = b"\0FOO_1.0\0";
535 let mut verdef = Vec::new();
536 verdef.extend_from_slice(&1u16.to_le_bytes());
537 verdef.extend_from_slice(&0u16.to_le_bytes());
538 verdef.extend_from_slice(&0u16.to_le_bytes());
539 verdef.extend_from_slice(&1u16.to_le_bytes());
540 verdef.extend_from_slice(&0u32.to_le_bytes());
541 verdef.extend_from_slice(&(VERDEF_ENTRY as u32).to_le_bytes());
542 verdef.extend_from_slice(&0u32.to_le_bytes());
543 verdef.extend_from_slice(&9999u32.to_le_bytes());
544 verdef.extend_from_slice(&0u32.to_le_bytes());
545 let mut dynamic = Vec::new();
546 dynamic.extend_from_slice(&DT_NEEDED.to_le_bytes());
547 dynamic.extend_from_slice(&9999u64.to_le_bytes());
548 let path = write_temp("bad.so", &build_elf(strings, &verdef, &dynamic));
549 assert!(read(&path).is_err());
550 assert!(read_needed(&path).is_err());
551 }
552
553 #[test]
554 fn a_chain_that_leaves_the_section_is_an_error() {
555 let mut elf = shared_object(&[(0, "FOO_1.0"), (0, "FOO_1.1")], &[]);
556 let dynamic_len = 16;
558 let verdef_at = elf.len() - dynamic_len - 2 * (VERDEF_ENTRY + VERDAUX_ENTRY);
559 elf[verdef_at + 16..verdef_at + 20].copy_from_slice(&1000u32.to_le_bytes());
560 let path = write_temp("runaway.so", &elf);
561 assert!(read(&path).is_err());
562
563 let mut elf = shared_object(&[(0, "FOO_1.0"), (0, "FOO_1.1")], &[]);
565 elf[verdef_at + 16..verdef_at + 20].copy_from_slice(&0u32.to_le_bytes());
566 let path = write_temp("short.so", &elf);
567 assert_eq!(read(&path).unwrap(), set(&["FOO_1.0"]));
568 }
569
570 #[test]
571 fn reads_the_host_libc_when_present() {
572 let candidates = [
573 "/lib64/libc.so.6",
574 "/usr/lib64/libc.so.6",
575 "/usr/lib/libc.so.6",
576 "/lib/x86_64-linux-gnu/libc.so.6",
577 "/lib/aarch64-linux-gnu/libc.so.6",
578 ];
579 let Some(libc) = candidates.iter().find(|p| Path::new(p).is_file()) else {
580 return;
581 };
582 let set = read(Path::new(libc)).unwrap();
583 assert!(set.iter().any(|v| v.starts_with("GLIBC_2.")), "{set:?}");
584 assert!(!set.iter().any(|v| v == "libc.so.6"));
585 let needed = read_needed(Path::new(libc)).unwrap();
586 assert!(
587 needed.iter().any(|n| n.starts_with("ld-linux")),
588 "{needed:?}"
589 );
590 }
591}