1use std::collections::BTreeSet;
17
18const ELF_HEADER_SIZE: u64 = 64;
19const PROGRAM_HEADER_SIZE: u64 = 56;
20const DYNAMIC_ENTRY_SIZE: u64 = 16;
21const VERSION_ENTRY_SIZE: u64 = 16;
22
23const PT_LOAD: u32 = 1;
24const PT_DYNAMIC: u32 = 2;
25const PT_INTERP: u32 = 3;
26const DT_NULL: u64 = 0;
27const DT_NEEDED: u64 = 1;
28const DT_STRTAB: u64 = 5;
29const DT_STRSZ: u64 = 10;
30const DT_RPATH: u64 = 15;
31const DT_RUNPATH: u64 = 29;
32const DT_VERDEF: u64 = 0x6fff_fffc;
33const DT_VERDEFNUM: u64 = 0x6fff_fffd;
34const DT_VERNEED: u64 = 0x6fff_fffe;
35const DT_VERNEEDNUM: u64 = 0x6fff_ffff;
36const VER_FLG_BASE: u16 = 1;
37const VER_FLG_WEAK: u16 = 2;
38const VERDEF_SIZE: u64 = 20;
39const VERDAUX_SIZE: u64 = 8;
40
41const MAX_PROGRAM_HEADERS: u64 = 1_024;
42const MAX_METADATA_RECORDS: usize = 65_536;
43const MAX_STRING_LENGTH: usize = 65_536;
44const MAX_OUTPUT_STRING_BYTES: usize = 16 * 1024 * 1024;
45
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct ElfInfo {
48 pub machine: u16,
49 pub interpreter: Option<String>,
50 pub needed: Vec<String>,
52 pub rpath: Vec<String>,
54 pub runpath: Vec<String>,
56 pub versions: Vec<VersionNeed>,
57 pub definitions: Vec<String>,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct VersionNeed {
64 pub library: String,
65 pub name: String,
66 pub weak: bool,
67}
68
69#[derive(Clone, Copy, Debug)]
70struct Segment {
71 offset: u64,
72 vaddr: u64,
73 filesz: u64,
74}
75
76struct Reader<'a> {
77 bytes: &'a [u8],
78}
79
80impl<'a> Reader<'a> {
81 fn take(&self, offset: u64, length: u64) -> Result<&'a [u8], String> {
82 let end = offset
83 .checked_add(length)
84 .ok_or("ELF file offset overflow")?;
85 let start = usize::try_from(offset).map_err(|_| "ELF offset exceeds address space")?;
86 let end = usize::try_from(end).map_err(|_| "ELF range exceeds address space")?;
87 self.bytes.get(start..end).ok_or_else(|| {
88 format!("truncated ELF: file range {offset:#x} + {length:#x} is unavailable")
89 })
90 }
91
92 fn u16(&self, offset: u64) -> Result<u16, String> {
93 let b = self.take(offset, 2)?;
94 Ok(u16::from_le_bytes([b[0], b[1]]))
95 }
96
97 fn u32(&self, offset: u64) -> Result<u32, String> {
98 let b = self.take(offset, 4)?;
99 Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
100 }
101
102 fn u64(&self, offset: u64) -> Result<u64, String> {
103 let b = self.take(offset, 8)?;
104 Ok(u64::from_le_bytes([
105 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
106 ]))
107 }
108}
109
110struct Budget {
111 records: usize,
112 string_bytes: usize,
113}
114
115impl Budget {
116 fn record(&mut self) -> Result<(), String> {
117 if self.records == 0 {
118 return Err("ELF metadata record count exceeds inspection limit".into());
119 }
120 self.records -= 1;
121 Ok(())
122 }
123
124 fn charge_string(&mut self, length: usize) -> Result<(), String> {
125 self.string_bytes = self
126 .string_bytes
127 .checked_sub(length)
128 .ok_or("ELF metadata strings exceed inspection limit")?;
129 Ok(())
130 }
131
132 fn string(&mut self, bytes: &[u8], kind: &str, allow_empty: bool) -> Result<String, String> {
133 let end = bytes
134 .iter()
135 .take(MAX_STRING_LENGTH + 1)
136 .position(|byte| *byte == 0)
137 .ok_or_else(|| {
138 format!("{kind} is not NUL terminated within the string inspection limit")
139 })?;
140 if !allow_empty && end == 0 {
141 return Err(format!("{kind} cannot be empty"));
142 }
143 self.charge_string(end)?;
144 let value = std::str::from_utf8(&bytes[..end])
145 .map_err(|_| format!("{kind} is not UTF-8; this prototype cannot represent it"))?;
146 Ok(value.to_owned())
147 }
148}
149
150fn add(value: u64, delta: u64, kind: &str) -> Result<u64, String> {
151 value
152 .checked_add(delta)
153 .ok_or_else(|| format!("{kind} address overflow"))
154}
155
156fn file_offset(loads: &[Segment], address: u64, length: u64) -> Result<u64, String> {
159 let end = add(address, length, "virtual")?;
160 let mut result = None;
161 for load in loads {
162 let load_end = add(load.vaddr, load.filesz, "PT_LOAD")?;
163 if address >= load.vaddr && end <= load_end {
164 let offset = add(load.offset, address - load.vaddr, "PT_LOAD file")?;
165 if let Some(previous) = result {
166 if previous != offset {
167 return Err("ambiguous overlapping PT_LOAD address mapping".into());
168 }
169 }
170 result = Some(offset);
171 }
172 }
173 result.ok_or_else(|| {
174 format!("ELF address {address:#x} + {length:#x} is not file-backed by one PT_LOAD")
175 })
176}
177
178fn check_load_mappings(loads: &mut [Segment]) -> Result<(), String> {
181 loads.sort_unstable_by_key(|load| load.vaddr);
182 let mut previous: Option<Segment> = None;
183 for &load in loads.iter().filter(|load| load.filesz != 0) {
184 if let Some(prior) = previous {
185 let prior_end = add(prior.vaddr, prior.filesz, "PT_LOAD")?;
186 if load.vaddr < prior_end {
187 let expected = add(prior.offset, load.vaddr - prior.vaddr, "PT_LOAD file")?;
188 if expected != load.offset {
189 return Err("ambiguous overlapping PT_LOAD address mapping".into());
190 }
191 if add(load.vaddr, load.filesz, "PT_LOAD")? <= prior_end {
192 continue;
193 }
194 }
195 }
196 previous = Some(load);
197 }
198 Ok(())
199}
200
201fn set_once(slot: &mut Option<u64>, value: u64, name: &str) -> Result<(), String> {
202 if slot.replace(value).is_some() {
203 return Err(format!("duplicate {name} entry"));
204 }
205 Ok(())
206}
207
208fn table_string(
209 strings: &[u8],
210 offset: u64,
211 kind: &str,
212 allow_empty: bool,
213 budget: &mut Budget,
214) -> Result<String, String> {
215 let start = usize::try_from(offset).map_err(|_| format!("{kind} string offset overflow"))?;
216 let remaining = strings
217 .get(start..)
218 .ok_or_else(|| format!("{kind} offset lies outside DT_STRTAB/DT_STRSZ"))?;
219 budget.string(remaining, kind, allow_empty)
220}
221
222fn claim_record(seen: &mut BTreeSet<u64>, address: u64) -> Result<(), String> {
225 let end = add(address, VERSION_ENTRY_SIZE, "version record")?;
226 if let Some(&prior) = seen.range(..=address).next_back() {
227 if add(prior, VERSION_ENTRY_SIZE, "version record")? > address {
228 return Err("overlapping or cyclic ELF version records".into());
229 }
230 }
231 if let Some(&next) = seen.range(address..).next() {
232 if next < end {
233 return Err("overlapping or cyclic ELF version records".into());
234 }
235 }
236 seen.insert(address);
237 Ok(())
238}
239
240fn next_record(current: u64, delta: u32, last: bool, kind: &str) -> Result<u64, String> {
241 if last {
242 if delta != 0 {
243 return Err(format!("{kind} chain exceeds its declared count"));
244 }
245 return Ok(current);
246 }
247 if u64::from(delta) < VERSION_ENTRY_SIZE {
248 return Err(format!(
249 "{kind} chain ends early or overlaps its current record"
250 ));
251 }
252 add(current, u64::from(delta), kind)
253}
254
255fn read_versions(
256 reader: &Reader<'_>,
257 loads: &[Segment],
258 strings: &[u8],
259 address: u64,
260 count: u64,
261 budget: &mut Budget,
262) -> Result<Vec<VersionNeed>, String> {
263 if count == 0 || count > MAX_METADATA_RECORDS as u64 {
264 return Err("invalid DT_VERNEEDNUM or version count exceeds inspection limit".into());
265 }
266 let mut versions = Vec::new();
267 let mut seen = BTreeSet::new();
268 let mut current = address;
269 for index in 0..count {
270 budget.record()?;
271 claim_record(&mut seen, current)?;
272 let offset = file_offset(loads, current, VERSION_ENTRY_SIZE)?;
273 let record = Reader {
274 bytes: reader.take(offset, VERSION_ENTRY_SIZE)?,
275 };
276 if record.u16(0)? != 1 {
277 return Err("unsupported ELF Verneed structure version".into());
278 }
279 let aux_count = u64::from(record.u16(2)?);
280 let library = table_string(
281 strings,
282 u64::from(record.u32(4)?),
283 "version dependency library",
284 false,
285 budget,
286 )?;
287 let aux_delta = u64::from(record.u32(8)?);
288 let next_delta = record.u32(12)?;
289 if aux_count == 0 || aux_delta < VERSION_ENTRY_SIZE {
290 return Err("Verneed has no auxiliary entries or an invalid vn_aux offset".into());
291 }
292 let mut aux_address = add(current, aux_delta, "Vernaux")?;
293 for aux_index in 0..aux_count {
294 budget.record()?;
295 claim_record(&mut seen, aux_address)?;
296 let offset = file_offset(loads, aux_address, VERSION_ENTRY_SIZE)?;
297 let aux = Reader {
298 bytes: reader.take(offset, VERSION_ENTRY_SIZE)?,
299 };
300 let flags = aux.u16(4)?;
301 if flags & !3 != 0 {
302 return Err(format!(
303 "unsupported ELF version requirement flags {flags:#x}"
304 ));
305 }
306 let name = table_string(
307 strings,
308 u64::from(aux.u32(8)?),
309 "version requirement name",
310 false,
311 budget,
312 )?;
313 budget.charge_string(library.len())?;
314 versions.push(VersionNeed {
315 library: library.clone(),
316 name,
317 weak: flags & VER_FLG_WEAK != 0,
318 });
319 aux_address = next_record(
320 aux_address,
321 aux.u32(12)?,
322 aux_index + 1 == aux_count,
323 "Vernaux",
324 )?;
325 }
326 current = next_record(current, next_delta, index + 1 == count, "Verneed")?;
327 }
328 Ok(versions)
329}
330
331fn read_definitions(
334 reader: &Reader<'_>,
335 loads: &[Segment],
336 strings: &[u8],
337 address: u64,
338 count: u64,
339 budget: &mut Budget,
340) -> Result<Vec<String>, String> {
341 if count == 0 || count > MAX_METADATA_RECORDS as u64 {
342 return Err("invalid DT_VERDEFNUM or definition count exceeds inspection limit".into());
343 }
344 let mut definitions = Vec::new();
345 let mut current = address;
346 for index in 0..count {
347 budget.record()?;
348 let offset = file_offset(loads, current, VERDEF_SIZE)?;
349 let record = Reader {
350 bytes: reader.take(offset, VERDEF_SIZE)?,
351 };
352 if record.u16(0)? != 1 {
353 return Err("unsupported ELF Verdef structure version".into());
354 }
355 let flags = record.u16(2)?;
356 let aux_count = record.u16(6)?;
357 let aux_delta = u64::from(record.u32(12)?);
358 let next_delta = u64::from(record.u32(16)?);
359 if aux_count == 0 || aux_delta < VERDEF_SIZE {
360 return Err("Verdef has no auxiliary entries or an invalid vd_aux offset".into());
361 }
362 let aux_address = add(current, aux_delta, "Verdaux")?;
363 let aux_offset = file_offset(loads, aux_address, VERDAUX_SIZE)?;
364 let aux = Reader {
365 bytes: reader.take(aux_offset, VERDAUX_SIZE)?,
366 };
367 let name = table_string(
368 strings,
369 u64::from(aux.u32(0)?),
370 "version definition name",
371 false,
372 budget,
373 )?;
374 if flags & VER_FLG_BASE == 0 {
375 definitions.push(name);
376 }
377 if index + 1 == count {
379 if next_delta != 0 {
380 return Err("Verdef chain exceeds its declared count".into());
381 }
382 } else if next_delta < VERDEF_SIZE {
383 return Err("Verdef chain ends early or overlaps its current record".into());
384 } else {
385 current = add(current, next_delta, "Verdef")?;
386 }
387 }
388 Ok(definitions)
389}
390
391pub fn inspect(bytes: &[u8]) -> Result<ElfInfo, String> {
392 let reader = Reader { bytes };
393 let header = reader.take(0, ELF_HEADER_SIZE)?;
394 if &header[..4] != b"\x7fELF" {
395 return Err("input is not an ELF file".into());
396 }
397 if header[4] != 2 || header[5] != 1 {
398 return Err("only ELF64 little-endian files are supported".into());
399 }
400 if header[6] != 1 || reader.u32(20)? != 1 {
401 return Err("unsupported ELF format version".into());
402 }
403 if header[7] != 0 && header[7] != 3 {
404 return Err("only System V / GNU Linux ELF OS ABI values are supported".into());
405 }
406 let file_type = reader.u16(16)?;
407 if file_type != 2 && file_type != 3 {
408 return Err(
409 "only ELF executables and shared objects (ET_EXEC/ET_DYN) are supported".into(),
410 );
411 }
412 let machine = reader.u16(18)?;
413 if machine != 62 {
414 return Err(format!(
415 "only x86-64 ELF is supported (e_machine is {machine})"
416 ));
417 }
418 if reader.u16(52)? != ELF_HEADER_SIZE as u16 {
419 return Err("unexpected ELF header size".into());
420 }
421 let phoff = reader.u64(32)?;
422 let phsize = u64::from(reader.u16(54)?);
423 let phcount = u64::from(reader.u16(56)?);
424 if phcount == 0xffff {
425 return Err("extended ELF program-header numbering is not supported".into());
426 }
427 if phcount == 0 || phcount > MAX_PROGRAM_HEADERS {
428 return Err("ELF program-header count is zero or exceeds inspection limit".into());
429 }
430 if phsize != PROGRAM_HEADER_SIZE || phoff < ELF_HEADER_SIZE {
431 return Err("invalid ELF program-header entry size or table offset".into());
432 }
433 reader.take(phoff, phcount * phsize)?;
435
436 let mut loads = Vec::new();
437 let mut dynamic = None;
438 let mut interpreter = None;
439 let mut budget = Budget {
440 records: MAX_METADATA_RECORDS,
441 string_bytes: MAX_OUTPUT_STRING_BYTES,
442 };
443 for index in 0..phcount {
444 let offset = add(phoff, index * phsize, "program header")?;
445 let ph = Reader {
446 bytes: reader.take(offset, phsize)?,
447 };
448 let kind = ph.u32(0)?;
449 let segment = Segment {
450 offset: ph.u64(8)?,
451 vaddr: ph.u64(16)?,
452 filesz: ph.u64(32)?,
453 };
454 let memsz = ph.u64(40)?;
455 if segment.filesz != 0 {
456 reader.take(segment.offset, segment.filesz)?;
457 }
458 match kind {
459 PT_LOAD => {
460 if segment.filesz > memsz {
461 return Err("PT_LOAD file size exceeds memory size".into());
462 }
463 add(segment.vaddr, memsz, "PT_LOAD memory")?;
464 loads.push(segment);
465 }
466 PT_DYNAMIC => {
467 if dynamic.replace(segment).is_some() {
468 return Err("duplicate PT_DYNAMIC segments".into());
469 }
470 }
471 PT_INTERP => {
472 if interpreter.is_some() {
473 return Err("duplicate PT_INTERP segments".into());
474 }
475 let data = reader.take(segment.offset, segment.filesz)?;
476 let value = budget.string(data, "ELF interpreter", false)?;
477 if data[value.len() + 1..].iter().any(|byte| *byte != 0) {
478 return Err("nonzero bytes follow the ELF interpreter terminator".into());
479 }
480 interpreter = Some(value);
481 }
482 _ => {}
483 }
484 }
485 if loads.is_empty() {
486 return Err("ELF executable/shared object has no PT_LOAD segments".into());
487 }
488 check_load_mappings(&mut loads)?;
489 let mut info = ElfInfo {
490 machine,
491 interpreter,
492 needed: Vec::new(),
493 rpath: Vec::new(),
494 runpath: Vec::new(),
495 versions: Vec::new(),
496 definitions: Vec::new(),
497 };
498 let Some(dynamic) = dynamic else {
499 return Ok(info);
500 };
501 if dynamic.filesz == 0 || dynamic.filesz % DYNAMIC_ENTRY_SIZE != 0 {
502 return Err("PT_DYNAMIC size is not a nonempty array of ELF64 dynamic entries".into());
503 }
504 if file_offset(&loads, dynamic.vaddr, dynamic.filesz)? != dynamic.offset {
505 return Err("PT_DYNAMIC file offset disagrees with its PT_LOAD address mapping".into());
506 }
507 let entries = dynamic.filesz / DYNAMIC_ENTRY_SIZE;
508 if entries > MAX_METADATA_RECORDS as u64 {
509 return Err("PT_DYNAMIC entry count exceeds inspection limit".into());
510 }
511 let mut strtab = None;
512 let mut strsz = None;
513 let mut verneed = None;
514 let mut verneednum = None;
515 let mut verdef = None;
516 let mut verdefnum = None;
517 let mut needed = Vec::new();
518 let mut rpath = None;
519 let mut runpath = None;
520 let mut terminated = false;
521 for index in 0..entries {
522 budget.record()?;
523 let offset = add(dynamic.offset, index * DYNAMIC_ENTRY_SIZE, "dynamic entry")?;
524 let tag = reader.u64(offset)?;
525 let value = reader.u64(add(offset, 8, "dynamic entry")?)?;
526 match tag {
527 DT_NULL => {
528 terminated = true;
529 break;
530 }
531 DT_NEEDED => needed.push(value),
532 DT_STRTAB => set_once(&mut strtab, value, "DT_STRTAB")?,
533 DT_STRSZ => set_once(&mut strsz, value, "DT_STRSZ")?,
534 DT_RPATH => set_once(&mut rpath, value, "DT_RPATH")?,
535 DT_RUNPATH => set_once(&mut runpath, value, "DT_RUNPATH")?,
536 DT_VERNEED => set_once(&mut verneed, value, "DT_VERNEED")?,
537 DT_VERNEEDNUM => set_once(&mut verneednum, value, "DT_VERNEEDNUM")?,
538 DT_VERDEF => set_once(&mut verdef, value, "DT_VERDEF")?,
539 DT_VERDEFNUM => set_once(&mut verdefnum, value, "DT_VERDEFNUM")?,
540 _ => {}
541 }
542 }
543 if !terminated {
544 return Err("PT_DYNAMIC has no DT_NULL terminator".into());
545 }
546 let strings = match (strtab, strsz) {
547 (None, None) => &[][..],
548 (Some(address), Some(length)) if length != 0 => {
549 let offset = file_offset(&loads, address, length)?;
550 let data = reader.take(offset, length)?;
551 if data.first() != Some(&0) || data.last() != Some(&0) {
552 return Err("DT_STRTAB must start and end with a NUL byte".into());
553 }
554 data
555 }
556 _ => return Err("DT_STRTAB and a nonzero DT_STRSZ must be supplied together".into()),
557 };
558 for offset in needed {
559 info.needed.push(table_string(
560 strings,
561 offset,
562 "DT_NEEDED library",
563 false,
564 &mut budget,
565 )?);
566 }
567 if let Some(offset) = rpath {
568 info.rpath.push(table_string(
569 strings,
570 offset,
571 "DT_RPATH",
572 true,
573 &mut budget,
574 )?);
575 }
576 if let Some(offset) = runpath {
577 info.runpath.push(table_string(
578 strings,
579 offset,
580 "DT_RUNPATH",
581 true,
582 &mut budget,
583 )?);
584 }
585 match (verneed, verneednum) {
586 (None, None | Some(0)) => {}
587 (Some(address), Some(count)) => {
588 info.versions = read_versions(&reader, &loads, strings, address, count, &mut budget)?;
589 }
590 _ => return Err("DT_VERNEED and DT_VERNEEDNUM must be supplied together".into()),
591 }
592 match (verdef, verdefnum) {
593 (None, None | Some(0)) => {}
594 (Some(address), Some(count)) => {
595 info.definitions =
596 read_definitions(&reader, &loads, strings, address, count, &mut budget)?;
597 }
598 _ => return Err("DT_VERDEF and DT_VERDEFNUM must be supplied together".into()),
599 }
600 Ok(info)
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606
607 const BASE: u64 = 0x40_0000;
608 const DYNAMIC: usize = 0x200;
609 const STRINGS: usize = 0x300;
610 const VERNEED: usize = 0x400;
611 const INTERP: usize = 0x500;
612
613 fn put16(bytes: &mut [u8], offset: usize, value: u16) {
614 bytes[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
615 }
616
617 fn put32(bytes: &mut [u8], offset: usize, value: u32) {
618 bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
619 }
620
621 fn put64(bytes: &mut [u8], offset: usize, value: u64) {
622 bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
623 }
624
625 fn ph(bytes: &mut [u8], index: usize, kind: u32, offset: usize, size: usize) {
626 let start = 64 + index * 56;
627 put32(bytes, start, kind);
628 put64(bytes, start + 8, offset as u64);
629 put64(bytes, start + 16, BASE + offset as u64);
630 put64(bytes, start + 32, size as u64);
631 put64(bytes, start + 40, size as u64);
632 }
633
634 fn dyn_entry(bytes: &mut [u8], index: usize, tag: u64, value: u64) {
635 put64(bytes, DYNAMIC + index * 16, tag);
636 put64(bytes, DYNAMIC + index * 16 + 8, value);
637 }
638
639 fn string(bytes: &mut [u8], next: &mut usize, value: &str) -> u32 {
640 let offset = *next;
641 let end = offset + value.len();
642 bytes[STRINGS + offset..STRINGS + end].copy_from_slice(value.as_bytes());
643 bytes[STRINGS + end] = 0;
644 *next = end + 1;
645 offset as u32
646 }
647
648 fn fixture() -> Vec<u8> {
651 let mut bytes = vec![0_u8; 0x600];
652 bytes[..7].copy_from_slice(b"\x7fELF\x02\x01\x01");
653 put16(&mut bytes, 16, 3);
654 put16(&mut bytes, 18, 62);
655 put32(&mut bytes, 20, 1);
656 put64(&mut bytes, 32, 64);
657 put16(&mut bytes, 52, 64);
658 put16(&mut bytes, 54, 56);
659 put16(&mut bytes, 56, 3);
660 let length = bytes.len();
661 ph(&mut bytes, 0, PT_LOAD, 0, length);
662 ph(&mut bytes, 1, PT_DYNAMIC, DYNAMIC, 9 * 16);
663 let interp = b"/lib64/ld-linux-x86-64.so.2\0";
664 ph(&mut bytes, 2, PT_INTERP, INTERP, interp.len());
665 bytes[INTERP..INTERP + interp.len()].copy_from_slice(interp);
666
667 let mut next = 1;
668 let libc = string(&mut bytes, &mut next, "libc.so.6");
669 let libgcc = string(&mut bytes, &mut next, "libgcc_s.so.1");
670 let old = string(&mut bytes, &mut next, "GLIBC_2.2.5");
671 let newer_weak = string(&mut bytes, &mut next, "GLIBC_2.34");
672 let gcc = string(&mut bytes, &mut next, "GCC_3.0");
673 let rpath = string(&mut bytes, &mut next, "/old/lib:/old/lib64");
674 let runpath = string(&mut bytes, &mut next, "$ORIGIN/lib::/usr/lib64");
675 dyn_entry(&mut bytes, 0, DT_STRTAB, BASE + STRINGS as u64);
676 dyn_entry(&mut bytes, 1, DT_STRSZ, next as u64);
677 dyn_entry(&mut bytes, 2, DT_NEEDED, u64::from(libc));
678 dyn_entry(&mut bytes, 3, DT_NEEDED, u64::from(libgcc));
679 dyn_entry(&mut bytes, 4, DT_VERNEED, BASE + VERNEED as u64);
680 dyn_entry(&mut bytes, 5, DT_VERNEEDNUM, 2);
681 dyn_entry(&mut bytes, 6, DT_RPATH, u64::from(rpath));
682 dyn_entry(&mut bytes, 7, DT_RUNPATH, u64::from(runpath));
683 dyn_entry(&mut bytes, 8, DT_NULL, 0);
684
685 put16(&mut bytes, VERNEED, 1);
686 put16(&mut bytes, VERNEED + 2, 2);
687 put32(&mut bytes, VERNEED + 4, libc);
688 put32(&mut bytes, VERNEED + 8, 16);
689 put32(&mut bytes, VERNEED + 12, 48);
690 put16(&mut bytes, VERNEED + 16 + 6, 2);
691 put32(&mut bytes, VERNEED + 16 + 8, old);
692 put32(&mut bytes, VERNEED + 16 + 12, 16);
693 put16(&mut bytes, VERNEED + 32 + 4, VER_FLG_WEAK);
694 put16(&mut bytes, VERNEED + 32 + 6, 3);
695 put32(&mut bytes, VERNEED + 32 + 8, newer_weak);
696
697 put16(&mut bytes, VERNEED + 48, 1);
698 put16(&mut bytes, VERNEED + 48 + 2, 1);
699 put32(&mut bytes, VERNEED + 48 + 4, libgcc);
700 put32(&mut bytes, VERNEED + 48 + 8, 16);
701 put16(&mut bytes, VERNEED + 64 + 6, 4);
702 put32(&mut bytes, VERNEED + 64 + 8, gcc);
703 bytes
704 }
705
706 #[test]
707 fn reads_stripped_dynamic_metadata_and_distinguishes_weak_versions() {
708 let info = inspect(&fixture()).unwrap();
709 assert_eq!(info.machine, 62);
710 assert_eq!(
711 info.interpreter.as_deref(),
712 Some("/lib64/ld-linux-x86-64.so.2")
713 );
714 assert_eq!(info.needed, ["libc.so.6", "libgcc_s.so.1"]);
715 assert_eq!(info.rpath, ["/old/lib:/old/lib64"]);
716 assert_eq!(info.runpath, ["$ORIGIN/lib::/usr/lib64"]);
717 assert_eq!(
718 info.versions,
719 [
720 VersionNeed {
721 library: "libc.so.6".into(),
722 name: "GLIBC_2.2.5".into(),
723 weak: false,
724 },
725 VersionNeed {
726 library: "libc.so.6".into(),
727 name: "GLIBC_2.34".into(),
728 weak: true,
729 },
730 VersionNeed {
731 library: "libgcc_s.so.1".into(),
732 name: "GCC_3.0".into(),
733 weak: false,
734 },
735 ]
736 );
737 }
738
739 fn library_fixture() -> Vec<u8> {
742 const VERDEF: usize = 0x480;
743 let mut bytes = fixture();
744 let mut next = 0x80;
745 let soname = string(&mut bytes, &mut next, "libc.so.6");
746 let old = string(&mut bytes, &mut next, "GLIBC_2.2.5");
747 let new = string(&mut bytes, &mut next, "GLIBC_2.34");
748 dyn_entry(&mut bytes, 1, DT_STRSZ, next as u64);
749 dyn_entry(&mut bytes, 6, DT_VERDEF, BASE + VERDEF as u64);
750 dyn_entry(&mut bytes, 7, DT_VERDEFNUM, 3);
751 let records = [
752 (VER_FLG_BASE, 1, soname, None),
753 (0, 1, old, None),
754 (0, 2, new, Some(old)),
755 ];
756 for (index, (flags, count, name, parent)) in records.into_iter().enumerate() {
757 let at = VERDEF + index * 36;
758 put16(&mut bytes, at, 1);
759 put16(&mut bytes, at + 2, flags);
760 put16(&mut bytes, at + 6, count);
761 put32(&mut bytes, at + 12, 20);
762 put32(&mut bytes, at + 16, if index == 2 { 0 } else { 36 });
763 put32(&mut bytes, at + 20, name);
764 if let Some(parent) = parent {
765 put32(&mut bytes, at + 24, 8);
766 put32(&mut bytes, at + 28, parent);
767 }
768 }
769 bytes
770 }
771
772 #[test]
773 fn reads_version_definitions_without_base_or_parent_entries() {
774 let info = inspect(&library_fixture()).unwrap();
775 assert_eq!(info.definitions, ["GLIBC_2.2.5", "GLIBC_2.34"]);
776 assert!(inspect(&fixture()).unwrap().definitions.is_empty());
777 }
778
779 #[test]
780 fn rejects_malformed_definition_chains() {
781 let mut bytes = library_fixture();
782 put32(&mut bytes, 0x480 + 16, 0);
783 assert!(inspect(&bytes).unwrap_err().contains("ends early"));
784 let mut bytes = library_fixture();
785 put32(&mut bytes, 0x480 + 72 + 16, 36);
786 assert!(inspect(&bytes).is_err());
787 let mut bytes = library_fixture();
788 dyn_entry(&mut bytes, 7, 21, 0);
789 assert!(inspect(&bytes).unwrap_err().contains("supplied together"));
790 let bytes = library_fixture();
791 for length in 0x480..bytes.len() {
792 assert!(
793 inspect(&bytes[..length]).is_err(),
794 "accepted prefix {length}"
795 );
796 }
797 }
798
799 #[test]
800 fn accepts_executable_without_dynamic_metadata() {
801 let mut bytes = fixture();
802 put16(&mut bytes, 16, 2);
803 put16(&mut bytes, 56, 1);
804 let info = inspect(&bytes).unwrap();
805 assert!(info.interpreter.is_none());
806 assert!(info.needed.is_empty());
807 assert!(info.versions.is_empty());
808 }
809
810 #[test]
811 fn every_truncated_prefix_returns_an_error_without_panicking() {
812 let bytes = fixture();
813 for length in 0..bytes.len() {
814 assert!(
815 inspect(&bytes[..length]).is_err(),
816 "accepted prefix {length}"
817 );
818 }
819 }
820
821 #[test]
822 fn rejects_unsupported_architecture_encoding_and_object_type() {
823 for (offset, value) in [(0, 0), (4, 1), (5, 2), (6, 2), (7, 9), (16, 1), (18, 3)] {
824 let mut bytes = fixture();
825 bytes[offset] = value;
826 assert!(inspect(&bytes).is_err(), "accepted mutation at {offset}");
827 }
828 }
829
830 #[test]
831 fn rejects_header_count_range_and_overflow() {
832 let mut bytes = fixture();
833 put16(&mut bytes, 56, 0xffff);
834 assert!(inspect(&bytes).unwrap_err().contains("extended"));
835 put16(&mut bytes, 56, 3);
836 put64(&mut bytes, 32, u64::MAX - 16);
837 assert!(inspect(&bytes).unwrap_err().contains("overflow"));
838 }
839
840 #[test]
841 fn rejects_dynamic_pointer_outside_file_backed_memory() {
842 let mut bytes = fixture();
843 dyn_entry(&mut bytes, 0, DT_STRTAB, BASE + 0x700);
844 put64(&mut bytes, 64 + 40, 0x1000); assert!(inspect(&bytes).unwrap_err().contains("file-backed"));
846 }
847
848 #[test]
849 fn rejects_dynamic_address_offset_disagreement() {
850 let mut bytes = fixture();
851 put64(&mut bytes, 64 + 56 + 16, BASE + DYNAMIC as u64 + 16);
852 assert!(inspect(&bytes).unwrap_err().contains("disagrees"));
853 }
854
855 #[test]
856 fn rejects_unterminated_or_duplicate_dynamic_metadata() {
857 let mut bytes = fixture();
858 dyn_entry(&mut bytes, 8, 21, 0);
859 assert!(inspect(&bytes).unwrap_err().contains("DT_NULL"));
860 dyn_entry(&mut bytes, 8, DT_STRTAB, BASE + STRINGS as u64);
861 assert!(inspect(&bytes).unwrap_err().contains("duplicate DT_STRTAB"));
862 }
863
864 #[test]
865 fn rejects_invalid_string_bounds_and_termination() {
866 let mut bytes = fixture();
867 dyn_entry(&mut bytes, 2, DT_NEEDED, u64::MAX);
868 assert!(inspect(&bytes).is_err());
869 let mut bytes = fixture();
870 bytes[INTERP + b"/lib64/ld-linux-x86-64.so.2".len()] = b'x';
871 assert!(inspect(&bytes).unwrap_err().contains("NUL terminated"));
872 let mut bytes = fixture();
873 bytes[STRINGS] = b'x';
874 assert!(inspect(&bytes).unwrap_err().contains("start and end"));
875 }
876
877 #[test]
878 fn rejects_early_or_overlong_version_chains() {
879 let mut bytes = fixture();
880 put32(&mut bytes, VERNEED + 12, 0);
881 assert!(inspect(&bytes).unwrap_err().contains("ends early"));
882 let mut bytes = fixture();
883 put32(&mut bytes, VERNEED + 32 + 12, 16);
884 assert!(inspect(&bytes)
885 .unwrap_err()
886 .contains("exceeds its declared count"));
887 let mut bytes = fixture();
888 put32(&mut bytes, VERNEED + 16 + 12, 0);
889 assert!(inspect(&bytes).unwrap_err().contains("ends early"));
890 }
891
892 #[test]
893 fn rejects_overlapping_or_wrapping_version_records() {
894 let mut bytes = fixture();
895 put32(&mut bytes, VERNEED + 12, 16); assert!(inspect(&bytes).unwrap_err().contains("overlapping"));
897 let mut bytes = fixture();
898 put32(&mut bytes, VERNEED + 8, u32::MAX);
899 assert!(inspect(&bytes).unwrap_err().contains("file-backed"));
900 let mut bytes = fixture();
901 dyn_entry(&mut bytes, 4, DT_VERNEED, u64::MAX - 7);
902 assert!(inspect(&bytes).unwrap_err().contains("overflow"));
903 }
904
905 #[test]
906 fn rejects_huge_version_counts_and_missing_companion_tags() {
907 let mut bytes = fixture();
908 dyn_entry(&mut bytes, 5, DT_VERNEEDNUM, u64::MAX);
909 assert!(inspect(&bytes).unwrap_err().contains("inspection limit"));
910 let mut bytes = fixture();
911 dyn_entry(&mut bytes, 5, 21, 0);
912 assert!(inspect(&bytes).unwrap_err().contains("supplied together"));
913 }
914
915 #[test]
916 fn rejects_partial_overlapping_load_mappings() {
917 let mut bytes = fixture();
918 put16(&mut bytes, 56, 4);
919 ph(&mut bytes, 3, PT_LOAD, 0x100, 0x100);
920 put64(&mut bytes, 64 + 3 * 56 + 16, BASE + 0x120);
921 assert!(inspect(&bytes)
922 .unwrap_err()
923 .contains("ambiguous overlapping"));
924 }
925
926 #[test]
927 fn accepts_redundant_load_with_identical_address_mapping() {
928 let mut bytes = fixture();
929 put16(&mut bytes, 56, 4);
930 ph(&mut bytes, 3, PT_LOAD, STRINGS, 0x100);
931 assert_eq!(inspect(&bytes).unwrap().versions.len(), 3);
932 }
933}