1use std::fs;
28use std::io::Read;
29use std::os::unix::ffi::OsStrExt;
30use std::os::unix::fs::MetadataExt;
31use std::os::unix::fs::OpenOptionsExt;
32use std::path::Path;
33use std::path::PathBuf;
34
35use serde::Deserialize;
36use serde::Serialize;
37
38const BINPRM_BUF_SIZE: usize = 256;
41
42const MAX_INTERPRETER_DEPTH: usize = 5;
45
46#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
48pub struct ExecRecord {
49 pub exe: PathBuf,
52 dev: u64,
55 ino: u64,
56 nlink: u64,
60 ctime: (i64, i64),
61 directories: Vec<(u64, u64)>,
65}
66
67fn directory_chain(root: &Path, path: &Path) -> Option<Vec<(u64, u64)>> {
73 path.ancestors()
74 .skip(1)
75 .take_while(|directory| directory.starts_with(root))
76 .map(|directory| directory_sample(&crate::util::raw_lstat(directory).ok()?))
77 .collect()
78}
79
80fn directory_sample(stat: &libc::stat) -> Option<(u64, u64)> {
85 (stat.st_mode & libc::S_IFMT == libc::S_IFDIR).then_some((stat.st_dev, stat.st_ino))
86}
87
88impl ExecRecord {
89 pub fn resolve(root: &Path, cwd: &Path, path: &Path) -> Option<Self> {
106 Self::resolve_from(root, Some(cwd), path)
107 }
108
109 pub fn resolve_from(root: &Path, cwd: Option<&Path>, path: &Path) -> Option<Self> {
114 let from_cwd = |path: &Path| {
115 if path.is_absolute() {
116 Some(path.to_path_buf())
117 } else {
118 Some(cwd?.join(path))
119 }
120 };
121 let mut spelled = from_cwd(path)?;
122 for _ in 0..=MAX_INTERPRETER_DEPTH {
123 let found = crate::util::canonicalize(&below(root, &spelled)?).ok()?;
124 if !fs::metadata(&found).ok()?.is_file() {
127 return None;
128 }
129 match script_interpreter(&found) {
130 Script::Not => return Self::at(root, &found),
131 Script::Interpreter(interpreter) => spelled = from_cwd(&interpreter)?,
134 Script::Declined | Script::Unread => return None,
135 }
136 }
137 None
138 }
139
140 pub fn at(root: &Path, found: &Path) -> Option<Self> {
143 let metadata = fs::metadata(found).ok()?;
144 Some(Self {
145 exe: Path::new("/").join(found.strip_prefix(root).ok()?),
146 dev: metadata.dev(),
147 ino: metadata.ino(),
148 nlink: metadata.nlink(),
149 ctime: (metadata.ctime(), metadata.ctime_nsec()),
150 directories: directory_chain(root, found).unwrap_or_default(),
151 })
152 }
153
154 pub fn identity(&self) -> (u64, u64) {
156 (self.dev, self.ino)
157 }
158
159 pub fn is_present(&self, root: &Path) -> bool {
166 below(root, &self.exe)
167 .and_then(|path| fs::symlink_metadata(path).ok())
168 .is_some_and(|now| {
169 (
170 now.dev(),
171 now.ino(),
172 now.nlink(),
173 (now.ctime(), now.ctime_nsec()),
174 ) == (self.dev, self.ino, self.nlink, self.ctime)
175 })
176 }
177
178 pub fn names_path_exactly(&self, root: &Path) -> bool {
191 self.is_present(root)
192 && !self.directories.is_empty()
193 && below(root, &self.exe)
194 .and_then(|path| directory_chain(root, &path))
195 .is_some_and(|now| now == self.directories)
196 }
197}
198
199pub fn program_interpreter(path: &Path) -> Option<PathBuf> {
206 let file = crate::util::RawFd::open(path, libc::O_RDONLY).ok()?;
207 let read_exact = |offset: u64, buffer: &mut [u8]| {
208 (file.read_at(buffer, offset).ok()? == buffer.len()).then_some(())
209 };
210 let mut header = [0_u8; 64];
211 read_exact(0, &mut header)?;
212 if header[..4] != *b"\x7fELF" || header[4] != 2 || header[5] != 1 {
213 return None;
214 }
215 let u16_at = |at: usize| u16::from_le_bytes([header[at], header[at + 1]]);
216 let phoff = u64::from_le_bytes(header[32..40].try_into().unwrap());
217 let (phentsize, phnum) = (usize::from(u16_at(54)), u16_at(56));
218 if phentsize < 56 {
219 return None;
220 }
221 let mut entry = vec![0_u8; phentsize];
222 for index in 0..u64::from(phnum) {
223 read_exact(phoff.checked_add(index * phentsize as u64)?, &mut entry)?;
224 if u32::from_le_bytes(entry[..4].try_into().unwrap()) != 3 {
225 continue; }
227 let field = |at: usize| u64::from_le_bytes(entry[at..at + 8].try_into().unwrap());
228 let (offset, size) = (field(8), field(32));
229 if size < 2 || size > libc::PATH_MAX as u64 {
230 return None;
231 }
232 let mut name = vec![0_u8; size as usize];
233 read_exact(offset, &mut name)?;
234 if name.last() != Some(&0) {
235 return None;
236 }
237 let end = name.iter().position(|byte| *byte == 0)?;
238 return Some(PathBuf::from(std::ffi::OsStr::from_bytes(&name[..end])));
239 }
240 None
241}
242
243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
247pub struct ElfLayout {
248 pub position_independent: bool,
251 pub interpreter: bool,
253 pub phdr: u64,
258 pub end: u64,
261 pub text: Option<(u64, u64)>,
265 pub data_span: u64,
270 pub page: u64,
275}
276
277impl ElfLayout {
278 pub fn read(path: &Path) -> Option<Self> {
282 Self::read_file(&crate::util::RawFd::open(path, libc::O_RDONLY).ok()?)
283 }
284
285 pub fn read_file(file: &crate::util::RawFd) -> Option<Self> {
288 let read_exact = |offset: u64, buffer: &mut [u8]| {
289 (file.read_at(buffer, offset).ok()? == buffer.len()).then_some(())
290 };
291 let mut header = [0_u8; 64];
292 read_exact(0, &mut header)?;
293 Self::parse(&header, read_exact, crate::page::page_size())
294 }
295
296 fn parse(
300 header: &[u8; 64],
301 mut read_at: impl FnMut(u64, &mut [u8]) -> Option<()>,
302 page: u64,
303 ) -> Option<Self> {
304 let u16_at = |at: usize| u16::from_le_bytes([header[at], header[at + 1]]);
305 let u64_at = |at: usize| u64::from_le_bytes(header[at..at + 8].try_into().unwrap());
306 if header[..4] != *b"\x7fELF" || header[4] != 2 || header[5] != 1 {
308 return None;
309 }
310 let position_independent = match u16_at(16) {
311 2 => false, 3 => true, _ => return None,
314 };
315 let (phoff, phentsize, phnum) = (u64_at(32), usize::from(u16_at(54)), u16_at(56));
316 if phentsize < 56 {
317 return None;
318 }
319 let mut entry = vec![0_u8; phentsize];
320 let (mut phdr, mut end, mut interpreter) = (0, None::<u64>, false);
321 let (mut text, mut start_data, mut end_data) = (None::<(u64, u64)>, 0_u64, 0_u64);
322 for index in 0..u64::from(phnum) {
323 read_at(phoff.checked_add(index * phentsize as u64)?, &mut entry)?;
324 let field = |at: usize| u64::from_le_bytes(entry[at..at + 8].try_into().unwrap());
325 match u32::from_le_bytes(entry[..4].try_into().unwrap()) {
326 1 => {} 3 => {
328 interpreter = true;
330 continue;
331 }
332 _ => continue,
333 }
334 let (offset, vaddr, filesz, memsz) = (field(8), field(16), field(32), field(40));
335 if text.is_none() && u32::from_le_bytes(entry[4..8].try_into().unwrap()) & 1 != 0 {
337 text = Some((offset & !(page - 1), vaddr & !(page - 1)));
338 }
339 if offset <= phoff && phoff < offset.checked_add(filesz)? {
340 phdr = (phoff - offset).checked_add(vaddr)?;
341 }
342 start_data = start_data.max(vaddr);
343 end_data = end_data.max(vaddr.checked_add(filesz)?);
344 let segment_end = vaddr.checked_add(memsz)?;
345 end = Some(end.map_or(segment_end, |end| end.max(segment_end)));
346 }
347 Some(Self {
348 position_independent,
349 interpreter,
350 phdr,
351 end: end?,
352 text,
353 data_span: end_data.saturating_sub(start_data),
354 page,
355 })
356 }
357
358 pub fn start_brk(&self, at_phdr: u64) -> Result<u64, &'static str> {
364 let bias = at_phdr
365 .checked_sub(self.phdr)
366 .ok_or("AT_PHDR below the image's program headers")?;
367 self.start_brk_at_bias(bias)
368 }
369
370 pub fn start_brk_at_bias(&self, bias: u64) -> Result<u64, &'static str> {
372 if self.position_independent && !self.interpreter {
373 return Err(
377 "a position-independent image without an interpreter, whose break depends on the kernel's build",
378 );
379 }
380 bias.checked_add(self.end)
381 .and_then(|end| end.checked_add(self.page - 1))
382 .map(|end| end & !(self.page - 1))
383 .ok_or("an image ending past the address space")
384 }
385}
386
387pub fn load_bias(
401 maps: &[u8],
402 path: &Path,
403 ino: u64,
404 (text_offset, text_page): (u64, u64),
405) -> Result<u64, &'static str> {
406 fn text(field: &[u8]) -> Option<&str> {
409 std::str::from_utf8(field).ok()
410 }
411 let mut displayed = Vec::new();
414 for &byte in path.as_os_str().as_bytes() {
415 if byte == b'\n' {
416 displayed.extend_from_slice(b"\\012");
417 } else {
418 displayed.push(byte);
419 }
420 }
421 let mut biases = maps.split(|&byte| byte == b'\n').filter_map(|line| {
422 let mut fields = line.splitn(6, |byte| byte.is_ascii_whitespace());
423 let range = text(fields.next()?)?;
424 let permissions = fields.next()?;
425 let offset = u64::from_str_radix(text(fields.next()?)?, 16).ok()?;
426 let _device = fields.next()?;
427 let map_ino: u64 = text(fields.next()?)?.parse().ok()?;
428 let pathname = fields.next()?.trim_ascii_start();
429 if !permissions.contains(&b'x')
430 || offset != text_offset
431 || map_ino != ino
432 || pathname != displayed
433 {
434 return None;
435 }
436 let start = u64::from_str_radix(range.split_once('-')?.0, 16).ok()?;
437 start.checked_sub(text_page)
438 });
439 let bias = biases
440 .next()
441 .ok_or("no executable mapping of the image's text")?;
442 if biases.any(|other| other != bias) {
443 return Err("executable mappings of the image's text at different biases");
444 }
445 Ok(bias)
446}
447
448pub fn stat_start_brk(stat: &str) -> Option<u64> {
451 stat[stat.rfind(')')? + 1..]
452 .split_ascii_whitespace()
453 .nth(47 - 3)?
454 .parse()
455 .ok()
456}
457
458#[derive(Clone, Copy, Debug, PartialEq, Eq)]
461pub enum BrkCheck {
462 Equal(u64),
464 Differs {
466 model: u64,
468 kernel: u64,
470 },
471 Unmodeled(&'static str),
473 Randomized(u64),
476 RandomizationUnknown(u64),
478 KernelUnread(u64),
480}
481
482impl BrkCheck {
483 pub fn of(
487 model: Result<u64, &'static str>,
488 randomized: Option<bool>,
489 kernel: Option<u64>,
490 ) -> Self {
491 let model = match model {
492 Ok(model) => model,
493 Err(reason) => return Self::Unmodeled(reason),
494 };
495 match (randomized, kernel) {
496 (Some(true), _) => Self::Randomized(model),
497 (None, _) => Self::RandomizationUnknown(model),
498 (Some(false), None) => Self::KernelUnread(model),
499 (Some(false), Some(kernel)) if kernel == model => Self::Equal(model),
500 (Some(false), Some(kernel)) => Self::Differs { model, kernel },
501 }
502 }
503}
504
505impl std::fmt::Display for BrkCheck {
506 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
507 match self {
508 Self::Equal(model) => write!(f, "model {model:#x} equals the kernel's start_brk"),
509 Self::Differs { model, kernel } => write!(
510 f,
511 "model {model:#x} differs from the kernel's start_brk {kernel:#x}"
512 ),
513 Self::Unmodeled(reason) => write!(f, "not modeled: {reason}"),
514 Self::Randomized(model) => write!(
515 f,
516 "model {model:#x} not compared: the kernel randomized the break"
517 ),
518 Self::RandomizationUnknown(model) => write!(
519 f,
520 "model {model:#x} not compared: whether the kernel randomized the break is unknown"
521 ),
522 Self::KernelUnread(model) => write!(
523 f,
524 "model {model:#x} not compared: the kernel's start_brk is unreadable"
525 ),
526 }
527 }
528}
529
530pub fn auxv_entry(auxv: &[u8], key: u64) -> Option<u64> {
533 auxv.as_chunks::<16>()
534 .0
535 .iter()
536 .map(|pair| {
537 let word = |at: usize| u64::from_ne_bytes(pair[at..at + 8].try_into().unwrap());
538 (word(0), word(8))
539 })
540 .take_while(|&(entry, _)| entry != 0)
541 .find_map(|(entry, value)| (entry == key).then_some(value))
542}
543
544fn below(root: &Path, path: &Path) -> Option<PathBuf> {
547 Some(root.join(path.strip_prefix("/").ok()?))
548}
549
550#[derive(Debug, PartialEq, Eq)]
552enum Script {
553 Not,
555 Unread,
557 Interpreter(PathBuf),
559 Declined,
561}
562
563fn script_interpreter(path: &Path) -> Script {
565 let mut header = Vec::with_capacity(BINPRM_BUF_SIZE);
566 let read = fs::OpenOptions::new()
569 .read(true)
570 .custom_flags(libc::O_NONBLOCK | libc::O_NOCTTY)
571 .open(path)
572 .and_then(|file| file.take(BINPRM_BUF_SIZE as u64).read_to_end(&mut header));
573 match read {
574 Ok(_) => parse_script(&header),
575 Err(_) => Script::Unread,
576 }
577}
578
579fn parse_script(header: &[u8]) -> Script {
586 let mut buffer = [0_u8; BINPRM_BUF_SIZE];
587 let read = header.len().min(BINPRM_BUF_SIZE);
588 buffer[..read].copy_from_slice(&header[..read]);
589 let Some(line) = buffer.strip_prefix(b"#!") else {
590 return Script::Not;
591 };
592 let (line, ended) = match line.iter().position(|&byte| byte == b'\n') {
593 Some(end) => (&line[..end], true),
594 None => (line, false),
595 };
596 let start = line
597 .iter()
598 .position(|&byte| byte != b' ' && byte != b'\t')
599 .unwrap_or(line.len());
600 let rest = &line[start..];
601 let end = rest
602 .iter()
603 .position(|&byte| matches!(byte, b' ' | b'\t' | 0));
604 if end.is_none() && !ended {
605 return Script::Declined;
606 }
607 let interpreter = &rest[..end.unwrap_or(rest.len())];
608 if interpreter.is_empty() {
609 return Script::Declined;
610 }
611 Script::Interpreter(PathBuf::from(std::ffi::OsStr::from_bytes(interpreter)))
612}
613
614#[cfg(test)]
615mod tests {
616 use std::os::unix::fs::PermissionsExt;
617
618 use super::*;
619
620 #[test]
621 fn an_elf_layout_gives_linuxs_start_of_the_program_break() {
622 let layout = ElfLayout::read(&std::env::current_exe().unwrap()).unwrap();
624 assert!(layout.end > layout.phdr);
625 let (text_offset, text_page) = layout.text.unwrap();
627 assert_eq!((text_offset % layout.page, text_page % layout.page), (0, 0));
628 assert_eq!(layout.page, crate::page::page_size());
629 assert_eq!(ElfLayout::read(Path::new("/proc/self/status")), None);
631 let mut header = [0_u8; 64];
632 header[..4].copy_from_slice(b"\x7fELF");
633 header[4] = 1;
634 header[5] = 1;
635 assert_eq!(ElfLayout::parse(&header, |_, _| None, 4096), None);
636
637 let mut header = [0_u8; 64];
641 header[..4].copy_from_slice(b"\x7fELF");
642 header[4] = 2;
643 header[5] = 1;
644 header[16..18].copy_from_slice(&2_u16.to_le_bytes());
645 header[32..40].copy_from_slice(&64_u64.to_le_bytes());
646 header[54..56].copy_from_slice(&56_u16.to_le_bytes());
647 header[56..58].copy_from_slice(&3_u16.to_le_bytes());
648 let program_header = |kind: u32, offset: u64, vaddr: u64, filesz: u64, memsz: u64| {
649 let mut entry = [0_u8; 56];
650 entry[..4].copy_from_slice(&kind.to_le_bytes());
651 entry[8..16].copy_from_slice(&offset.to_le_bytes());
652 entry[16..24].copy_from_slice(&vaddr.to_le_bytes());
653 entry[32..40].copy_from_slice(&filesz.to_le_bytes());
654 entry[40..48].copy_from_slice(&memsz.to_le_bytes());
655 entry
656 };
657 let entries = [
658 program_header(6, 64, 0x400040, 0xa8, 0xa8), program_header(1, 0, 0x400000, 0x1000, 0x1000),
660 program_header(1, 0x2e10, 0x403e10, 0x290, 0x12a0),
661 ];
662 let read_at = |offset: u64, buffer: &mut [u8]| {
663 let index = usize::try_from((offset - 64) / 56).ok()?;
664 buffer.copy_from_slice(entries.get(index)?);
665 Some(())
666 };
667 let layout = ElfLayout::parse(&header, read_at, 4096).unwrap();
668 assert_eq!(
669 layout,
670 ElfLayout {
671 position_independent: false,
672 interpreter: false,
673 phdr: 0x400040,
674 end: 0x4050b0,
675 text: None,
678 data_span: 0x290,
679 page: 4096,
680 }
681 );
682 assert_eq!(layout.start_brk(0x400040), Ok(0x406000));
683 assert!(layout.start_brk(0x400000).is_err());
684 let large_pages = ElfLayout::parse(&header, read_at, 0x10000).unwrap();
687 assert_eq!(
688 large_pages,
689 ElfLayout {
690 page: 0x10000,
691 ..layout
692 }
693 );
694 assert_eq!(large_pages.start_brk(0x400040), Ok(0x410000));
695 let aligned = ElfLayout {
697 end: 0x406000,
698 ..layout
699 };
700 assert_eq!(aligned.start_brk(0x400040), Ok(0x406000));
701 let dynamic = ElfLayout {
703 position_independent: true,
704 interpreter: true,
705 ..aligned
706 };
707 assert_eq!(
708 dynamic.start_brk(0x5555_5555_4000 + 0x400040),
709 Ok(0x5555_5595_a000)
710 );
711 let static_pie = ElfLayout {
714 interpreter: false,
715 ..dynamic
716 };
717 assert!(static_pie.start_brk(0x5555_5555_4000 + 0x400040).is_err());
718 let unloaded = ElfLayout { phdr: 0, ..layout };
720 assert_eq!(unloaded.start_brk(0), Ok(0x406000));
721 header[16..18].copy_from_slice(&3_u16.to_le_bytes());
723 header[56..58].copy_from_slice(&4_u16.to_le_bytes());
724 let with_interp = [
725 entries[0],
726 program_header(3, 0x2a8, 0x4002a8, 0x1c, 0x1c), entries[1],
728 entries[2],
729 ];
730 let read_at = |offset: u64, buffer: &mut [u8]| {
731 let index = usize::try_from((offset - 64) / 56).ok()?;
732 buffer.copy_from_slice(with_interp.get(index)?);
733 Some(())
734 };
735 assert_eq!(
736 ElfLayout::parse(&header, read_at, 4096).unwrap(),
737 ElfLayout {
738 position_independent: true,
739 interpreter: true,
740 ..layout
741 }
742 );
743 }
744
745 #[test]
748 fn the_kernels_start_of_the_break_is_what_the_layout_gives() {
749 use std::os::unix::process::CommandExt;
750 let mut command = std::process::Command::new("/bin/sh");
751 command
752 .args(["-c", "read line"])
753 .stdin(std::process::Stdio::piped());
754 unsafe {
756 command.pre_exec(|| {
757 if libc::personality(libc::ADDR_NO_RANDOMIZE as libc::c_ulong) == -1 {
758 return Err(std::io::Error::last_os_error());
759 }
760 Ok(())
761 });
762 }
763 let mut child = command.spawn().unwrap();
765 let process = PathBuf::from(format!("/proc/{}", child.id()));
766 let image = fs::read_link(process.join("exe")).unwrap();
767 let at_phdr = auxv_entry(&fs::read(process.join("auxv")).unwrap(), libc::AT_PHDR).unwrap();
768 let start_brk = stat_start_brk(&fs::read_to_string(process.join("stat")).unwrap()).unwrap();
769 let maps = fs::read(process.join("maps")).unwrap();
770 drop(child.stdin.take());
771 child.wait().unwrap();
772 let layout = ElfLayout::read(&image).unwrap();
773 assert_eq!(layout.start_brk(at_phdr), Ok(start_brk), "{image:?}");
774 let ino = std::os::unix::fs::MetadataExt::ino(&fs::metadata(&image).unwrap());
777 let bias = load_bias(&maps, &image, ino, layout.text.unwrap()).unwrap();
778 assert_eq!(layout.start_brk_at_bias(bias), Ok(start_brk), "{image:?}");
779 }
780
781 #[test]
782 fn the_start_of_the_break_is_field_47_past_the_command() {
783 let fields: Vec<String> = (3..=52).map(|field| field.to_string()).collect();
784 let stat = format!("12 (a) b) (c) {}\n", fields.join(" "));
785 assert_eq!(stat_start_brk(&stat), Some(47));
786 assert_eq!(stat_start_brk("12 (a) S 1 2"), None);
787 assert_eq!(stat_start_brk("12 a S"), None);
788 }
789
790 #[test]
791 fn a_break_check_compares_only_where_linuxs_rule_gives_the_kernels_start() {
792 assert_eq!(
793 BrkCheck::of(Ok(0x406000), Some(false), Some(0x406000)),
794 BrkCheck::Equal(0x406000)
795 );
796 let differs = BrkCheck::of(Ok(0x406000), Some(false), Some(0x407000));
797 assert_eq!(
798 differs,
799 BrkCheck::Differs {
800 model: 0x406000,
801 kernel: 0x407000
802 }
803 );
804 assert_eq!(
805 differs.to_string(),
806 "model 0x406000 differs from the kernel's start_brk 0x407000"
807 );
808 assert_eq!(
809 BrkCheck::of(Ok(0x406000), Some(true), Some(0x1234000)),
810 BrkCheck::Randomized(0x406000)
811 );
812 assert_eq!(
813 BrkCheck::of(Ok(0x406000), None, Some(0x406000)),
814 BrkCheck::RandomizationUnknown(0x406000)
815 );
816 assert_eq!(
817 BrkCheck::of(Ok(0x406000), Some(false), None),
818 BrkCheck::KernelUnread(0x406000)
819 );
820 assert_eq!(
821 BrkCheck::of(Err("no exec record"), Some(false), Some(0x406000)),
822 BrkCheck::Unmodeled("no exec record")
823 );
824 }
825
826 #[test]
827 fn a_load_bias_comes_from_the_images_executable_mapping() {
828 let listed = "\
83200401000-00402000 r-xp 00001000 00:20 1234 /dir/image
83300402000-00403000 r--p 00002000 00:20 1234 /dir/image
834555555553000-555555554000 r-xp 00001000 00:20 99 /other
8357ffff7bac000-7ffff7bb1000 r--p 00000000 00:20 1234 /dir/image
8367ffff7fc6000-7ffff7fc7000 r-xp 00001000 00:21 5678 /with spaces
837";
838 let maps = listed.as_bytes();
839 let path = |path: &'static str| Path::new(path);
840 let text = (0x1000, 0x401000);
841 assert_eq!(load_bias(maps, path("/dir/image"), 1234, text), Ok(0));
842 assert!(load_bias(maps, path("/dir/image"), 4321, text).is_err());
843 assert!(load_bias(maps, path("/dir/ima"), 1234, text).is_err());
844 assert!(load_bias(maps, path("/elsewhere/image"), 1234, text).is_err());
847 let twice =
849 format!("{listed}7ffff0001000-7ffff0002000 r-xp 00001000 00:20 1234 /dir/image\n");
850 assert!(load_bias(twice.as_bytes(), path("/dir/image"), 1234, text).is_err());
851 let again = format!("{listed}00401000-00402000 r-xp 00001000 00:20 1234 /dir/image\n");
853 assert_eq!(
854 load_bias(again.as_bytes(), path("/dir/image"), 1234, text),
855 Ok(0)
856 );
857 assert_eq!(
859 load_bias(maps, path("/dir/image"), 1234, (0x2000, 0x402000)).ok(),
860 None
861 );
862 assert_eq!(
864 load_bias(maps, path("/with spaces"), 5678, (0x1000, 0x1000)),
865 Ok(0x7ffff7fc5000)
866 );
867 let mut named = maps.to_vec();
870 named.extend_from_slice(b"7ffff7fd0000-7ffff7fd1000 r-xp 00001000 00:21 42 /n\xffme\n");
871 let unnamed = Path::new(std::ffi::OsStr::from_bytes(b"/n\xffme"));
872 assert_eq!(
873 load_bias(&named, unnamed, 42, (0x1000, 0x1000)),
874 Ok(0x7ffff7fcf000)
875 );
876 assert_eq!(load_bias(&named, path("/dir/image"), 1234, text), Ok(0));
877 named.extend_from_slice(
879 b"7ffff7fe0000-7ffff7fe1000 r-xp 00001000 00:21 43 /line\\012break\n",
880 );
881 let broken = Path::new("/line\nbreak");
882 assert_eq!(
883 load_bias(&named, broken, 43, (0x1000, 0x1000)),
884 Ok(0x7ffff7fdf000)
885 );
886 let layout = ElfLayout {
889 position_independent: true,
890 interpreter: true,
891 phdr: 0x40,
892 end: 0x50b0,
893 text: Some((0x1000, 0x1000)),
894 data_span: 0x290,
895 page: 4096,
896 };
897 assert_eq!(layout.start_brk_at_bias(0x555555556000), Ok(0x55555555c000));
898 }
899
900 #[test]
901 fn an_auxiliary_vector_entry_is_read_up_to_its_end() {
902 let auxv: Vec<u8> = [(3_u64, 0x400040_u64), (6, 4096), (0, 0), (9, 1)]
903 .iter()
904 .flat_map(|(key, value)| [key.to_ne_bytes(), value.to_ne_bytes()].concat())
905 .collect();
906 assert_eq!(auxv_entry(&auxv, 3), Some(0x400040));
907 assert_eq!(auxv_entry(&auxv, 6), Some(4096));
908 assert_eq!(auxv_entry(&auxv, 9), None);
909 assert_eq!(auxv_entry(&auxv[..20], 6), None);
910 }
911
912 #[test]
913 fn a_script_line_names_its_interpreter_as_the_kernel_reads_it() {
914 let interpreter = |path: &str| Script::Interpreter(PathBuf::from(path));
915 assert_eq!(parse_script(b"\x7fELF"), Script::Not);
916 assert_eq!(parse_script(b"#!/bin/sh\n"), interpreter("/bin/sh"));
917 assert_eq!(parse_script(b"#! \t/bin/sh -e\n"), interpreter("/bin/sh"));
918 assert_eq!(parse_script(b"#!/bin/sh\r\n"), interpreter("/bin/sh\r"));
920 assert_eq!(parse_script(b"#!/bin/sh -x"), interpreter("/bin/sh"));
921 assert_eq!(parse_script(b"#!/bin/sh"), interpreter("/bin/sh"));
923 let long = [b"#!/".as_slice(), &[b'a'; BINPRM_BUF_SIZE]].concat();
925 assert_eq!(parse_script(&long), Script::Declined);
926 assert_eq!(parse_script(b"#!\n"), Script::Declined);
927 assert_eq!(parse_script(b"#!\0/bin/sh\n"), Script::Declined);
928 }
929
930 #[test]
931 #[allow(clippy::disallowed_methods)] fn a_script_resolves_to_its_interpreter_and_a_link_to_its_file() {
933 let directory = tempfile::tempdir().unwrap();
934 let root = std::fs::canonicalize(directory.path()).unwrap();
935 let interpreter = std::env::current_exe().unwrap();
936 let interpreter = std::fs::canonicalize(&interpreter).unwrap();
937 let script = root.join("script");
938 fs::write(&script, format!("#!{}\n", interpreter.display())).unwrap();
939 fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).unwrap();
940 std::os::unix::fs::symlink("script", root.join("link")).unwrap();
941
942 let record = ExecRecord::resolve(Path::new("/"), &root, Path::new("link")).unwrap();
943 assert_eq!(record.exe, interpreter);
944 assert!(record.is_present(Path::new("/")));
945
946 let image = root.join("image");
948 fs::copy(&interpreter, &image).unwrap();
949 let record = ExecRecord::resolve(&root, Path::new("/"), Path::new("/image")).unwrap();
950 assert_eq!(record.exe, Path::new("/image"));
951 assert!(record.is_present(&root));
952
953 fs::remove_file(&image).unwrap();
955 assert!(!record.is_present(&root));
956 fs::copy(&interpreter, &image).unwrap();
957 assert!(!record.is_present(&root));
958
959 let image = root.join("image");
962 let record = ExecRecord::resolve(&root, Path::new("/"), Path::new("/image")).unwrap();
963 fs::hard_link(&image, root.join("alias")).unwrap();
964 fs::rename(&image, root.join("moved")).unwrap();
965 fs::hard_link(root.join("alias"), &image).unwrap();
966 assert!(!record.is_present(&root));
967
968 let image = root.join("relinked");
971 fs::copy(&interpreter, &image).unwrap();
972 fs::hard_link(&image, root.join("second")).unwrap();
973 let record = ExecRecord::at(&root, &image).unwrap();
974 fs::rename(&image, root.join("away")).unwrap();
975 fs::rename(root.join("second"), &image).unwrap();
976 assert_eq!(fs::metadata(&image).unwrap().nlink(), 2);
977 assert!(!record.is_present(&root));
978
979 let fifo = root.join("fifo");
983 let fifo_name = std::ffi::CString::new(fifo.as_os_str().as_bytes()).unwrap();
984 assert_eq!(unsafe { libc::mkfifo(fifo_name.as_ptr(), 0o755) }, 0);
985 assert_eq!(
986 ExecRecord::resolve(&root, Path::new("/"), Path::new("/fifo")),
987 None
988 );
989 assert_eq!(
990 ExecRecord::resolve(Path::new("/"), Path::new("/"), Path::new("/dev/null")),
991 None
992 );
993
994 assert_eq!(
995 ExecRecord::resolve(&root, Path::new("/"), Path::new("/missing")),
996 None
997 );
998 assert_eq!(script_interpreter(&root), Script::Unread);
1002 }
1003
1004 #[test]
1005 fn only_a_directory_samples_as_one() {
1006 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
1008 stat.st_dev = 7;
1009 stat.st_ino = 42;
1010 stat.st_mode = libc::S_IFDIR | 0o755;
1011 assert_eq!(directory_sample(&stat), Some((7, 42)));
1012 for kind in [
1016 libc::S_IFLNK,
1017 libc::S_IFREG,
1018 libc::S_IFIFO,
1019 libc::S_IFSOCK,
1020 libc::S_IFCHR,
1021 libc::S_IFBLK,
1022 ] {
1023 stat.st_mode = kind | 0o777;
1024 assert_eq!(directory_sample(&stat), None, "{kind:o}");
1025 }
1026 let directory = tempfile::tempdir().unwrap();
1028 let root = crate::util::canonicalize(directory.path()).unwrap();
1029 fs::create_dir(root.join("away")).unwrap();
1030 std::os::unix::fs::symlink("away", root.join("a")).unwrap();
1031 assert_eq!(directory_chain(&root, &root.join("a/image")), None);
1032 assert!(directory_chain(&root, &root.join("away/image")).is_some());
1033 }
1034
1035 #[test]
1036 fn a_renamed_directory_above_the_image_is_not_its_path() {
1037 let directory = tempfile::tempdir().unwrap();
1038 let root = crate::util::canonicalize(directory.path()).unwrap();
1039 fs::create_dir(root.join("a")).unwrap();
1040 fs::create_dir(root.join("b")).unwrap();
1041 let image = root.join("a/image");
1042 fs::write(&image, b"image").unwrap();
1043 fs::hard_link(&image, root.join("b/image")).unwrap();
1044 let record = ExecRecord::at(&root, &image).unwrap();
1045 assert_eq!(record.exe, Path::new("/a/image"));
1046 assert!(record.names_path_exactly(&root));
1047 fs::rename(root.join("a"), root.join("away")).unwrap();
1051 fs::rename(root.join("b"), root.join("a")).unwrap();
1052 assert!(record.is_present(&root));
1053 assert!(!record.names_path_exactly(&root));
1054 fs::rename(root.join("a"), root.join("b")).unwrap();
1056 fs::rename(root.join("away"), root.join("a")).unwrap();
1057 assert!(record.names_path_exactly(&root));
1058 fs::rename(root.join("a"), root.join("away")).unwrap();
1060 std::os::unix::fs::symlink("away", root.join("a")).unwrap();
1061 assert!(!record.names_path_exactly(&root));
1062 }
1063
1064 #[test]
1068 fn an_image_names_its_program_interpreter_as_the_kernel_reads_it() {
1069 let interpreter = program_interpreter(Path::new("/proc/self/exe"))
1070 .expect("the test binary is dynamically linked");
1071 assert!(interpreter.is_absolute(), "{}", interpreter.display());
1072 assert!(interpreter.exists(), "{}", interpreter.display());
1073 let directory = tempfile::tempdir().unwrap();
1074 let script = directory.path().join("script");
1075 fs::write(&script, "#!/bin/sh\n").unwrap();
1076 assert_eq!(program_interpreter(&script), None);
1077 }
1078}