1#![doc = include_str!("../README.md")]
11#![deny(clippy::all)]
12#![allow(clippy::uninlined_format_args)]
13#![cfg_attr(test, allow(clippy::disallowed_methods))]
18
19extern crate self as hermit;
21
22#[path = "bin/hermit/mod.rs"]
23pub mod app;
24
25#[cfg(all(test, feature = "native-exec"))]
27pub(crate) use app::exec_initial;
28
29#[cfg(feature = "liteinst")]
33reverie_liteinst::declare_legacy_tool_backing!();
34
35pub mod backend_stats;
36pub mod build_info;
37pub mod canonical_verdict;
38pub mod chaos_timeslice;
39mod chroot;
40pub mod compatibility;
41use hermit_record_format::consts;
42pub mod logdiff_report;
43pub mod cpu_profile_admission;
45mod error;
46use hermit_record_format::event;
47use hermit_record_format::event_stream;
48pub mod gdbserver;
49pub mod happens_before;
50pub mod host_input_change;
51mod id;
52#[cfg(feature = "liteinst")]
53pub mod in_guest_exits;
54pub mod instruction_map;
55pub mod kvm_address_space;
56#[cfg(feature = "liteinst")]
57use hermit_record_format::interp;
58pub mod liteinst_bootstrap;
59mod liteinst_constructor;
60pub mod liteinst_record;
61mod metadata;
62#[cfg(target_arch = "x86_64")]
63pub mod xstate_probe;
64pub use detcore::nonwaiting_write;
67#[cfg(test)]
68mod chosen_input_tests;
69pub mod physical_exit_watch;
70pub mod preemption_admission;
71pub mod proc_mount;
72
73#[cfg(test)]
83pub(crate) fn run_in_fresh_process(name: &str) -> bool {
84 const INNER: &str = "HERMIT_LIB_TEST_FRESH_PROCESS";
85 if std::env::var_os(INNER).is_some_and(|value| value == name) {
86 return false;
87 }
88 let output = std::process::Command::new(std::env::current_exe().unwrap())
89 .args(["--exact", name, "--nocapture", "--test-threads=1"])
90 .env(INNER, name)
91 .output()
92 .unwrap();
93 let stdout = String::from_utf8_lossy(&output.stdout);
94 let stderr = String::from_utf8_lossy(&output.stderr);
95 assert!(
96 output.status.success(),
97 "inner run of {name} failed:\n{stdout}\n{stderr}"
98 );
99 assert!(
100 stdout.contains("1 passed"),
101 "inner run of {name} did not execute the test:\n{stdout}\n{stderr}"
102 );
103 true
104}
105mod ptrace_completion;
106#[cfg(test)]
107mod replay_departure_completion_tests;
108#[cfg(test)]
109mod replay_departure_test_fixture;
110mod timeout_survivors;
111
112pub use canonical_verdict::Verdict;
113
114pub const RECORD_REPLAY_HASHES_IO_BUFFERS: bool = true;
127
128pub const RECORD_REPLAY_VIRTUALIZES_TIME: bool = false;
143
144pub const HERMIT_VERIFICATION_DIVERGENCE_EXIT: i32 = 1;
150
151pub const HERMIT_INTERNAL_FAILURE_EXIT: i32 = 125;
205
206pub const HERMIT_DEADLINE_EXIT: i32 = 124;
223
224const _: () = assert!(
228 detcore_model::TerminalOutcome::VirtualTimeout { bound_ns: 1 }.exit_status()
229 == HERMIT_DEADLINE_EXIT,
230 "a virtual timeout exits the deadline status"
231);
232
233pub const HERMIT_LOG_CAP_EXIT: i32 = 123;
247
248pub const GUEST_PROGRAM_NOT_FOUND_EXIT: i32 = 127;
261
262pub const GUEST_PROGRAM_NOT_EXECUTABLE_EXIT: i32 = 126;
268
269const _: () = assert!(
415 HERMIT_VERIFICATION_DIVERGENCE_EXIT == 1,
416 "1 is the established diff-style result for a completed comparison that diverged"
417);
418const _: () = assert!(
419 HERMIT_INTERNAL_FAILURE_EXIT == 125,
420 "125 is the GNU wrapper-failed convention; 1 is the commonest guest status and 0 is success"
421);
422const _: () = assert!(
423 GUEST_PROGRAM_NOT_FOUND_EXIT == 127,
424 "127 is the GNU command-not-found convention and must stay distinct from 125 and 126"
425);
426const _: () = assert!(
427 GUEST_PROGRAM_NOT_EXECUTABLE_EXIT == 126,
428 "126 is the GNU found-but-not-executable convention and must stay distinct from 125 and 127"
429);
430const _: () = assert!(
431 HERMIT_LOG_CAP_EXIT == 123,
432 "123 is Hermit's log-cap code, reserved for bin/safehermit's log-cap kill as well; see the allocation table"
433);
434const _: () = assert!(
435 HERMIT_LOG_CAP_EXIT != 0
436 && HERMIT_VERIFICATION_DIVERGENCE_EXIT != 0
437 && HERMIT_INTERNAL_FAILURE_EXIT != 0
438 && GUEST_PROGRAM_NOT_FOUND_EXIT != 0
439 && GUEST_PROGRAM_NOT_EXECUTABLE_EXIT != 0,
440 "a failure code of 0 would invert every assert_eq!(code, Some(CONST)) into demanding success"
441);
442mod record;
443use hermit_record_format::record_replay_path;
444mod replay;
445
446use hermit_record_format::script;
447mod tracer_thread;
448
449use std::fs;
450use std::io;
451use std::io::Seek;
452use std::io::SeekFrom;
453use std::io::Write;
454use std::os::fd::AsRawFd;
455use std::os::fd::FromRawFd;
456#[cfg(all(test, feature = "liteinst"))]
457use std::os::unix::ffi::OsStrExt;
458#[cfg(feature = "liteinst")]
459use std::os::unix::fs::PermissionsExt;
460use std::path::Path;
461use std::path::PathBuf;
462use std::sync::Mutex;
463use std::time::Duration;
464#[cfg(target_arch = "x86_64")]
465use std::time::Instant;
466
467use anyhow::anyhow;
468use clap::ValueEnum;
469pub use consts::LAST;
470pub use consts::LAST_RUN;
471use consts::MANIFEST_NAME as RUN_MANIFEST_NAME;
472pub use consts::MANIFEST_NAME;
473use consts::METADATA_NAME;
474pub use consts::METADATA_NAME as RECORDING_METADATA_NAME;
475pub use detcore::Config as DetConfig;
476pub use detcore::Detcore;
477pub use detcore::RecordOrReplay;
478#[doc(hidden)]
479#[doc(hidden)]
480#[doc(hidden)]
481#[doc(hidden)]
482#[doc(hidden)]
483#[doc(hidden)]
484#[doc(hidden)]
485#[doc(hidden)]
486pub use error::Context;
487pub use error::Error;
488pub use error::FailureKind;
489pub use error::SerializableError;
490use goblin::elf::Elf;
491use goblin::elf::header;
492use goblin::elf::section_header;
493pub use id::Id;
494use metadata::Metadata;
495pub use metadata::RecordOptions;
496pub use metadata::RecordedSchedule;
497pub use ptrace_completion::HermitCleanupStage;
498pub use ptrace_completion::HermitCleanupUnconfirmed;
499pub use ptrace_completion::HermitPtraceFailure;
500pub use ptrace_completion::RecoveryRefusal;
501use record::Record;
502use replay::Replay;
503pub use reverie::ExitStatus;
504pub use reverie::process;
505pub use reverie::process::Command;
506pub use reverie::process::Mount;
507pub use reverie::process::Namespace;
508pub use reverie::process::Output;
509pub use reverie::process::Stdio;
510pub use script::Shebang;
511use serde::Deserialize;
512use serde::Serialize;
513pub use tracer_thread::launch_on_calling_thread;
514
515enum KvmStdinReservation {
516 Open(
517 #[cfg_attr(
518 not(target_arch = "x86_64"),
519 allow(
520 dead_code,
521 reason = "only the x86_64 KVM backend reads the reserved stdin"
522 )
523 )]
524 fs::File,
525 ),
526 Closed,
527}
528
529static KVM_STDIN_RESERVATION: Mutex<Option<KvmStdinReservation>> = Mutex::new(None);
530
531pub fn reserve_kvm_stdin(stdin: Option<fs::File>) -> io::Result<()> {
533 let mut reservation = KVM_STDIN_RESERVATION
534 .lock()
535 .map_err(|_| io::Error::other("KVM stdin reservation lock is poisoned"))?;
536 if reservation.is_some() {
537 return Err(io::Error::new(
538 io::ErrorKind::AlreadyExists,
539 "KVM stdin is already reserved",
540 ));
541 }
542 *reservation = Some(match stdin {
543 Some(file) => KvmStdinReservation::Open(file),
544 None => KvmStdinReservation::Closed,
545 });
546 Ok(())
547}
548
549fn duplicate_current_stdin() -> io::Result<Option<fs::File>> {
550 let duplicate = unsafe { libc::fcntl(libc::STDIN_FILENO, libc::F_DUPFD_CLOEXEC, 3) };
552 if duplicate >= 0 {
553 return Ok(Some(unsafe { fs::File::from_raw_fd(duplicate) }));
555 }
556 let error = io::Error::last_os_error();
557 if error.raw_os_error() == Some(libc::EBADF) {
558 Ok(None)
559 } else {
560 Err(error)
561 }
562}
563
564#[cfg_attr(
573 not(target_arch = "x86_64"),
574 allow(
575 dead_code,
576 reason = "only the x86_64 KVM launch groups the standard streams"
577 )
578)]
579fn standard_stream_aliases(fds: [Option<std::os::fd::RawFd>; 3]) -> Option<[u8; 3]> {
580 const KCMP_FILE: libc::c_long = 0;
581 let pid = libc::c_long::from(unsafe { libc::getpid() });
583 let mut aliases = [0u8, 1, 2];
584 for later in 1..fds.len() {
585 let Some(b) = fds[later] else {
586 continue;
587 };
588 for earlier in 0..later {
589 let Some(a) = fds[earlier] else {
590 continue;
591 };
592 let answer = unsafe {
594 libc::syscall(
595 libc::SYS_kcmp,
596 pid,
597 pid,
598 KCMP_FILE,
599 libc::c_long::from(a),
600 libc::c_long::from(b),
601 )
602 };
603 match answer {
604 0 => {
605 aliases[later] = aliases[earlier];
606 break;
607 }
608 1..=3 => {}
609 _ => return None,
610 }
611 }
612 }
613 Some(aliases)
614}
615
616fn ensure_kvm_stdin_reserved() -> io::Result<()> {
617 let mut reservation = KVM_STDIN_RESERVATION
618 .lock()
619 .map_err(|_| io::Error::other("KVM stdin reservation lock is poisoned"))?;
620 if reservation.is_none() {
621 *reservation = Some(match duplicate_current_stdin()? {
622 Some(file) => KvmStdinReservation::Open(file),
623 None => KvmStdinReservation::Closed,
624 });
625 }
626 Ok(())
627}
628
629#[cfg(target_arch = "x86_64")]
630fn reserved_kvm_stdin() -> Result<Option<fs::File>, Error> {
631 ensure_kvm_stdin_reserved()?;
632 let reservation = KVM_STDIN_RESERVATION
633 .lock()
634 .map_err(|_| io::Error::other("KVM stdin reservation lock is poisoned"))?;
635 match reservation.as_ref() {
636 Some(KvmStdinReservation::Open(file)) => Ok(Some(file.try_clone()?)),
637 Some(KvmStdinReservation::Closed) => Ok(None),
638 None => unreachable!("stdin reservation was initialized above"),
639 }
640}
641
642enum StdinSnapshot {
654 Seekable(fs::File),
655 None,
656}
657
658static OUTPUT_STDIN_SNAPSHOT: Mutex<Option<StdinSnapshot>> = Mutex::new(None);
659
660pub fn reserve_output_stdin_snapshot(stdin: Option<fs::File>) -> io::Result<()> {
670 let snapshot = match stdin {
671 None => StdinSnapshot::None,
672 Some(mut file) => {
673 let is_tty = unsafe { libc::isatty(file.as_raw_fd()) } == 1;
675 if is_tty {
676 StdinSnapshot::None
677 } else {
678 let replay = if file.stream_position().is_ok() {
679 file
682 } else {
683 const BUFFERING_NOTE: &str = "hermit: --verify is buffering stdin from a non-seekable stream so both \
706 runs receive identical input. If this appears to hang, stdin has not \
707 reached end-of-file; pass `< /dev/null` when the guest needs no input.";
708 if nonwaiting_write::diagnostics_must_not_wait() {
709 nonwaiting_write::write_without_waiting(
712 libc::STDERR_FILENO,
713 format!("{BUFFERING_NOTE}\n").as_bytes(),
714 );
715 } else {
716 eprintln!("{BUFFERING_NOTE}");
717 }
718 let mut buffered = tempfile::tempfile()?;
719 io::copy(&mut file, &mut buffered)?;
720 buffered.seek(SeekFrom::Start(0))?;
721 buffered
722 };
723
724 let replay_path = format!("/proc/self/fd/{}", replay.as_raw_fd());
728 StdinSnapshot::Seekable(fs::File::open(replay_path)?)
729 }
730 }
731 };
732 let mut reservation = OUTPUT_STDIN_SNAPSHOT
733 .lock()
734 .map_err(|_| io::Error::other("output stdin snapshot lock is poisoned"))?;
735 if reservation.is_some() {
736 return Err(io::Error::new(
737 io::ErrorKind::AlreadyExists,
738 "output stdin snapshot is already reserved",
739 ));
740 }
741 *reservation = Some(snapshot);
742 Ok(())
743}
744
745fn output_backend_stdin_reservation() -> Result<(bool, Option<fs::File>), Error> {
754 let mut reservation = OUTPUT_STDIN_SNAPSHOT
755 .lock()
756 .map_err(|_| io::Error::other("output stdin snapshot lock is poisoned"))?;
757 match reservation.as_mut() {
758 Some(StdinSnapshot::Seekable(file)) => {
759 file.seek(SeekFrom::Start(0))?;
760 Ok((true, Some(file.try_clone()?)))
761 }
762 Some(StdinSnapshot::None) => Ok((true, None)),
763 None => Ok((false, None)),
764 }
765}
766
767fn output_backend_stdin_file() -> Result<Option<fs::File>, Error> {
768 Ok(output_backend_stdin_reservation()?.1)
769}
770
771pub fn output_backend_stdin() -> Result<Stdio, Error> {
779 match output_backend_stdin_file()? {
780 Some(file) => Ok(Stdio::from(file)),
781 None => Ok(Stdio::null()),
782 }
783}
784
785#[derive(Debug, Serialize, Deserialize)]
787pub struct Recording {
788 pub id: Id,
790
791 pub exit_status: ExitStatus,
793}
794
795#[derive(Clone, Copy, Debug, PartialEq, Eq)]
796enum CapabilityProbe {
797 Namespaces,
798 Ptrace,
799 Seccomp,
800}
801
802fn run_capability_probe(probe: CapabilityProbe) -> Result<bool, Error> {
803 let pid = unsafe { libc::fork() };
805 if pid == -1 {
806 return Err(std::io::Error::last_os_error()).context("Failed to fork capability probe");
807 }
808 if pid == 0 {
809 let supported = match probe {
810 CapabilityProbe::Namespaces => unsafe {
811 libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWPID | libc::CLONE_NEWIPC) == 0
812 },
813 CapabilityProbe::Ptrace => {
814 unsafe {
816 libc::ptrace(
817 libc::PTRACE_TRACEME,
818 0,
819 std::ptr::null_mut::<libc::c_void>(),
820 std::ptr::null_mut::<libc::c_void>(),
821 ) != -1
822 }
823 }
824 CapabilityProbe::Seccomp => {
825 let mut filter = libc::sock_filter {
826 code: 0x06, jt: 0,
828 jf: 0,
829 k: 0x7fff0000, };
831 let program = libc::sock_fprog {
832 len: 1,
833 filter: &mut filter,
834 };
835 unsafe {
837 libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == 0
838 && libc::syscall(
839 libc::SYS_seccomp,
840 1, 0,
842 &program,
843 ) == 0
844 }
845 }
846 };
847 unsafe { libc::_exit(i32::from(!supported)) }
849 }
850
851 let mut status = 0;
852 loop {
853 let result = unsafe { libc::waitpid(pid, &mut status, 0) };
855 if result == pid {
856 return Ok(libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0);
857 }
858 if result == -1 {
859 let error = std::io::Error::last_os_error();
860 if error.kind() == std::io::ErrorKind::Interrupted {
861 continue;
862 }
863 return Err(error).context("Failed to wait for capability probe");
864 }
865 }
866}
867
868#[derive(Clone, Copy, Debug, PartialEq, Eq)]
873pub enum NamespaceUse {
874 Creates,
876 None,
878}
879
880pub fn validate_tracing_environment(namespaces: NamespaceUse) -> Result<(), Error> {
883 check_tracing_environment(namespaces, run_capability_probe)
884}
885
886fn check_tracing_environment(
890 namespaces: NamespaceUse,
891 mut probe: impl FnMut(CapabilityProbe) -> Result<bool, Error>,
892) -> Result<(), Error> {
893 if namespaces == NamespaceUse::Creates && !probe(CapabilityProbe::Namespaces)? {
894 anyhow::bail!(
895 "Hermit cannot create its required user, PID and IPC namespaces: \
896 unshare(CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWIPC) was denied. Allow unprivileged \
897 user namespaces, IPC namespaces (user.max_ipc_namespaces) and the unshare syscall in \
898 the host/container policy."
899 );
900 }
901 if !probe(CapabilityProbe::Ptrace)? {
902 anyhow::bail!(
903 "Hermit cannot use ptrace in this environment: a child PTRACE_TRACEME probe was \
904 denied. Allow same-UID parent-child ptrace in the container seccomp and host \
905 Yama/LSM policy; CAP_SYS_PTRACE is normally not required. Use --namespace-only for \
906 a sandbox smoke test without ptrace (it intercepts only kill, through a seccomp \
907 filter)."
908 );
909 }
910 if !probe(CapabilityProbe::Seccomp)? {
911 anyhow::bail!(
912 "Hermit cannot install its tracee seccomp filter: \
913 seccomp(SECCOMP_SET_MODE_FILTER) was denied. Allow seccomp and \
914 prctl(PR_SET_NO_NEW_PRIVS) in the container policy. --namespace-only also \
915 installs a seccomp filter (on kill), so it cannot run here either."
916 );
917 }
918 Ok(())
919}
920
921fn validate_preload_constructor_library(
930 path: &Path,
931 label: &str,
932 required: &[&str],
933 initializer_name: &str,
934) -> io::Result<PathBuf> {
935 let bytes = fs::read(path).map_err(|error| {
936 io::Error::new(
937 error.kind(),
938 format!("failed to read {label} {}: {error}", path.display()),
939 )
940 })?;
941 let elf = Elf::parse(&bytes).map_err(|error| {
942 io::Error::new(
943 io::ErrorKind::InvalidData,
944 format!("{label} {} is not an ELF DSO: {error}", path.display()),
945 )
946 })?;
947 if elf.header.e_type != header::ET_DYN || elf.header.e_machine != header::EM_X86_64 {
948 return Err(io::Error::new(
949 io::ErrorKind::InvalidData,
950 format!("{label} {} is not an x86-64 shared object", path.display()),
951 ));
952 }
953
954 for &name in required {
955 if !elf
956 .dynsyms
957 .iter()
958 .any(|symbol| elf.dynstrtab.get_at(symbol.st_name) == Some(name))
959 {
960 return Err(io::Error::new(
961 io::ErrorKind::InvalidData,
962 format!(
963 "{label} {} is missing required export {name}",
964 path.display()
965 ),
966 ));
967 }
968 }
969 if initializer_name == liteinst_constructor::ROOT {
970 liteinst_constructor::validate(&bytes, &elf).map_err(|error| {
971 io::Error::new(
972 error.kind(),
973 format!(
974 "{label} {} has invalid Detcore constructor binding: {error}",
975 path.display()
976 ),
977 )
978 })?;
979 return path.canonicalize();
980 }
981 let (initializer_index, initializer) = elf
982 .dynsyms
983 .iter()
984 .enumerate()
985 .find(|(_, symbol)| elf.dynstrtab.get_at(symbol.st_name) == Some(initializer_name))
986 .ok_or_else(|| {
987 io::Error::other(format!(
988 "checked {label} initializer {initializer_name} disappeared"
989 ))
990 })?;
991 let init_array = elf
992 .section_headers
993 .iter()
994 .find(|section| section.sh_type == section_header::SHT_INIT_ARRAY)
995 .ok_or_else(|| {
996 io::Error::new(
997 io::ErrorKind::InvalidData,
998 format!("{label} {} has no constructor array", path.display()),
999 )
1000 })?;
1001 let init_start = init_array.sh_addr;
1002 let init_end = init_start.saturating_add(init_array.sh_size);
1003 let relocated_initializer = elf
1004 .dynrelas
1005 .iter()
1006 .chain(elf.dynrels.iter())
1007 .any(|relocation| {
1008 (init_start..init_end).contains(&relocation.r_offset)
1009 && relocation.r_sym == initializer_index
1010 });
1011 let init_bytes = usize::try_from(init_array.sh_offset)
1012 .ok()
1013 .and_then(|start| {
1014 usize::try_from(init_array.sh_size)
1015 .ok()
1016 .and_then(|size| bytes.get(start..start.checked_add(size)?))
1017 })
1018 .unwrap_or_default();
1019 let direct_initializer = init_bytes
1020 .as_chunks::<8>()
1021 .0
1022 .iter()
1023 .any(|entry| u64::from_le_bytes(*entry) == initializer.st_value);
1024 if !relocated_initializer && !direct_initializer {
1025 return Err(io::Error::new(
1026 io::ErrorKind::InvalidData,
1027 format!(
1028 "{label} {} does not register {initializer_name} as a preload constructor",
1029 path.display()
1030 ),
1031 ));
1032 }
1033 path.canonicalize()
1034}
1035
1036pub const LITEINST_TOOL_RUNTIME_ENV: &str = "HERMIT_LITEINST_TOOL_RUNTIME";
1039
1040#[doc(hidden)]
1055pub fn liteinst_tool_runtime_library_path() -> io::Result<PathBuf> {
1056 if let Some(path) = std::env::var_os(LITEINST_TOOL_RUNTIME_ENV) {
1057 let path = PathBuf::from(path);
1058 if !path.is_file() {
1059 return Err(io::Error::new(
1060 io::ErrorKind::NotFound,
1061 format!("{LITEINST_TOOL_RUNTIME_ENV} does not name a regular file"),
1062 ));
1063 }
1064 return validate_liteinst_tool_runtime_library(&path).map_err(|error| {
1065 io::Error::new(
1066 error.kind(),
1067 format!("{LITEINST_TOOL_RUNTIME_ENV} is invalid: {error}"),
1068 )
1069 });
1070 }
1071
1072 if let Some(path) = hermit_resources::resource("libdetcore_liteinst.so")? {
1073 if !path.is_file() {
1074 return Err(io::Error::new(
1075 io::ErrorKind::NotFound,
1076 "selected Hermit installation has no finalized in-guest Detcore runtime; \
1077 run `make install-deps` to build and stage the qualified LiteInst runtime",
1078 ));
1079 }
1080 return validate_liteinst_tool_runtime_library(&path);
1081 }
1082
1083 let executable = std::env::current_exe()?;
1084 let directory = executable.parent().ok_or_else(|| {
1085 io::Error::new(
1086 io::ErrorKind::NotFound,
1087 "Hermit executable has no parent directory",
1088 )
1089 })?;
1090 if let Some(path) = [
1091 directory.join("libdetcore_liteinst.so"),
1092 directory.join("deps/libdetcore_liteinst.so"),
1093 ]
1094 .into_iter()
1095 .find(|path| path.is_file())
1096 {
1097 return validate_liteinst_tool_runtime_library(&path);
1098 }
1099 Err(io::Error::new(
1100 io::ErrorKind::NotFound,
1101 format!(
1102 "libdetcore_liteinst.so was not built beside {} or staged as an installed resource",
1103 executable.display()
1104 ),
1105 ))
1106}
1107
1108fn validate_liteinst_tool_runtime_library(path: &Path) -> io::Result<PathBuf> {
1109 validate_preload_constructor_library(
1110 path,
1111 "in-guest Detcore runtime",
1112 &["detcore_liteinst_initialize"],
1113 "detcore_liteinst_initialize",
1114 )
1115}
1116
1117#[derive(Debug, Clone, PartialEq, Eq)]
1125pub struct LiteinstInGuestRefusal {
1126 backend: Backend,
1127 reason: String,
1128}
1129
1130impl LiteinstInGuestRefusal {
1131 pub fn new(reason: impl Into<String>) -> Self {
1133 Self::for_backend(Backend::Liteinst, reason)
1134 }
1135
1136 pub fn for_backend(backend: Backend, reason: impl Into<String>) -> Self {
1140 debug_assert!(backend.is_in_guest_liteinst(), "{backend:?}");
1141 Self {
1142 backend,
1143 reason: reason.into(),
1144 }
1145 }
1146}
1147
1148impl std::fmt::Display for LiteinstInGuestRefusal {
1149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1150 let runtime = match self.backend {
1151 Backend::InGuestTrap => "in-guest LiteInst, site patching off",
1152 _ => "in-guest LiteInst",
1153 };
1154 write!(
1155 f,
1156 "--backend={} ({runtime}) refuses this run: {}",
1157 self.backend.as_str(),
1158 self.reason
1159 )
1160 }
1161}
1162
1163impl std::error::Error for LiteinstInGuestRefusal {}
1164
1165#[cfg(feature = "liteinst")]
1187fn refuse_in_guest_liteinst_run(
1188 command: &mut Command,
1189 config: &DetConfig,
1190 backend: Backend,
1191) -> Result<(), Error> {
1192 let reason = if let Some(conflict) = in_guest_trap_site_patching_conflict(command, backend) {
1193 Some(conflict)
1194 } else if config.max_timeslice.is_some() {
1195 Some(
1196 "it cannot deliver Detcore's preemption timer yet (the in-guest Tool host refuses \
1197 set_timer with ENOSYS), so the run would fail at its first timeslice; pass \
1198 --max-timeslice=disabled"
1199 .to_owned(),
1200 )
1201 } else if let Some(gap) = in_guest_liteinst_kernel_gap(&running_kernel_release()) {
1202 Some(gap)
1203 } else {
1204 in_guest_liteinst_program_gap(command)?
1205 };
1206 match reason {
1207 Some(reason) => Err(Error::new(LiteinstInGuestRefusal::for_backend(
1208 backend, reason,
1209 ))),
1210 None => {
1211 force_in_guest_site_patching(command, backend);
1212 Ok(())
1213 }
1214 }
1215}
1216
1217pub fn forced_site_patching_value(backend: Backend) -> Option<&'static str> {
1223 match backend {
1224 Backend::InGuestTrap => Some("0"),
1225 _ => None,
1226 }
1227}
1228
1229pub const fn in_guest_guard_router_installed(config: &DetConfig, backend: Backend) -> bool {
1250 backend.is_in_guest_liteinst() && !config.in_guest_site_patching_off
1251}
1252
1253pub fn in_guest_reserved_signals(config: &DetConfig, backend: Backend) -> u64 {
1267 if !backend.is_in_guest_liteinst() {
1268 return 0;
1269 }
1270 let bit = |signal: libc::c_int| 1_u64 << (signal - 1);
1271 let subscription = <Detcore as reverie::Tool>::subscriptions(config);
1272 let mut reserved = bit(libc::SIGSYS);
1273 if subscription.has_rdtsc() || subscription.has_cpuid() {
1274 reserved |= bit(libc::SIGSEGV);
1275 }
1276 if in_guest_guard_router_installed(config, backend) {
1277 reserved |= bit(libc::SIGTRAP);
1278 }
1279 reserved
1280}
1281
1282pub const fn in_guest_refuses_host_timed_producers(backend: Backend) -> bool {
1288 backend.is_in_guest_liteinst()
1289}
1290
1291pub fn scheduler_owns_guest_pid_namespace() -> bool {
1312 unsafe { libc::getpid() == 1 }
1314}
1315
1316#[cfg(feature = "liteinst")]
1321const IN_GUEST_RUNTIME_DELIVERS_SIGNALS: bool = false;
1322
1323#[cfg(all(test, feature = "liteinst"))]
1328static FORCE_IN_GUEST_DELIVERY: std::sync::atomic::AtomicBool =
1329 std::sync::atomic::AtomicBool::new(false);
1330
1331#[cfg(feature = "liteinst")]
1335fn runtime_delivers_signals() -> bool {
1336 #[cfg(test)]
1337 if FORCE_IN_GUEST_DELIVERY.load(std::sync::atomic::Ordering::Acquire) {
1338 return true;
1339 }
1340 IN_GUEST_RUNTIME_DELIVERS_SIGNALS
1341}
1342
1343#[cfg(all(test, feature = "liteinst"))]
1351static GATED_LAUNCHES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
1352
1353#[cfg(all(test, feature = "liteinst"))]
1357const GATED_LAUNCH_WITNESS: &str = "IN-GUEST-DELIVERY-GATE-ON";
1358
1359#[cfg(feature = "liteinst")]
1362fn record_delivery_gate(gate: bool) -> bool {
1363 #[cfg(test)]
1364 if gate {
1365 GATED_LAUNCHES.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
1366 println!("{GATED_LAUNCH_WITNESS}");
1367 }
1368 gate
1369}
1370
1371pub fn in_guest_signal_delivery_gate(
1383 config: &DetConfig,
1384 backend: Backend,
1385 owns_guest_pid_namespace: bool,
1386) -> bool {
1387 backend.is_in_guest_liteinst()
1388 && config.in_guest_site_patching_off
1389 && config.sequentialize_threads
1390 && detcore::two_phase_refusal(config).is_none()
1391 && owns_guest_pid_namespace
1392}
1393
1394#[cfg(feature = "liteinst")]
1397fn rlimit_seconds(value: libc::rlim_t) -> String {
1398 if value == libc::RLIM_INFINITY {
1399 "unlimited".to_owned()
1400 } else {
1401 format!("{value}s")
1402 }
1403}
1404
1405#[cfg(feature = "liteinst")]
1409fn rlimit_cpu_report_line(old: libc::rlimit) -> String {
1410 if old.rlim_cur == libc::RLIM_INFINITY && old.rlim_max == libc::RLIM_INFINITY {
1411 format!(
1412 "HERMIT_IN_GUEST_RLIMIT_CPU: the guest's physical RLIMIT_CPU was soft {} hard {}, the \
1413 value Detcore reports; the guest starts with it unchanged (A5 step 3c, D25)",
1414 rlimit_seconds(old.rlim_cur),
1415 rlimit_seconds(old.rlim_max),
1416 )
1417 } else {
1418 format!(
1419 "HERMIT_IN_GUEST_RLIMIT_CPU: the guest's physical RLIMIT_CPU was soft {} hard {}; \
1420 Hermit sets it to soft unlimited hard unlimited as the guest starts, the value \
1421 Detcore reports, so the kernel never sends SIGXCPU at a moment host CPU time decides \
1422 (A5 step 3c, D25)",
1423 rlimit_seconds(old.rlim_cur),
1424 rlimit_seconds(old.rlim_max),
1425 )
1426 }
1427}
1428
1429#[cfg(feature = "liteinst")]
1436fn rlimit_cpu_refusal(old: libc::rlimit, errno: nix::errno::Errno) -> String {
1437 format!(
1438 "the guest would inherit a finite physical RLIMIT_CPU (soft {}, hard {}), and Hermit \
1439 cannot raise it ({errno}), so the kernel would send SIGXCPU or SIGKILL at a moment host \
1440 CPU time decides; run Hermit with an unlimited hard CPU-time limit \
1441 (A5 step 3c, D25, https://github.com/rrnewton/hermit/issues/4485)",
1442 rlimit_seconds(old.rlim_cur),
1443 rlimit_seconds(old.rlim_max),
1444 )
1445}
1446
1447#[cfg(feature = "liteinst")]
1456fn inherited_producer_refusal(
1457 fd: std::os::fd::RawFd,
1458 state: detcore::host_timed_producers::ProducerState,
1459 lease: bool,
1460) -> Option<String> {
1461 let what = if lease {
1462 "it holds a lease, whose owner Linux sends SIGIO when another process opens the file"
1463 } else if state.is_complete() && state.o_async {
1464 "it has O_ASYNC and an owner, which Linux sends SIGIO when it becomes ready"
1465 } else if state.is_complete() {
1466 "it is a socket with an owner, which Linux sends SIGURG when out-of-band data arrives"
1467 } else if state.owner {
1468 "it has an owner, which a directory notification (F_NOTIFY) on it would make Linux send \
1469 SIGIO for the directory's changes, and Hermit cannot tell whether one is set"
1470 } else {
1471 return None;
1472 };
1473 Some(format!(
1474 "the guest would inherit descriptor {fd}, which is already a signal producer: {what}, at \
1475 a moment host timing decides; close it, or clear its owner and O_ASYNC, before running \
1476 Hermit (A5 step 3c, D25, https://github.com/rrnewton/hermit/issues/4485)"
1477 ))
1478}
1479
1480#[cfg(feature = "liteinst")]
1483fn unreadable_descriptor_refusal(fd: std::os::fd::RawFd, errno: nix::errno::Errno) -> String {
1484 format!(
1485 "the guest would inherit descriptor {fd}, whose signal owner and flags cannot be read \
1486 ({errno}), so Hermit cannot tell whether it is a signal producer \
1487 (A5 step 3c, D25, https://github.com/rrnewton/hermit/issues/4485)"
1488 )
1489}
1490
1491#[cfg(feature = "liteinst")]
1494fn unlisted_descriptors_refusal(errno: nix::errno::Errno) -> String {
1495 format!(
1496 "the guest's descriptor table could not be listed from /proc/self/fd ({errno}), so \
1497 Hermit cannot tell whether a descriptor it would inherit is a signal producer \
1498 (A5 step 3c, D25, https://github.com/rrnewton/hermit/issues/4485)"
1499 )
1500}
1501
1502#[cfg(feature = "liteinst")]
1523struct LaunchAudit {
1524 pipe: std::sync::Arc<LaunchPipe>,
1525}
1526
1527#[cfg(feature = "liteinst")]
1529struct LaunchPipe {
1530 read: std::os::fd::OwnedFd,
1531 _write: std::os::fd::OwnedFd,
1532 report: std::sync::OnceLock<Option<LaunchReport>>,
1533}
1534
1535#[cfg(feature = "liteinst")]
1536impl LaunchPipe {
1537 fn read_report(&self) -> Option<LaunchReport> {
1539 let mut bytes = [0u8; LaunchReport::SIZE];
1540 let read = nix::unistd::read(&self.read, &mut bytes).ok()?;
1541 (read == bytes.len()).then(|| LaunchReport::decode(bytes))
1542 }
1543
1544 fn report(&self) -> Option<LaunchReport> {
1546 *self.report.get_or_init(|| self.read_report())
1547 }
1548
1549 fn log_rlimit(&self) {
1552 match self.report() {
1553 Some(report) if report.kind == audit_bits::PASSED => {
1554 tracing::info!("{}", rlimit_cpu_report_line(report.limit()));
1555 }
1556 Some(_) => {}
1558 None => tracing::warn!(
1559 "HERMIT_IN_GUEST_RLIMIT_CPU: the guest's launch check reported nothing \
1560 (A5 step 3c, D25)"
1561 ),
1562 }
1563 }
1564}
1565
1566#[cfg(feature = "liteinst")]
1568mod audit_bits {
1569 pub const PASSED: u8 = 0;
1571 pub const PRODUCER: u8 = 1;
1573 pub const UNREADABLE: u8 = 2;
1575 pub const UNLISTED: u8 = 3;
1577 pub const RLIMIT_CPU: u8 = 4;
1579
1580 pub const O_ASYNC: u8 = 1;
1581 pub const OWNER: u8 = 2;
1582 pub const SOCKET: u8 = 4;
1583 pub const LEASE: u8 = 8;
1584}
1585
1586#[cfg(feature = "liteinst")]
1590#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1591struct LaunchReport {
1592 kind: u8,
1593 bits: u8,
1594 errno: u8,
1595 fd: libc::c_int,
1596 rlimit: (libc::rlim_t, libc::rlim_t),
1597}
1598
1599#[cfg(feature = "liteinst")]
1600impl LaunchReport {
1601 const SIZE: usize = 24;
1602
1603 fn encode(self) -> [u8; Self::SIZE] {
1604 let mut bytes = [0u8; Self::SIZE];
1605 bytes[0] = self.kind;
1606 bytes[1] = self.bits;
1607 bytes[2] = self.errno;
1608 bytes[4..8].copy_from_slice(&self.fd.to_ne_bytes());
1609 bytes[8..16].copy_from_slice(&self.rlimit.0.to_ne_bytes());
1610 bytes[16..24].copy_from_slice(&self.rlimit.1.to_ne_bytes());
1611 bytes
1612 }
1613
1614 fn decode(bytes: [u8; Self::SIZE]) -> Self {
1615 let word = |range: std::ops::Range<usize>| {
1616 libc::rlim_t::from_ne_bytes(bytes[range].try_into().expect("eight bytes"))
1617 };
1618 Self {
1619 kind: bytes[0],
1620 bits: bytes[1],
1621 errno: bytes[2],
1622 fd: libc::c_int::from_ne_bytes(bytes[4..8].try_into().expect("four bytes")),
1623 rlimit: (word(8..16), word(16..24)),
1624 }
1625 }
1626
1627 fn errno(self) -> nix::errno::Errno {
1628 nix::errno::Errno::from_raw(self.errno.into())
1629 }
1630
1631 fn limit(self) -> libc::rlimit {
1632 libc::rlimit {
1633 rlim_cur: self.rlimit.0,
1634 rlim_max: self.rlimit.1,
1635 }
1636 }
1637
1638 fn refusal(self) -> Option<String> {
1640 match self.kind {
1641 audit_bits::PRODUCER => {
1642 let state = detcore::host_timed_producers::ProducerState {
1643 o_async: self.bits & audit_bits::O_ASYNC != 0,
1644 owner: self.bits & audit_bits::OWNER != 0,
1645 socket: self.bits & audit_bits::SOCKET != 0,
1646 };
1647 inherited_producer_refusal(self.fd, state, self.bits & audit_bits::LEASE != 0)
1648 }
1649 audit_bits::UNREADABLE => Some(unreadable_descriptor_refusal(self.fd, self.errno())),
1650 audit_bits::UNLISTED => Some(unlisted_descriptors_refusal(self.errno())),
1651 audit_bits::RLIMIT_CPU => Some(rlimit_cpu_refusal(self.limit(), self.errno())),
1652 _ => None,
1653 }
1654 }
1655}
1656
1657#[cfg(feature = "liteinst")]
1662fn check_inherited_descriptor(fd: libc::c_int, report: libc::c_int) -> Option<LaunchReport> {
1663 let fd_flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
1665 if fd == report || fd_flags < 0 || fd_flags & libc::FD_CLOEXEC != 0 {
1666 return None;
1667 }
1668 let (kind, bits, errno) = match detcore::host_timed_producers::host_descriptor_state(fd) {
1669 Ok((state, lease)) if lease || state.owner || state.is_complete() => (
1670 audit_bits::PRODUCER,
1671 (if state.o_async {
1672 audit_bits::O_ASYNC
1673 } else {
1674 0
1675 }) | (if state.owner { audit_bits::OWNER } else { 0 })
1676 | (if state.socket { audit_bits::SOCKET } else { 0 })
1677 | (if lease { audit_bits::LEASE } else { 0 }),
1678 0,
1679 ),
1680 Ok(_) | Err(nix::errno::Errno::EBADF) => return None,
1681 Err(errno) => (audit_bits::UNREADABLE, 0, errno as i32 as u8),
1682 };
1683 Some(LaunchReport {
1684 kind,
1685 bits,
1686 errno,
1687 fd,
1688 rlimit: (0, 0),
1689 })
1690}
1691
1692#[cfg(feature = "liteinst")]
1701fn audit_inherited_descriptors(report: libc::c_int) -> Option<LaunchReport> {
1702 let unlisted = || LaunchReport {
1703 kind: audit_bits::UNLISTED,
1704 bits: 0,
1705 errno: nix::errno::Errno::last_raw() as u8,
1706 fd: -1,
1707 rlimit: (0, 0),
1708 };
1709 unsafe {
1712 let directory = libc::open(
1713 c"/proc/self/fd".as_ptr(),
1714 libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC,
1715 );
1716 if directory < 0 {
1717 return Some(unlisted());
1718 }
1719 let mut buffer = [0u8; 4096];
1720 loop {
1721 let read = libc::syscall(
1722 libc::SYS_getdents64,
1723 directory,
1724 buffer.as_mut_ptr(),
1725 buffer.len(),
1726 );
1727 if read < 0 {
1728 let failed = unlisted();
1729 libc::close(directory);
1730 return Some(failed);
1731 }
1732 if read == 0 {
1733 break;
1734 }
1735 let mut offset = 0;
1736 while offset + 19 < read as usize {
1737 let length =
1740 u16::from_ne_bytes([buffer[offset + 16], buffer[offset + 17]]) as usize;
1741 if length == 0 {
1742 break;
1743 }
1744 let mut fd: libc::c_int = 0;
1745 let mut digits = 0;
1746 for &byte in &buffer[offset + 19..offset + length] {
1747 if !byte.is_ascii_digit() {
1748 break;
1749 }
1750 fd = fd
1751 .saturating_mul(10)
1752 .saturating_add((byte - b'0') as libc::c_int);
1753 digits += 1;
1754 }
1755 if digits > 0
1756 && fd != directory
1757 && let Some(found) = check_inherited_descriptor(fd, report)
1758 {
1759 libc::close(directory);
1760 return Some(found);
1761 }
1762 offset += length;
1763 }
1764 }
1765 libc::close(directory);
1766 None
1767 }
1768}
1769
1770#[cfg(feature = "liteinst")]
1774fn launch_check(report: libc::c_int) -> LaunchReport {
1775 if let Some(failed) = audit_inherited_descriptors(report) {
1776 return failed;
1777 }
1778 unsafe {
1780 let mut old: libc::rlimit = std::mem::zeroed();
1781 if libc::getrlimit(libc::RLIMIT_CPU, &mut old) != 0 {
1782 return LaunchReport {
1783 kind: audit_bits::RLIMIT_CPU,
1784 bits: 0,
1785 errno: nix::errno::Errno::last_raw() as u8,
1786 fd: -1,
1787 rlimit: (0, 0),
1788 };
1789 }
1790 let mut found = LaunchReport {
1791 kind: audit_bits::PASSED,
1792 bits: 0,
1793 errno: 0,
1794 fd: -1,
1795 rlimit: (old.rlim_cur, old.rlim_max),
1796 };
1797 if old.rlim_cur != libc::RLIM_INFINITY || old.rlim_max != libc::RLIM_INFINITY {
1798 let unlimited = libc::rlimit {
1799 rlim_cur: libc::RLIM_INFINITY,
1800 rlim_max: libc::RLIM_INFINITY,
1801 };
1802 if libc::setrlimit(libc::RLIMIT_CPU, &unlimited) != 0 {
1803 found.kind = audit_bits::RLIMIT_CPU;
1804 found.errno = nix::errno::Errno::last_raw() as u8;
1805 }
1806 }
1807 found
1808 }
1809}
1810
1811#[cfg(feature = "liteinst")]
1812impl LaunchAudit {
1813 fn install(command: &mut Command) -> Result<Self, Error> {
1824 use std::os::fd::AsRawFd;
1825 let (read, write) =
1826 nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC | nix::fcntl::OFlag::O_NONBLOCK)?;
1827 let report = write.as_raw_fd();
1828 unsafe {
1831 command.pre_exec(move || {
1832 let found = launch_check(report);
1833 let bytes = found.encode();
1834 libc::write(report, bytes.as_ptr().cast(), bytes.len());
1835 if found.kind == audit_bits::PASSED {
1836 Ok(())
1837 } else {
1838 Err(reverie::Errno::EPERM)
1839 }
1840 });
1841 }
1842 let pipe = std::sync::Arc::new(LaunchPipe {
1843 read,
1844 _write: write,
1845 report: std::sync::OnceLock::new(),
1846 });
1847 let logged = pipe.clone();
1848 detcore::host_timed_producers::defer_run_end_log(move || logged.log_rlimit());
1849 Ok(Self { pipe })
1850 }
1851
1852 fn launch_error(&self, backend: Backend, error: reverie::Error) -> Error {
1857 match self.pipe.report().and_then(LaunchReport::refusal) {
1858 Some(reason) => Error::new(LiteinstInGuestRefusal::for_backend(backend, reason)),
1859 None => {
1860 detcore::host_timed_producers::emit_run_end_log();
1861 Error::from(error)
1862 }
1863 }
1864 }
1865}
1866
1867#[cfg(feature = "liteinst")]
1878fn neutralize_inherited_signal_producers(
1879 command: &mut Command,
1880 backend: Backend,
1881) -> Result<Option<LaunchAudit>, Error> {
1882 if !in_guest_refuses_host_timed_producers(backend) {
1883 return Ok(None);
1884 }
1885 Ok(Some(LaunchAudit::install(command)?))
1886}
1887
1888#[cfg(feature = "liteinst")]
1897fn in_guest_trap_site_patching_conflict(command: &Command, backend: Backend) -> Option<String> {
1898 let forced = forced_site_patching_value(backend)?;
1899 let name = reverie_liteinst::SITE_PATCHING_ENV;
1900 let value = command.get_env(name)?;
1901 (*value != *std::ffi::OsStr::new(forced)).then(|| {
1902 format!(
1903 "the guest environment sets {name}={}, but --backend={} runs with syscall site \
1904 patching off and sets {name}={forced} itself; drop the variable, or select \
1905 --backend=liteinst to patch syscall sites",
1906 value.to_string_lossy(),
1907 backend.as_str()
1908 )
1909 })
1910}
1911
1912#[cfg(feature = "liteinst")]
1919fn force_in_guest_site_patching(command: &mut Command, backend: Backend) {
1920 if let Some(value) = forced_site_patching_value(backend) {
1921 command.env(reverie_liteinst::SITE_PATCHING_ENV, value);
1922 }
1923}
1924
1925#[cfg(feature = "liteinst")]
1929fn in_guest_site_patching_off(backend: Backend) -> bool {
1930 forced_site_patching_value(backend) == Some("0")
1931}
1932
1933#[cfg(feature = "liteinst")]
1938fn in_guest_stats_request(
1939 backend: Backend,
1940 summary_json: &Option<PathBuf>,
1941) -> reverie::BackendStatsRequest {
1942 if backend == Backend::InGuestTrap {
1943 reverie::BackendStatsRequest::ENABLED
1944 } else {
1945 backend_stats::request(summary_json)
1946 }
1947}
1948
1949#[cfg(feature = "liteinst")]
1957fn refuse_in_guest_policy(backend: Backend, refusal: Option<String>) -> Result<(), Error> {
1958 match refusal {
1959 Some(reason) => Err(Error::new(LiteinstInGuestRefusal::for_backend(
1960 backend,
1961 format!("{reason}; --backend=ptrace runs this program"),
1962 ))),
1963 None => Ok(()),
1964 }
1965}
1966
1967#[cfg(feature = "liteinst")]
1986fn in_guest_trap_patching_evidence(
1987 backend: Backend,
1988 dispatch: Option<&reverie::DispatchStats>,
1989 process_reports: u64,
1990) -> Option<String> {
1991 if backend != Backend::InGuestTrap {
1992 return None;
1993 }
1994 let Some(record) = dispatch else {
1995 return Some(
1996 "the run produced no LiteInst dispatch record, so it cannot show that no syscall \
1997 site was patched"
1998 .to_owned(),
1999 );
2000 };
2001 if process_reports == 0 {
2002 return Some(
2003 "no guest process reported LiteInst statistics, so the run's zero patched sites \
2004 are not a measurement and it cannot show that no syscall site was patched"
2005 .to_owned(),
2006 );
2007 }
2008 let shown =
2009 |count: Option<u64>| count.map_or_else(|| "unmeasured".to_owned(), |n| n.to_string());
2010 (record.sites.patched != Some(0) || record.sites.candidates != Some(0)).then(|| {
2011 format!(
2012 "its guests' LiteInst statistics report {} patched and {} candidate syscall sites, \
2013 so it did not run with syscall site patching off and its result is not an \
2014 in-guest-trap result",
2015 shown(record.sites.patched),
2016 shown(record.sites.candidates)
2017 )
2018 })
2019}
2020
2021#[cfg(feature = "liteinst")]
2026fn refuse_in_guest_trap_patching(backend: Backend, evidence: Option<String>) -> Result<(), Error> {
2027 match evidence {
2028 Some(reason) => {
2029 detcore::detlog::record_determinism_loss(&reason);
2030 Err(Error::new(LiteinstInGuestRefusal::for_backend(
2031 backend, reason,
2032 )))
2033 }
2034 None => Ok(()),
2035 }
2036}
2037
2038#[cfg(any(
2041 feature = "native-exec",
2042 all(feature = "exec-stop-launcher", feature = "liteinst")
2043))]
2044#[doc(hidden)]
2045pub fn in_guest_detlog_forward_policy_for_native() -> detcore::detlog::ForwardPolicy {
2046 in_guest_detlog_forward_policy()
2047}
2048
2049#[cfg(feature = "liteinst")]
2053#[doc(hidden)]
2054pub fn native_trap_dispatch_stats(
2055 source: &reverie_liteinst::LiteinstBackendStatsSource,
2056) -> Result<Option<reverie::DispatchStats>, Error> {
2057 let backend = Backend::InGuestTrap;
2058 let record = backend_stats::report(backend, reverie::BackendStatsRequest::ENABLED, source);
2059 let evidence = in_guest_trap_patching_evidence(
2060 backend,
2061 record.as_ref(),
2062 source.snapshot().process_reports(),
2063 );
2064 refuse_in_guest_trap_patching(backend, evidence)?;
2065 Ok(record)
2066}
2067
2068#[cfg(feature = "liteinst")]
2073const IN_GUEST_LITEINST_KERNEL_FLOOR: (u32, u32) = (6, 5);
2074
2075#[cfg(feature = "liteinst")]
2077fn running_kernel_release() -> String {
2078 let mut name: libc::utsname = unsafe { std::mem::zeroed() };
2079 if unsafe { libc::uname(&mut name) } != 0 {
2081 return String::new();
2082 }
2083 unsafe { std::ffi::CStr::from_ptr(name.release.as_ptr()) }
2085 .to_string_lossy()
2086 .into_owned()
2087}
2088
2089#[cfg(feature = "liteinst")]
2093fn in_guest_liteinst_kernel_gap(release: &str) -> Option<String> {
2094 let mut numbers = release
2095 .split(|c: char| !c.is_ascii_digit())
2096 .map(|part| part.parse::<u32>().ok());
2097 let (floor_major, floor_minor) = IN_GUEST_LITEINST_KERNEL_FLOOR;
2098 match (numbers.next().flatten(), numbers.next().flatten()) {
2099 (Some(major), Some(minor)) if (major, minor) >= IN_GUEST_LITEINST_KERNEL_FLOOR => None,
2100 (Some(_), Some(_)) => Some(format!(
2101 "it needs Linux {floor_major}.{floor_minor} or later (SO_PEERPIDFD, which it uses to \
2102 admit each guest process and observe its exit); this kernel is {release}"
2103 )),
2104 _ => Some(format!(
2105 "it needs Linux {floor_major}.{floor_minor} or later (SO_PEERPIDFD), and the kernel \
2106 release {release:?} does not say which version this is"
2107 )),
2108 }
2109}
2110
2111#[cfg(feature = "liteinst")]
2116const MAX_INTERPRETER_DEPTH: usize = 4;
2117
2118#[cfg(feature = "liteinst")]
2159fn in_guest_liteinst_program_gap(command: &mut Command) -> Result<Option<String>, Error> {
2160 use std::io::Read;
2161 use std::os::unix::fs::OpenOptionsExt;
2162
2163 use script::KernelScript;
2164
2165 let mut path = pin_spawn_program(command)?;
2166 for _ in 0..=MAX_INTERPRETER_DEPTH {
2167 let shown = path.display().to_string();
2168 let not_regular = || {
2169 Ok(Some(format!(
2170 "{shown} is not a regular file, and the kernel executes only regular files"
2171 )))
2172 };
2173 if !fs::metadata(&path)
2174 .with_context(|| format!("cannot read guest program {shown}"))?
2175 .is_file()
2176 {
2177 return not_regular();
2178 }
2179 let mut file = fs::OpenOptions::new()
2185 .read(true)
2186 .custom_flags(libc::O_NONBLOCK | libc::O_NOCTTY)
2187 .open(&path)
2188 .with_context(|| format!("cannot read guest program {shown}"))?;
2189 if !file
2190 .metadata()
2191 .with_context(|| format!("cannot read guest program {shown}"))?
2192 .is_file()
2193 {
2194 return not_regular();
2195 }
2196 let mut header = Vec::new();
2197 (&file)
2198 .take(256)
2199 .read_to_end(&mut header)
2200 .with_context(|| format!("cannot read guest program {shown}"))?;
2201 match KernelScript::from_buf(&header) {
2202 KernelScript::Interpreter(interpreter) => {
2203 path = match command.get_current_dir() {
2206 Some(directory) => directory.join(interpreter),
2207 None => interpreter,
2208 };
2209 continue;
2210 }
2211 KernelScript::Declined => {
2212 return Ok(Some(format!(
2213 "{shown} starts with #! but the kernel's script handler declines it (it \
2214 names no interpreter, or the interpreter path fills the first 256 bytes), \
2215 so the kernel would fail the execve or hand the file to another handler \
2216 that need not load the runtime"
2217 )));
2218 }
2219 KernelScript::NotScript => {}
2220 }
2221 let Some(startup) = interp::elf_startup(&mut file)
2222 .with_context(|| format!("cannot read the ELF headers of guest program {shown}"))?
2223 else {
2224 return Ok(Some(format!(
2225 "{shown} is neither an ELF executable nor a #! script, so the kernel would hand \
2226 it to a handler that need not load the runtime"
2227 )));
2228 };
2229 let reason = if !startup.x86_64 {
2230 "is not a 64-bit x86-64 ELF image, so the dynamic loader cannot load the 64-bit \
2231 runtime into it"
2232 } else if !startup.has_interp {
2233 "is statically linked (it has no PT_INTERP), so no dynamic loader runs to load the \
2234 runtime"
2235 } else if is_secure_execution_image(&file, &shown)? {
2236 "is set-user-ID, set-group-ID or has file capabilities, so the kernel runs it in \
2237 secure-execution mode, where the dynamic loader ignores LD_PRELOAD"
2238 } else if startup.has_preinit_array {
2239 "has a DT_PREINIT_ARRAY, whose functions run before the runtime's constructor"
2240 } else {
2241 return Ok(None);
2242 };
2243 return Ok(Some(format!(
2244 "guest program {shown} {reason}, so it would run unmonitored if Hermit launched it"
2245 )));
2246 }
2247 Ok(Some(format!(
2248 "the #! interpreters of guest program {} nest more than {MAX_INTERPRETER_DEPTH} deep",
2249 Path::new(command.get_program()).display()
2250 )))
2251}
2252
2253#[cfg(feature = "liteinst")]
2264fn pin_spawn_program(command: &mut Command) -> Result<PathBuf, Error> {
2265 let arg0 = command.get_arg0().to_owned();
2266 let program = command.find_program().with_context(|| {
2267 format!(
2268 "cannot find guest program {}",
2269 Path::new(command.get_program()).display()
2270 )
2271 })?;
2272 command.program(&program).arg0(arg0);
2273 Ok(program)
2274}
2275
2276#[cfg(feature = "liteinst")]
2280fn is_secure_execution_image(file: &fs::File, shown: &str) -> Result<bool, Error> {
2281 let mode = file
2282 .metadata()
2283 .with_context(|| format!("cannot stat guest program {shown}"))?
2284 .permissions()
2285 .mode();
2286 if mode & 0o4000 != 0 || mode & 0o2010 == 0o2010 {
2287 return Ok(true);
2288 }
2289 let size = unsafe {
2292 libc::fgetxattr(
2293 file.as_raw_fd(),
2294 c"security.capability".as_ptr(),
2295 std::ptr::null_mut(),
2296 0,
2297 )
2298 };
2299 if size >= 0 {
2300 return Ok(true);
2301 }
2302 match io::Error::last_os_error().raw_os_error() {
2303 Some(libc::ENODATA) | Some(libc::ENOTSUP) => Ok(false),
2304 _ => Err(Error::new(io::Error::last_os_error()).context(format!(
2305 "cannot read the file capabilities of guest program {shown}"
2306 ))),
2307 }
2308}
2309
2310#[cfg(feature = "liteinst")]
2314fn refuse_in_guest_liteinst_timeout(
2315 backend: Backend,
2316 timeout: Option<Duration>,
2317) -> Result<(), Error> {
2318 if backend.is_in_guest_liteinst() && timeout.is_some() {
2319 return Err(Error::new(LiteinstInGuestRefusal::for_backend(
2320 backend,
2321 "a wall-clock timeout has not been qualified for the in-guest runtime, whose run \
2322 future does not kill the guest when it is dropped",
2323 )));
2324 }
2325 Ok(())
2326}
2327
2328#[cfg(not(feature = "liteinst"))]
2329fn refuse_in_guest_liteinst_timeout(
2330 _backend: Backend,
2331 _timeout: Option<Duration>,
2332) -> Result<(), Error> {
2333 Ok(())
2334}
2335
2336#[cfg(feature = "liteinst")]
2337fn liteinst_unavailable_reason() -> Option<String> {
2338 liteinst_runtime_unavailable_reason()
2339}
2340
2341#[cfg(not(feature = "liteinst"))]
2346fn liteinst_unavailable_reason() -> Option<String> {
2347 Some("this build was compiled without the liteinst backend: the `liteinst` feature is not enabled; rebuild with `--features liteinst` (or `--features third-party-backends`). This says nothing about whether LiteInst works on this machine -- it has not been checked".to_owned())
2348}
2349
2350#[cfg(feature = "liteinst")]
2351fn liteinst_runtime_unavailable_reason() -> Option<String> {
2352 liteinst_tool_runtime_library_path().err().map(|error| {
2353 format!(
2354 "the in-guest Detcore runtime is unavailable: {error}; build it with `cargo build -p detcore-liteinst` in the target directory of this hermit binary, install it as rsrcs/libdetcore_liteinst.so, or name it with {LITEINST_TOOL_RUNTIME_ENV}"
2355 )
2356 })
2357}
2358
2359fn kvm_device_unavailable_reason(path: &Path) -> Option<String> {
2360 fs::OpenOptions::new()
2361 .read(true)
2362 .write(true)
2363 .open(path)
2364 .err()
2365 .map(|error| {
2366 format!(
2367 "cannot open {} read-write: {error}; grant access through the device owner/group \
2368 or root",
2369 path.display()
2370 )
2371 })
2372}
2373
2374#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, ValueEnum)]
2376pub enum Backend {
2377 #[default]
2379 Ptrace,
2380 Liteinst,
2382 InGuestTrap,
2389 Kvm,
2391}
2392
2393#[derive(Debug, Clone, Eq, PartialEq)]
2399pub struct BackendUnavailable {
2400 backend: Backend,
2401 reason: String,
2402}
2403
2404impl BackendUnavailable {
2405 pub fn new(backend: Backend, reason: impl Into<String>) -> Self {
2406 Self {
2407 backend,
2408 reason: reason.into(),
2409 }
2410 }
2411
2412 pub fn backend(&self) -> Backend {
2413 self.backend
2414 }
2415
2416 pub fn reason(&self) -> &str {
2417 &self.reason
2418 }
2419}
2420
2421impl std::fmt::Display for BackendUnavailable {
2422 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2423 write!(
2424 formatter,
2425 "backend `{}` is unavailable: {}",
2426 self.backend.as_str(),
2427 self.reason
2428 )
2429 }
2430}
2431
2432impl std::error::Error for BackendUnavailable {}
2433
2434impl Backend {
2435 const ALL: [Self; 4] = [Self::Ptrace, Self::Liteinst, Self::InGuestTrap, Self::Kvm];
2436
2437 pub const fn as_str(self) -> &'static str {
2439 match self {
2440 Self::Ptrace => "ptrace",
2441 Self::Liteinst => "liteinst",
2442 Self::InGuestTrap => "in-guest-trap",
2443 Self::Kvm => "kvm",
2444 }
2445 }
2446
2447 pub const fn is_in_guest_liteinst(self) -> bool {
2453 matches!(self, Self::Liteinst | Self::InGuestTrap)
2454 }
2455
2456 pub const fn runtime_name(self) -> &'static str {
2461 match self {
2462 Self::InGuestTrap => Self::Liteinst.as_str(),
2463 other => other.as_str(),
2464 }
2465 }
2466
2467 fn consumes_skid_overshoot_reports(self) -> bool {
2473 backend_delivers_preemption_timer(self)
2474 }
2475
2476 pub fn available() -> impl Iterator<Item = Self> {
2481 Self::ALL
2482 .into_iter()
2483 .filter(|backend| backend.is_available())
2484 }
2485
2486 pub fn is_available(self) -> bool {
2488 self.unavailable_reason().is_none()
2489 }
2490
2491 pub fn ensure_available(self) -> Result<(), Error> {
2493 self.ensure_available_for(NamespaceUse::Creates)
2494 }
2495
2496 pub fn ensure_available_for(self, namespaces: NamespaceUse) -> Result<(), Error> {
2501 if let Some(reason) = self.unavailable_reason_for(namespaces) {
2502 Err(Error::new(BackendUnavailable::new(self, reason)))
2503 } else {
2504 Ok(())
2505 }
2506 }
2507
2508 fn unavailable_reason(self) -> Option<String> {
2509 self.unavailable_reason_for(NamespaceUse::Creates)
2510 }
2511
2512 fn unavailable_reason_for(self, namespaces: NamespaceUse) -> Option<String> {
2513 match self {
2514 Self::Ptrace => validate_tracing_environment(namespaces)
2515 .err()
2516 .map(|error| error.to_string()),
2517 Self::Liteinst | Self::InGuestTrap => liteinst_unavailable_reason(),
2519 Self::Kvm => kvm_device_unavailable_reason(Path::new("/dev/kvm")),
2520 }
2521 }
2522}
2523
2524struct SkidOvershootReport {
2525 enabled: bool,
2526}
2527
2528#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2537pub struct BackendRunFailure;
2538
2539impl std::fmt::Display for BackendRunFailure {
2540 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2541 f.write_str("the backend failed while running the guest")
2542 }
2543}
2544
2545impl std::error::Error for BackendRunFailure {}
2546
2547#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2554pub struct SkidOvershootError {
2555 count: u64,
2556}
2557
2558impl SkidOvershootError {
2559 pub fn new(count: u64) -> Self {
2560 assert!(
2561 count > 0,
2562 "a skid-overshoot refusal requires a positive count"
2563 );
2564 Self { count }
2565 }
2566
2567 pub fn count(&self) -> u64 {
2568 self.count
2569 }
2570}
2571
2572impl std::fmt::Display for SkidOvershootError {
2573 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2574 write!(
2575 f,
2576 "observed {} {} report(s); refusing the result because precise PMU timer \
2577 delivery passed its target and deterministic execution was not established",
2578 self.count,
2579 reverie::SKID_OVERSHOOT_MARKER
2580 )
2581 }
2582}
2583
2584impl std::error::Error for SkidOvershootError {}
2585
2586fn take_skid_overshoot_error() -> Option<SkidOvershootError> {
2587 let count = reverie::take_skid_overshoot_count();
2588 (count > 0).then(|| SkidOvershootError::new(count))
2589}
2590
2591impl SkidOvershootReport {
2592 fn begin(enabled: bool) -> Self {
2593 if enabled {
2594 let _ = reverie::take_skid_overshoot_count();
2596 }
2597 Self { enabled }
2598 }
2599
2600 fn finish<T>(self, result: Result<T, Error>) -> Result<T, Error> {
2601 if !self.enabled {
2602 return result;
2603 }
2604
2605 let Some(overshoot) = take_skid_overshoot_error() else {
2606 return result;
2607 };
2608 match result {
2609 Ok(_) => Err(Error::new(overshoot)),
2610 Err(error) => Err(error.context(overshoot)),
2611 }
2612 }
2613
2614 fn finish_with_count<T>(self, result: Result<T, Error>) -> Result<(T, u64), Error> {
2615 if !self.enabled {
2616 return result.map(|value| (value, 0));
2617 }
2618
2619 let count = reverie::take_skid_overshoot_count();
2620 match result {
2621 Ok(value) => Ok((value, count)),
2622 Err(error) if count > 0 => Err(error.context(SkidOvershootError::new(count))),
2623 Err(error) => Err(error),
2624 }
2625 }
2626}
2627
2628#[cfg(feature = "liteinst")]
2637const LITEINST_DETLOG_FORWARD_ENV: &str = "HERMIT_LITEINST_FORWARD_DETLOG";
2638#[cfg(feature = "liteinst")]
2640const LITEINST_DETLOG_FORWARD_POLICY_ENV: &str = "HERMIT_LITEINST_FORWARD_DETLOG_POLICY";
2641
2642static IN_GUEST_DETLOG_SINK: Mutex<Option<std::os::fd::OwnedFd>> = Mutex::new(None);
2645
2646#[doc(hidden)]
2654pub fn forward_in_guest_detlogs_to(socket: std::os::fd::OwnedFd) {
2655 *IN_GUEST_DETLOG_SINK.lock().unwrap() = Some(socket);
2656}
2657
2658#[doc(hidden)]
2660pub fn clear_in_guest_detlog_forwarding() {
2661 IN_GUEST_DETLOG_SINK.lock().unwrap().take();
2662}
2663
2664#[cfg(feature = "liteinst")]
2683fn in_guest_liteinst_launch(
2684 command: &mut Command,
2685 policy: &detcore::detlog::ForwardPolicy,
2686) -> Result<
2687 (
2688 detcore::in_guest_launch::InGuestLaunch,
2689 Option<std::os::fd::OwnedFd>,
2690 ),
2691 Error,
2692> {
2693 command.env_remove(LITEINST_DETLOG_FORWARD_ENV);
2694 command.env_remove(LITEINST_DETLOG_FORWARD_POLICY_ENV);
2695 command.env_remove(detcore::LITEINST_CONFIG_FINGERPRINT_ENV);
2696 let mut launch = detcore::in_guest_launch::InGuestLaunch {
2697 coordinator_fingerprint: Some(detcore::config_wire_fingerprint()),
2698 ..Default::default()
2699 };
2700 if !policy.forwards_any() {
2705 return Ok((launch, None));
2706 }
2707 let Some(duplicate) = inheritable_in_guest_detlog_socket()? else {
2708 return Ok((launch, None));
2709 };
2710 launch.forward_request = Some(detcore::detlog::tool_output_env_value(
2711 duplicate.as_raw_fd(),
2712 )?);
2713 launch.forward_policy = Some(policy.encode());
2714 Ok((launch, Some(duplicate)))
2715}
2716
2717#[doc(hidden)]
2722pub fn inheritable_in_guest_detlog_socket() -> Result<Option<std::os::fd::OwnedFd>, Error> {
2723 let sink = IN_GUEST_DETLOG_SINK.lock().unwrap();
2724 let Some(socket) = sink.as_ref() else {
2725 return Ok(None);
2726 };
2727 let duplicate = unsafe { libc::fcntl(socket.as_raw_fd(), libc::F_DUPFD, 3) };
2729 if duplicate < 0 {
2730 return Err(Error::new(std::io::Error::last_os_error())
2731 .context("duplicating the in-guest DETLOG forwarding socket"));
2732 }
2733 Ok(Some(unsafe {
2735 std::os::fd::OwnedFd::from_raw_fd(duplicate)
2736 }))
2737}
2738
2739#[doc(hidden)]
2745pub fn in_guest_detlog_forward_policy() -> detcore::detlog::ForwardPolicy {
2746 let directives = std::env::var(tracing_subscriber::EnvFilter::DEFAULT_ENV).unwrap_or_default();
2747 detcore::detlog::ForwardPolicy::from_current_subscriber(&directives)
2748}
2749
2750fn ensure_backend_dispatch(backend: Backend) -> Result<(), Error> {
2765 if backend == Backend::Ptrace {
2768 return Ok(());
2769 }
2770
2771 backend.ensure_available()?;
2774 Err(anyhow!(
2775 "backend `{}` has no Hermit dispatch implementation",
2776 backend.as_str()
2777 ))
2778}
2779
2780#[cfg(target_arch = "x86_64")]
2798const KVM_GUEST_MEMORY_BYTES: usize = 3 * 1024 * 1024 * 1024;
2799
2800#[cfg(target_arch = "x86_64")]
2803const MAX_SHEBANG_DEPTH: usize = 4;
2804
2805#[cfg(target_arch = "x86_64")]
2821fn resolve_kvm_shebang(
2822 resolved_program: &Path,
2823 mut argv: Vec<String>,
2824) -> Result<(PathBuf, Vec<String>, fs::File), Error> {
2825 let mut load_path = resolved_program.to_path_buf();
2826 let (mut file, mut image) = read_kvm_image(&load_path)
2827 .map_err(|error| anyhow!("failed to read KVM guest executable {load_path:?}: {error}"))?;
2828
2829 let mut depth = 0;
2830 while image.starts_with(b"#!") {
2831 depth += 1;
2832 if depth > MAX_SHEBANG_DEPTH {
2833 return Err(anyhow!(
2834 "too many levels of `#!` interpreter indirection loading {resolved_program:?}"
2835 ));
2836 }
2837 let (interpreter, shebang_args) = Shebang::from_buf(&image)
2838 .ok_or_else(|| anyhow!("malformed `#!` interpreter line in {load_path:?}"))?
2839 .into_parts();
2840 let interpreter_str = interpreter
2841 .to_str()
2842 .ok_or_else(|| anyhow!("non-UTF-8 `#!` interpreter path in {load_path:?}"))?
2843 .to_owned();
2844
2845 let mut rewritten = Vec::with_capacity(argv.len() + shebang_args.len() + 2);
2850 rewritten.push(interpreter_str);
2851 for arg in &shebang_args {
2852 rewritten.push(
2853 arg.to_str()
2854 .ok_or_else(|| anyhow!("non-UTF-8 `#!` interpreter argument in {load_path:?}"))?
2855 .to_owned(),
2856 );
2857 }
2858 rewritten.push(load_path.to_string_lossy().into_owned());
2859 rewritten.extend_from_slice(&argv[1..]);
2860 argv = rewritten;
2861
2862 load_path = interpreter;
2863 (file, image) = read_kvm_image(&load_path).map_err(|error| {
2864 anyhow!(
2865 "failed to read `#!` interpreter {load_path:?} for {resolved_program:?}: {error}"
2866 )
2867 })?;
2868 }
2869
2870 Ok((load_path, argv, file))
2871}
2872
2873#[cfg(target_arch = "x86_64")]
2875fn read_kvm_image(path: &Path) -> io::Result<(fs::File, Vec<u8>)> {
2876 let mut file = fs::File::open(path)?;
2877 let mut image = Vec::new();
2878 io::Read::read_to_end(&mut file, &mut image)?;
2879 Ok((file, image))
2880}
2881
2882#[cfg(target_arch = "x86_64")]
2886const CONTAINER_REPLACED_TMP: &str = "/tmp";
2887
2888#[cfg(target_arch = "x86_64")]
2916fn kvm_cwd_resolution_error(requested_cwd: &Path, error: &std::io::Error) -> Error {
2917 let under_replaced_tmp = requested_cwd.starts_with(CONTAINER_REPLACED_TMP)
2918 && requested_cwd != Path::new(CONTAINER_REPLACED_TMP);
2919 if error.kind() == std::io::ErrorKind::NotFound && under_replaced_tmp {
2920 anyhow!(
2921 "failed to resolve KVM guest working directory {:?}: {error}\n\
2922 \n\
2923 This directory may well exist on the host. The container replaces \
2924 {CONTAINER_REPLACED_TMP} with a private, empty tmpfs, so nothing beneath \
2925 {CONTAINER_REPLACED_TMP} is visible to the guest, and resolving the working \
2926 directory happens inside that mount namespace.\n\
2927 Either run from a directory outside {CONTAINER_REPLACED_TMP}, or pass \
2928 --workdir with a path that exists inside the guest.",
2929 requested_cwd
2930 )
2931 } else {
2932 anyhow!(
2933 "failed to resolve KVM guest working directory {:?}: {error}",
2934 requested_cwd
2935 )
2936 }
2937}
2938
2939#[cfg(target_arch = "x86_64")]
2973fn prepare_kvm_mountinfo_config(
2974 config: &mut DetConfig,
2975 namespace: impl FnOnce() -> Result<detcore_model::config::MountNamespaceIdentity, Error>,
2976 capture: impl FnOnce() -> Result<Vec<u64>, Error>,
2977) -> Result<(), Error> {
2978 if !config.mountinfo_mount_ids_captured
2979 && (!config.mountinfo_mount_ids.is_empty()
2980 || !config.mount_id_assignment_order.is_empty()
2981 || config.mountinfo_mount_namespace.is_some())
2982 {
2983 anyhow::bail!("KVM mount identities have no producer-captured namespace");
2984 }
2985 if config.mountinfo_mount_ids_captured {
2986 let Some(producer) = config.mountinfo_mount_namespace else {
2987 anyhow::bail!(
2988 "KVM producer-captured mount identities do not name their mount namespace"
2989 );
2990 };
2991 if config.mountinfo_mount_ids.is_empty()
2994 || !detcore_model::procfs::mount_identity_provenance_is_well_formed(
2995 &config.mountinfo_mount_ids,
2996 &config.mount_id_assignment_order,
2997 )
2998 {
2999 anyhow::bail!(
3000 "KVM producer-captured mount identities are malformed ({} listed, {} assigned)",
3001 config.mountinfo_mount_ids.len(),
3002 config.mount_id_assignment_order.len()
3003 );
3004 }
3005 if let Some(missing) = config
3006 .mountinfo_root_rewrites
3007 .iter()
3008 .map(|rewrite| rewrite.raw_mount_id)
3009 .find(|id| !config.mountinfo_mount_ids.contains(id))
3010 {
3011 anyhow::bail!(
3012 "KVM mount root rewrite names mount {missing}, which the producer's snapshot does \
3013 not have"
3014 );
3015 }
3016 let backend = namespace().context("failed to identify the KVM backend mount namespace")?;
3017 if producer != backend {
3018 anyhow::bail!(
3019 "KVM producer-captured mount identities name another mount namespace \
3020 (captured in {producer:?}, backend in {backend:?})"
3021 );
3022 }
3023 } else {
3024 let order = capture().context("failed to capture the KVM backend mount namespace")?;
3025 config.mountinfo_mount_ids = order;
3026 config.mountinfo_mount_ids_captured = true;
3027 }
3028 if let Some(table) = &config.container_mount_table
3031 && let Some(missing) = table
3032 .mounts
3033 .iter()
3034 .map(|row| row.raw_mount_id)
3035 .find(|id| !config.mountinfo_mount_ids.contains(id))
3036 {
3037 anyhow::bail!(
3038 "KVM container mount table names mount {missing}, which the backend namespace does \
3039 not have"
3040 );
3041 }
3042 config.mountinfo_device_rewrites.clear();
3046 Ok(())
3047}
3048
3049#[cfg(target_arch = "x86_64")]
3071fn kvm_reports_stored_metadata_timestamps(config: &DetConfig) -> bool {
3072 config.virtualize_metadata && config.sequentialize_threads
3073}
3074
3075#[cfg(not(target_arch = "x86_64"))]
3077async fn run_kvm(
3078 _command: &Command,
3079 _config: DetConfig,
3080 _print_summary: bool,
3081 _print_summary_to_json_file: &Option<PathBuf>,
3082 _capture_output: bool,
3083) -> Result<Output, Error> {
3084 anyhow::bail!(
3085 "the KVM backend runs only on x86-64 hosts, and this host is {}",
3086 std::env::consts::ARCH
3087 )
3088}
3089
3090#[cfg(target_arch = "x86_64")]
3092async fn run_kvm(
3093 command: &Command,
3094 mut config: DetConfig,
3095 print_summary: bool,
3096 print_summary_to_json_file: &Option<PathBuf>,
3097 capture_output: bool,
3098) -> Result<Output, Error> {
3099 let dispatch_started = Instant::now();
3100 kvm_address_space::admit_kvm_address_space(KVM_GUEST_MEMORY_BYTES)?;
3104 if config.container_mount_table.is_none() {
3111 let launch_cwd = command
3112 .get_current_dir()
3113 .map_or_else(|| Path::new(".").to_owned(), Path::to_owned);
3114 let (table, order) = unplanned_container_mount_table(&launch_cwd)?;
3115 if !config.mountinfo_mount_ids_captured
3116 && config.mountinfo_mount_ids.is_empty()
3117 && config.mount_id_assignment_order.is_empty()
3118 && config.mountinfo_mount_namespace.is_none()
3119 {
3120 config.mountinfo_mount_namespace = Some(current_mount_namespace()?);
3121 config.mountinfo_mount_ids = order;
3122 config.mountinfo_mount_ids_captured = true;
3123 }
3124 config.container_mount_table = Some(table);
3125 }
3126 let keep = config
3127 .container_mount_table
3128 .as_ref()
3129 .map(detcore_model::container_mounts::ContainerMountTable::shown_mount_ids)
3130 .unwrap_or_default();
3131 prepare_kvm_mountinfo_config(&mut config, current_mount_namespace, || {
3132 capture_mountinfo_identity_order_keeping(&keep)
3133 })?;
3134 let stdin = if capture_output {
3135 let (snapshot_reserved, snapshot) = output_backend_stdin_reservation()?;
3136 if snapshot_reserved {
3137 snapshot
3138 } else {
3139 reserved_kvm_stdin()?
3144 }
3145 } else {
3146 reserved_kvm_stdin()?
3147 };
3148 config.standard_stream_aliases = if capture_output {
3154 None
3155 } else {
3156 standard_stream_aliases([stdin.as_ref().map(AsRawFd::as_raw_fd), Some(1), Some(2)])
3157 };
3158 let requested_cwd = match command.get_current_dir() {
3162 Some(cwd) => cwd.to_owned(),
3163 None => std::env::current_dir()?,
3164 };
3165 let cwd = fs::canonicalize(&requested_cwd)
3166 .map_err(|error| kvm_cwd_resolution_error(&requested_cwd, &error))?;
3167 let program = command
3168 .get_program()
3169 .to_str()
3170 .ok_or_else(|| anyhow!("KVM guest executable path is not valid UTF-8"))?
3171 .to_owned();
3172 if !cwd.is_dir() {
3173 return Err(anyhow!(
3174 "KVM guest working directory is not a directory: {:?}",
3175 cwd
3176 ));
3177 }
3178 let resolved_program = command.find_program().map_err(|error| {
3179 anyhow!("failed to resolve KVM guest executable {program:?} in the guest PATH: {error}")
3180 })?;
3181 let mut argv = Vec::with_capacity(1 + command.get_args().count());
3182 argv.push(program.clone());
3183 for argument in command.get_args() {
3184 argv.push(
3185 argument
3186 .to_str()
3187 .ok_or_else(|| anyhow!("KVM guest argument is not valid UTF-8"))?
3188 .to_owned(),
3189 );
3190 }
3191
3192 let (_interpreter_path, argv, executable) = resolve_kvm_shebang(&resolved_program, argv)?;
3194 let program = argv.first().cloned().unwrap_or(program);
3196 let envp = command
3197 .get_captured_envs()
3198 .into_iter()
3199 .map(|(key, value)| {
3200 let key = key
3201 .to_str()
3202 .ok_or_else(|| anyhow!("KVM guest environment key is not valid UTF-8"))?;
3203 let value = value
3204 .to_str()
3205 .ok_or_else(|| anyhow!("KVM guest environment value is not valid UTF-8"))?;
3206 Ok(format!("{key}={value}"))
3207 })
3208 .collect::<Result<Vec<_>, Error>>()?;
3209 tracing::info!(
3210 target: "hermit::kvm",
3211 program = %program,
3212 argv = ?argv,
3213 cwd = %cwd.display(),
3214 env_count = envp.len(),
3215 "launching guest through reverie-kvm",
3216 );
3217 let argv = argv.iter().map(String::as_str).collect::<Vec<_>>();
3218 let envp = envp.iter().map(String::as_str).collect::<Vec<_>>();
3219
3220 let setup_started = Instant::now();
3221 config.has_uts_namespace = false;
3226 let mut backend = reverie_kvm::KvmBackend::new_with_stdin(KVM_GUEST_MEMORY_BYTES, stdin)
3227 .map_err(|error| anyhow!("failed to initialize reverie-kvm: {error}"))?;
3228 backend
3231 .set_root_pid(detcore::ROOT_DETPID.as_raw())
3232 .map_err(|error| anyhow!("failed to configure KVM root PID: {error}"))?;
3233 backend
3236 .set_ids_per_task(reverie::task_ids::HERMIT_PTRACE_IDS_PER_TASK)
3237 .map_err(|error| anyhow!("failed to configure KVM task IDs: {error}"))?;
3238 backend
3253 .install_static_elf_file_with_exec_context(
3254 executable,
3255 resolved_program.as_os_str(),
3256 &argv,
3257 &envp,
3258 &cwd,
3259 )
3260 .map_err(|error| anyhow!("failed to load KVM guest executable {program:?}: {error}"))?;
3261 if let Some(placeholder) = config.directory_placeholder.as_deref() {
3270 backend
3271 .reserve_tool_directory(placeholder)
3272 .map_err(|error| {
3273 anyhow!("failed to reserve the KVM placeholder directory {placeholder:?}: {error}")
3274 })?;
3275 }
3276 if kvm_reports_stored_metadata_timestamps(&config) {
3285 backend
3286 .set_host_metadata_timestamps(true)
3287 .map_err(|error| anyhow!("failed to configure KVM metadata timestamps: {error}"))?;
3288 }
3289
3290 let execution_started = Instant::now();
3291 let completion = hermit_run_kvm::run_detcore(&mut backend, config, capture_output)
3292 .await
3293 .map_err(|error| kvm_execution_error(error, None))?;
3294 let cleanup_started = Instant::now();
3295 let result =
3296 finish_kvm_tool_completion(completion, print_summary, print_summary_to_json_file).await;
3297 let teardown_started = Instant::now();
3298 drop(backend);
3300 let teardown_finished = Instant::now();
3301 let (status, stdout, stderr) = result?;
3302
3303 tracing::debug!(
3308 target: "hermit::kvm",
3309 prepare_us = setup_started.duration_since(dispatch_started).as_micros() as u64,
3310 setup_us = execution_started.duration_since(setup_started).as_micros() as u64,
3311 execution_us = cleanup_started.duration_since(execution_started).as_micros() as u64,
3312 cleanup_us = teardown_started.duration_since(cleanup_started).as_micros() as u64,
3313 teardown_us = teardown_finished.duration_since(teardown_started).as_micros() as u64,
3314 lifecycle_us = teardown_finished.duration_since(dispatch_started).as_micros() as u64,
3315 "reverie-kvm lifecycle phase timings",
3316 );
3317
3318 if !capture_output {
3319 std::io::stdout().write_all(&stdout)?;
3320 std::io::stderr().write_all(&stderr)?;
3321 }
3322
3323 Ok(Output {
3324 status,
3325 stdout,
3326 stderr,
3327 })
3328}
3329
3330#[cfg(target_arch = "x86_64")]
3333async fn finish_kvm_tool_completion(
3334 completion: reverie_kvm::ToolRunCompletion<detcore::GlobalState>,
3335 print_summary: bool,
3336 print_summary_to_json_file: &Option<PathBuf>,
3337) -> Result<(ExitStatus, Vec<u8>, Vec<u8>), Error> {
3338 match completion.result {
3339 Ok(output) => {
3340 completion
3341 .global_state
3342 .clean_up(print_summary, print_summary_to_json_file)
3343 .await?;
3344 Ok(output)
3345 }
3346 Err(error) => {
3347 let cleanup = completion
3348 .global_state
3349 .clean_up_after_backend_failure()
3350 .await;
3351 Err(kvm_execution_error(error, Some(cleanup)))
3352 }
3353 }
3354}
3355
3356#[cfg(target_arch = "x86_64")]
3357fn kvm_execution_error(
3358 primary: reverie_kvm::Error,
3359 cleanup: Option<detcore::BackendFailureCleanup>,
3360) -> Error {
3361 let message = format!("KVM guest execution failed: {primary}");
3364 let mut error = Error::new(primary).context(BackendRunFailure);
3365 if let Some(cleanup) = cleanup {
3366 if let Err(recording) = cleanup.preemption_recording {
3367 error = error.context(format!("partial preemption recording failed: {recording}"));
3368 }
3369 if let Err(scheduler) = cleanup.scheduler {
3370 error = error.context(scheduler);
3371 }
3372 }
3373 error.context(message)
3374}
3375
3376#[cfg(all(test, target_arch = "x86_64"))]
3377mod kvm_failure_tests;
3378
3379#[cfg(all(test, feature = "kvm-execution-tests"))]
3380mod kvm_execution_tests;
3381
3382pub fn run(
3418 command: Command,
3419 config: DetConfig,
3420 print_summary: bool,
3421 print_summary_to_json_file: &Option<PathBuf>,
3422) -> Result<ExitStatus, Error> {
3423 run_with_backend(
3424 command,
3425 config,
3426 print_summary,
3427 print_summary_to_json_file,
3428 Backend::Ptrace,
3429 )
3430}
3431
3432pub fn run_with_backend(
3436 command: Command,
3437 config: DetConfig,
3438 print_summary: bool,
3439 print_summary_to_json_file: &Option<PathBuf>,
3440 backend: Backend,
3441) -> Result<ExitStatus, Error> {
3442 run_with_backend_timeout(
3443 command,
3444 config,
3445 print_summary,
3446 print_summary_to_json_file,
3447 backend,
3448 None,
3449 )
3450}
3451
3452pub fn run_with_backend_timeout(
3471 command: Command,
3472 config: DetConfig,
3473 print_summary: bool,
3474 print_summary_to_json_file: &Option<PathBuf>,
3475 backend: Backend,
3476 timeout: Option<Duration>,
3477) -> Result<ExitStatus, Error> {
3478 if backend == Backend::Ptrace {
3479 let summary_path = print_summary_to_json_file.clone();
3480 let command = tracer_thread::SendCommand::new(command);
3481 return ptrace_completion::run_on_tracer_thread(timeout, move |control| async move {
3482 let command = command.into_inner();
3483 let report = SkidOvershootReport::begin(true);
3484 let config = prepare_backend_config(config, backend);
3485 let result = dispatch_backend(
3486 command,
3487 config,
3488 print_summary,
3489 &summary_path,
3490 backend,
3491 Some(control),
3492 )
3493 .await;
3494 report.finish(result)
3495 });
3496 }
3497 let skid_overshoot_report =
3498 SkidOvershootReport::begin(backend.consumes_skid_overshoot_reports());
3499 if backend == Backend::Kvm {
3500 ensure_kvm_stdin_reserved()?;
3501 }
3502 let config = prepare_backend_config(config, backend);
3503 let result = run_with_backend_inner(
3504 command,
3505 config,
3506 print_summary,
3507 print_summary_to_json_file,
3508 backend,
3509 timeout,
3510 );
3511 skid_overshoot_report.finish(result)
3512}
3513
3514#[doc(hidden)]
3516pub fn prepare_backend_config(config: DetConfig, backend: Backend) -> DetConfig {
3517 prepare_backend_config_with_capabilities(config, backend_capabilities(backend))
3518}
3519
3520#[doc(hidden)]
3523pub fn prepare_backend_config_with_capabilities(
3524 mut config: DetConfig,
3525 capabilities: reverie::BackendCapabilities,
3526) -> DetConfig {
3527 config.backend = capabilities;
3550 config.guest_may_inherit_a_terminal =
3568 config.backend.host_timed_signals_are_physical && launcher_may_pass_a_terminal();
3569 config.in_guest_reserved_signals = 0;
3574 config.in_guest_refuses_host_timed_producers = false;
3577 config.in_guest_signal_delivery = false;
3581 config
3582}
3583
3584pub fn prepare_in_guest_loader_route_config(config: DetConfig) -> DetConfig {
3592 let mut config = prepare_backend_config(config, Backend::InGuestTrap);
3593 config.in_guest_site_patching_off = true;
3594 config
3595}
3596
3597fn launcher_may_pass_a_terminal() -> bool {
3606 let standard_terminal = (0..=2).any(|fd| unsafe { libc::isatty(fd) } == 1);
3608 may_pass_a_terminal(standard_terminal, controlling_terminal_device())
3609}
3610
3611fn may_pass_a_terminal(standard_terminal: bool, controlling_terminal: Option<i64>) -> bool {
3614 standard_terminal || controlling_terminal != Some(0)
3615}
3616
3617fn controlling_terminal_device() -> Option<i64> {
3621 parse_controlling_terminal_device(&std::fs::read_to_string("/proc/self/stat").ok()?)
3622}
3623
3624fn parse_controlling_terminal_device(stat: &str) -> Option<i64> {
3628 let after_command = stat.get(stat.rfind(')')? + 1..)?;
3629 after_command.split_ascii_whitespace().nth(4)?.parse().ok()
3630}
3631
3632#[doc(hidden)]
3638pub fn backend_capabilities(backend: Backend) -> reverie::BackendCapabilities {
3639 match backend {
3640 Backend::Ptrace => <reverie_ptrace::PtraceBackend as reverie::Backend>::capabilities(),
3645 #[cfg(target_arch = "x86_64")]
3646 Backend::Kvm => reverie_kvm::KvmBackend::capabilities(),
3647 #[cfg(not(target_arch = "x86_64"))]
3650 Backend::Kvm => reverie::BackendCapabilities::KVM,
3651 Backend::Liteinst | Backend::InGuestTrap => reverie::BackendCapabilities::LITEINST_IN_GUEST,
3654 }
3655}
3656
3657pub fn backend_delivers_preemption_timer(backend: Backend) -> bool {
3663 backend_capabilities(backend).delivers_preemption_timer_events
3664}
3665
3666#[cfg(feature = "liteinst")]
3680async fn settle_in_guest_exits(
3681 exits: &in_guest_exits::InGuestExitAdmission,
3682 global_state: &detcore::GlobalState,
3683) -> Result<(), Error> {
3684 let drained = exits.drain(std::time::Duration::from_secs(10)).await;
3685 for pid in exits.reported_exits() {
3686 global_state.complete_physical_process_exit(pid);
3687 }
3688 global_state.release_all_physical_process_exits();
3689 let failure = exits.failure().or_else(|| {
3690 drained
3691 .err()
3692 .map(|error| format!("not every in-guest LiteInst guest exit was reported: {error:?}"))
3693 });
3694 match failure {
3695 Some(reason) => {
3696 detcore::detlog::record_determinism_loss(&reason);
3697 Err(Error::msg(reason))
3698 }
3699 None => Ok(()),
3700 }
3701}
3702
3703#[cfg(feature = "liteinst")]
3736fn liteinst_requires_forced_shutdown(status: ExitStatus) -> bool {
3737 match status {
3738 ExitStatus::Signaled(_, _) | ExitStatus::SignaledRaw(_, _) => true,
3740 ExitStatus::Exited(code) => {
3741 (122..=127).contains(&code) || detcore_model::signal_from_exit_status(code).is_some()
3744 }
3745 }
3746}
3747
3748#[derive(Debug)]
3754pub struct GuestTimedOut {
3755 pub limit: Duration,
3757 pub survivors: Option<String>,
3763}
3764
3765impl std::fmt::Display for GuestTimedOut {
3766 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3767 write!(
3768 f,
3769 "Guest exceeded the --timeout bound of {} seconds",
3770 self.limit.as_secs()
3771 )?;
3772 if let Some(survivors) = &self.survivors {
3773 write!(f, "; {survivors}")?;
3774 }
3775 Ok(())
3776 }
3777}
3778
3779impl std::error::Error for GuestTimedOut {}
3780
3781#[non_exhaustive]
3798pub struct SchedulerEndedRun {
3799 pub outcome: detcore::TerminalOutcome,
3801 pub output: Option<Output>,
3803}
3804
3805impl SchedulerEndedRun {
3806 pub fn new(outcome: detcore::TerminalOutcome, output: Option<Output>) -> Self {
3809 Self { outcome, output }
3810 }
3811
3812 pub fn split<T>(result: Result<T, Error>) -> Result<Result<T, SchedulerEndedRun>, Error> {
3816 match result {
3817 Ok(value) => Ok(Ok(value)),
3818 Err(error)
3819 if error.downcast_ref::<SkidOvershootError>().is_some()
3820 || detcore::ReplayDepartureError::in_error(&error).is_some() =>
3821 {
3822 Err(error)
3823 }
3824 Err(error) => error.downcast::<SchedulerEndedRun>().map(Err),
3825 }
3826 }
3827
3828 pub fn into_output(self) -> Result<Output, Error> {
3830 self.output.ok_or_else(|| {
3831 Error::msg("a run that captured output ended with a terminal outcome and returned none")
3832 })
3833 }
3834
3835 pub fn status_of(
3838 result: Result<ExitStatus, Error>,
3839 ) -> Result<(ExitStatus, Option<detcore::TerminalOutcome>), Error> {
3840 Ok(match Self::split(result)? {
3841 Ok(status) => (status, None),
3842 Err(ended) => (
3843 ExitStatus::Exited(ended.outcome.exit_status()),
3844 Some(ended.outcome),
3845 ),
3846 })
3847 }
3848
3849 pub fn output_of(
3852 result: Result<Output, Error>,
3853 ) -> Result<(Output, Option<detcore::TerminalOutcome>), Error> {
3854 Ok(match Self::split(result)? {
3855 Ok(output) => (output, None),
3856 Err(ended) => {
3857 let outcome = ended.outcome;
3858 (ended.into_output()?, Some(outcome))
3859 }
3860 })
3861 }
3862}
3863
3864impl std::fmt::Debug for SchedulerEndedRun {
3865 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3866 let mut debug = f.debug_struct("SchedulerEndedRun");
3867 debug.field("outcome", &self.outcome);
3868 if let Some(output) = &self.output {
3869 debug
3870 .field("stdout_bytes", &output.stdout.len())
3871 .field("stderr_bytes", &output.stderr.len());
3872 }
3873 debug.finish()
3874 }
3875}
3876
3877impl std::fmt::Display for SchedulerEndedRun {
3878 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3879 match self.outcome {
3880 detcore::TerminalOutcome::GuestDeadlock => write!(
3881 f,
3882 "Deadlock under Hermit's model: no remaining thread can be woken by anything Hermit models"
3883 ),
3884 detcore::TerminalOutcome::VirtualTimeout { bound_ns } => write!(
3885 f,
3886 "The run's virtual clock passed --virtual-timeout={bound_ns} (virtual \
3887 nanoseconds): a bound Hermit imposed, not a guest result"
3888 ),
3889 }
3890 }
3891}
3892
3893impl std::error::Error for SchedulerEndedRun {}
3894
3895async fn with_run_deadline<F>(timeout: Option<Duration>, guest: F) -> Result<ExitStatus, Error>
3901where
3902 F: std::future::Future<Output = Result<ExitStatus, Error>>,
3903{
3904 match timeout {
3905 None => guest.await,
3906 Some(limit) => match tokio::time::timeout(limit, guest).await {
3907 Ok(result) => result,
3908 Err(_) => Err(Error::new(GuestTimedOut {
3909 limit,
3910 survivors: None,
3911 })),
3912 },
3913 }
3914}
3915
3916#[tokio::main(flavor = "current_thread")]
3917async fn run_with_backend_inner(
3918 command: Command,
3919 config: DetConfig,
3920 print_summary: bool,
3921 print_summary_to_json_file: &Option<PathBuf>,
3922 backend: Backend,
3923 timeout: Option<Duration>,
3924) -> Result<ExitStatus, Error> {
3925 refuse_in_guest_liteinst_timeout(backend, timeout)?;
3926 let guest = Box::pin(dispatch_backend(
3930 command,
3931 config,
3932 print_summary,
3933 print_summary_to_json_file,
3934 backend,
3935 None,
3936 ));
3937 with_run_deadline(timeout, guest).await
3938}
3939
3940pub fn launched_image(command: &Command) -> Option<detcore::LaunchImage> {
3948 let program = command.find_program().ok()?;
3949 let cwd = match command.get_current_dir() {
3950 Some(cwd) => cwd.to_owned(),
3951 None => std::env::current_dir().ok()?,
3952 };
3953 let record = detcore::exec_record::ExecRecord::resolve(Path::new("/"), &cwd, &program)?;
3954 Some(detcore::LaunchImage {
3955 exe: Some(record.exe),
3956 launcher: None,
3957 })
3958}
3959
3960async fn dispatch_backend(
3961 command: Command,
3962 mut config: DetConfig,
3963 print_summary: bool,
3964 print_summary_to_json_file: &Option<PathBuf>,
3965 backend: Backend,
3966 control: Option<std::rc::Rc<ptrace_completion::Control>>,
3967) -> Result<ExitStatus, Error> {
3968 crate::cpu_profile_admission::refuse_unadmitted_launch(&config, backend == Backend::Ptrace)?;
3970 detcore_model::virtual_timeout::refuse_debugger(config.virtual_timeout, config.gdbserver)?;
3971 crate::preemption_admission::refuse_unqualified_annotated_launch(&config)?;
3972 detcore::preemptions::validate_config_replay_inputs(&config)?;
3973 <detcore::Detcore as reverie::Tool>::facility_admission(&config)?;
3976 config.launch_image = launched_image(&command);
3977 if backend == Backend::Kvm {
3978 return Ok(run_kvm(
3979 &command,
3980 config,
3981 print_summary,
3982 print_summary_to_json_file,
3983 false,
3984 )
3985 .await?
3986 .status);
3987 }
3988
3989 if backend.is_in_guest_liteinst() {
3990 #[cfg(feature = "liteinst")]
3991 {
3992 let stats_request = in_guest_stats_request(backend, print_summary_to_json_file);
3993 let mut command = command;
3994 refuse_in_guest_liteinst_run(&mut command, &config, backend)?;
3995 let (launch, _detlog_descriptor) =
3996 in_guest_liteinst_launch(&mut command, &in_guest_detlog_forward_policy())?;
3997 let audit = neutralize_inherited_signal_producers(&mut command, backend)?;
4000 let tool_data = launch.encode();
4001 let mut config = config;
4004 config.in_guest_detlog_forward_policy = _detlog_descriptor
4005 .is_some()
4006 .then(|| in_guest_detlog_forward_policy().encode());
4007 config.in_guest_site_patching_off = in_guest_site_patching_off(backend);
4008 config.in_guest_refuses_host_timed_producers =
4009 in_guest_refuses_host_timed_producers(backend);
4010 config.in_guest_reserved_signals = in_guest_reserved_signals(&config, backend);
4012 config.in_guest_signal_delivery = record_delivery_gate(
4016 runtime_delivers_signals()
4017 && in_guest_signal_delivery_gate(
4018 &config,
4019 backend,
4020 scheduler_owns_guest_pid_namespace(),
4021 ),
4022 );
4023 let preload = liteinst_tool_runtime_library_path()?;
4024 let exits = std::sync::Arc::new(in_guest_exits::InGuestExitAdmission::default());
4025 let (exit_status, mut global_state, dispatch_stats, process_reports) =
4026 reverie_liteinst::LiteinstBackend::with_connection_admission(
4027 exits.clone(),
4028 async {
4029 if stats_request.is_enabled() {
4030 let (exit_status, global_state, source) =
4031 reverie_liteinst::LiteinstBackend::run_with_preload_data_and_stats::<
4032 Detcore,
4033 >(command, config, preload, tool_data)
4034 .await?;
4035 let dispatch_stats =
4036 backend_stats::report(backend, stats_request, &source);
4037 let process_reports = source.snapshot().process_reports();
4038 Ok::<_, reverie::Error>((
4039 exit_status,
4040 global_state,
4041 dispatch_stats,
4042 process_reports,
4043 ))
4044 } else {
4045 let (exit_status, global_state) =
4046 reverie_liteinst::LiteinstBackend::run_with_preload_data::<Detcore>(
4047 command, config, preload, tool_data,
4048 )
4049 .await?;
4050 Ok((exit_status, global_state, None, 0))
4051 }
4052 },
4053 )
4054 .await
4055 .inspect_err(|error| exits.fail_launch(error))
4056 .map_err(|error| match &audit {
4059 Some(audit) => audit.launch_error(backend, error),
4060 None => Error::from(error),
4061 })?;
4062 let patched =
4063 in_guest_trap_patching_evidence(backend, dispatch_stats.as_ref(), process_reports);
4064 let settled = settle_in_guest_exits(&exits, &global_state).await;
4065 let policy_refusal = global_state.policy_refusal();
4066 if settled.is_err() || liteinst_requires_forced_shutdown(exit_status) {
4067 global_state.force_shutdown_with_error();
4068 global_state.cancel_internal_scheduler().await;
4069 }
4070 global_state.note_syscall_observation_gap(
4073 detcore::SyscallObservationGap::InProcessRuntimeStart,
4074 );
4075 let finalized = global_state
4076 .clean_up_with_dispatch_stats(
4077 print_summary,
4078 print_summary_to_json_file,
4079 dispatch_stats,
4080 )
4081 .await;
4082 detcore::host_timed_producers::emit_run_end_log();
4086 let result = (|| {
4087 refuse_in_guest_policy(backend, policy_refusal)?;
4088 settled?;
4089 refuse_in_guest_trap_patching(backend, patched)
4090 })();
4091 detcore::combine_finalization(result, finalized)?;
4092 return Ok(exit_status);
4093 }
4094 #[cfg(not(feature = "liteinst"))]
4095 {
4096 backend.ensure_available()?;
4097 unreachable!("LiteInst availability must fail when the feature is disabled");
4098 }
4099 }
4100 ensure_backend_dispatch(backend)?;
4101
4102 let stats_request = backend_stats::request(print_summary_to_json_file);
4103 let control = control
4104 .ok_or_else(|| Error::msg("ordinary ptrace requires its retained operation owner"))?;
4105 let tracer = hermit_run_ptrace::spawn_tracer(
4108 command,
4109 config.clone(),
4110 config
4111 .gdbserver
4112 .then(|| gdbserver::connection(config.gdbserver_port))
4113 .transpose()?,
4114 config.sequentialize_threads,
4115 stats_request,
4116 )
4117 .await?;
4118 let stats_source = tracer.backend_stats();
4119 let (exit_status, global_state) = ptrace_completion::wait(tracer, control).await?;
4120 let terminal_outcome = global_state.terminal_outcome();
4121 let dispatch_stats =
4122 backend_stats::report_if_collected(backend, stats_request, stats_source.as_ref());
4123 let cleanup = global_state
4124 .clean_up_with_dispatch_stats(print_summary, print_summary_to_json_file, dispatch_stats)
4125 .await;
4126 let completed = match terminal_outcome {
4127 Some(outcome) => Err(SchedulerEndedRun::new(outcome, None)),
4128 None => Ok(exit_status),
4129 };
4130 complete_ptrace_run(completed, cleanup, exit_status)
4131}
4132
4133fn complete_ptrace_run<T>(
4139 completed: Result<T, SchedulerEndedRun>,
4140 cleanup: Result<(), Error>,
4141 status: ExitStatus,
4142) -> Result<T, Error> {
4143 match cleanup {
4144 Ok(()) => completed.map_err(Error::new),
4145 Err(error) => {
4146 if error
4147 .downcast_ref::<detcore::FinalizationFailure>()
4148 .is_none()
4149 && error.is::<detcore::ReplayDepartureError>()
4150 && let Err(terminal) = completed
4151 {
4152 return Err(Error::new(terminal).context(
4153 error
4154 .downcast::<detcore::ReplayDepartureError>()
4155 .expect("type checked"),
4156 ));
4157 }
4158 Err(error.context(format!(
4159 "ptrace cleanup after observed guest status {status:?}"
4160 )))
4161 }
4162 }
4163}
4164
4165pub fn run_with_output(
4169 command: Command,
4170 config: DetConfig,
4171 print_summary: bool,
4172 print_summary_to_json_file: &Option<PathBuf>,
4173) -> Result<Output, Error> {
4174 run_with_output_backend(
4175 command,
4176 config,
4177 print_summary,
4178 print_summary_to_json_file,
4179 Backend::Ptrace,
4180 )
4181}
4182
4183pub fn run_with_output_backend(
4187 command: Command,
4188 config: DetConfig,
4189 print_summary: bool,
4190 print_summary_to_json_file: &Option<PathBuf>,
4191 backend: Backend,
4192) -> Result<Output, Error> {
4193 if backend == Backend::Kvm {
4194 ensure_kvm_stdin_reserved()?;
4195 }
4196 run_with_output_backend_timeout(
4197 command,
4198 config,
4199 print_summary,
4200 print_summary_to_json_file,
4201 backend,
4202 None,
4203 )
4204}
4205
4206pub fn run_with_output_backend_timeout(
4211 command: Command,
4212 config: DetConfig,
4213 print_summary: bool,
4214 print_summary_to_json_file: &Option<PathBuf>,
4215 backend: Backend,
4216 timeout: Option<Duration>,
4217) -> Result<Output, Error> {
4218 if backend == Backend::Ptrace {
4219 let summary_path = print_summary_to_json_file.clone();
4220 let command = tracer_thread::SendCommand::new(command);
4221 return ptrace_completion::run_on_tracer_thread(timeout, move |control| async move {
4222 let command = command.into_inner();
4223 let report = SkidOvershootReport::begin(true);
4224 let result = Box::pin(dispatch_output_backend(
4227 command,
4228 prepare_backend_config(config, backend),
4229 print_summary,
4230 &summary_path,
4231 backend,
4232 Some(control),
4233 ))
4234 .await;
4235 report.finish(result)
4236 });
4237 }
4238 let (output, skid_overshoots) = run_with_output_backend_timeout_and_skid_overshoots(
4239 command,
4240 config,
4241 print_summary,
4242 print_summary_to_json_file,
4243 backend,
4244 timeout,
4245 )?;
4246 if skid_overshoots > 0 {
4247 return Err(Error::new(SkidOvershootError::new(skid_overshoots)));
4248 }
4249 Ok(output)
4250}
4251
4252#[doc(hidden)]
4257pub fn run_with_output_backend_timeout_and_skid_overshoots(
4258 command: Command,
4259 config: DetConfig,
4260 print_summary: bool,
4261 print_summary_to_json_file: &Option<PathBuf>,
4262 backend: Backend,
4263 timeout: Option<Duration>,
4264) -> Result<(Output, u64), Error> {
4265 if backend == Backend::Ptrace {
4266 let summary_path = print_summary_to_json_file.clone();
4267 let command = tracer_thread::SendCommand::new(command);
4268 return ptrace_completion::run_on_tracer_thread(timeout, move |control| async move {
4269 let command = command.into_inner();
4270 let report = SkidOvershootReport::begin(true);
4271 let result = Box::pin(dispatch_output_backend(
4272 command,
4273 prepare_backend_config(config, backend),
4274 print_summary,
4275 &summary_path,
4276 backend,
4277 Some(control),
4278 ))
4279 .await;
4280 report.finish_with_count(result)
4281 });
4282 }
4283 let skid_overshoot_report =
4284 SkidOvershootReport::begin(backend.consumes_skid_overshoot_reports());
4285 if backend == Backend::Kvm {
4286 ensure_kvm_stdin_reserved()?;
4290 }
4291 let config = prepare_backend_config(config, backend);
4292 let result = run_with_output_backend_inner(
4293 command,
4294 config,
4295 print_summary,
4296 print_summary_to_json_file,
4297 backend,
4298 timeout,
4299 );
4300 skid_overshoot_report.finish_with_count(result)
4301}
4302
4303#[tokio::main(flavor = "current_thread")]
4304async fn run_with_output_backend_inner(
4305 command: Command,
4306 config: DetConfig,
4307 print_summary: bool,
4308 print_summary_to_json_file: &Option<PathBuf>,
4309 backend: Backend,
4310 timeout: Option<Duration>,
4311) -> Result<Output, Error> {
4312 refuse_in_guest_liteinst_timeout(backend, timeout)?;
4313 let guest = Box::pin(dispatch_output_backend(
4316 command,
4317 config,
4318 print_summary,
4319 print_summary_to_json_file,
4320 backend,
4321 None,
4322 ));
4323 match timeout {
4324 None => guest.await,
4325 Some(limit) => match tokio::time::timeout(limit, guest).await {
4326 Ok(result) => result,
4327 Err(_elapsed) => Err(Error::new(GuestTimedOut {
4328 limit,
4329 survivors: None,
4330 })),
4331 },
4332 }
4333}
4334
4335async fn dispatch_output_backend(
4336 mut command: Command,
4337 mut config: DetConfig,
4338 print_summary: bool,
4339 print_summary_to_json_file: &Option<PathBuf>,
4340 backend: Backend,
4341 control: Option<std::rc::Rc<ptrace_completion::Control>>,
4342) -> Result<Output, Error> {
4343 crate::cpu_profile_admission::refuse_unadmitted_launch(&config, backend == Backend::Ptrace)?;
4345 detcore_model::virtual_timeout::refuse_debugger(config.virtual_timeout, config.gdbserver)?;
4346 crate::preemption_admission::refuse_unqualified_annotated_launch(&config)?;
4347 detcore::preemptions::validate_config_replay_inputs(&config)?;
4348 <detcore::Detcore as reverie::Tool>::facility_admission(&config)?;
4349 config.launch_image = launched_image(&command);
4350 if backend == Backend::Kvm {
4351 return run_kvm(
4352 &command,
4353 config,
4354 print_summary,
4355 print_summary_to_json_file,
4356 true,
4357 )
4358 .await;
4359 }
4360
4361 if backend.is_in_guest_liteinst() {
4362 #[cfg(feature = "liteinst")]
4363 {
4364 command.stdin(output_backend_stdin()?);
4365 let stats_request = in_guest_stats_request(backend, print_summary_to_json_file);
4366 refuse_in_guest_liteinst_run(&mut command, &config, backend)?;
4367 let (launch, _detlog_descriptor) =
4368 in_guest_liteinst_launch(&mut command, &in_guest_detlog_forward_policy())?;
4369 let audit = neutralize_inherited_signal_producers(&mut command, backend)?;
4372 let tool_data = launch.encode();
4373 let mut config = config;
4376 config.in_guest_detlog_forward_policy = _detlog_descriptor
4377 .is_some()
4378 .then(|| in_guest_detlog_forward_policy().encode());
4379 config.in_guest_site_patching_off = in_guest_site_patching_off(backend);
4380 config.in_guest_refuses_host_timed_producers =
4381 in_guest_refuses_host_timed_producers(backend);
4382 config.in_guest_reserved_signals = in_guest_reserved_signals(&config, backend);
4384 config.in_guest_signal_delivery = record_delivery_gate(
4388 runtime_delivers_signals()
4389 && in_guest_signal_delivery_gate(
4390 &config,
4391 backend,
4392 scheduler_owns_guest_pid_namespace(),
4393 ),
4394 );
4395 let preload = liteinst_tool_runtime_library_path()?;
4396 let exits = std::sync::Arc::new(in_guest_exits::InGuestExitAdmission::default());
4397 let (output, mut global_state, dispatch_stats, process_reports) =
4398 reverie_liteinst::LiteinstBackend::with_connection_admission(exits.clone(), async {
4399 if stats_request.is_enabled() {
4400 let (output, global_state, source) =
4401 reverie_liteinst::LiteinstBackend::run_with_output_and_preload_data_and_stats::<
4402 Detcore,
4403 >(command, config, preload, tool_data)
4404 .await?;
4405 let dispatch_stats = backend_stats::report(backend, stats_request, &source);
4406 let process_reports = source.snapshot().process_reports();
4407 Ok::<_, reverie::Error>((output, global_state, dispatch_stats, process_reports))
4408 } else {
4409 let (output, global_state) =
4410 reverie_liteinst::LiteinstBackend::run_with_output_and_preload_data::<
4411 Detcore,
4412 >(command, config, preload, tool_data)
4413 .await?;
4414 Ok((output, global_state, None, 0))
4415 }
4416 })
4417 .await
4418 .inspect_err(|error| exits.fail_launch(error))
4419 .map_err(|error| match &audit {
4422 Some(audit) => audit.launch_error(backend, error),
4423 None => Error::from(error),
4424 })?;
4425 let patched =
4426 in_guest_trap_patching_evidence(backend, dispatch_stats.as_ref(), process_reports);
4427 let settled = settle_in_guest_exits(&exits, &global_state).await;
4428 let policy_refusal = global_state.policy_refusal();
4429 let output = Output {
4430 status: output.status.into(),
4431 stdout: output.stdout,
4432 stderr: output.stderr,
4433 };
4434 let status = output.status;
4435 if settled.is_err() || liteinst_requires_forced_shutdown(status) {
4436 global_state.force_shutdown_with_error();
4437 global_state.cancel_internal_scheduler().await;
4438 }
4439 global_state.note_syscall_observation_gap(
4442 detcore::SyscallObservationGap::InProcessRuntimeStart,
4443 );
4444 let finalized = global_state
4445 .clean_up_with_dispatch_stats(
4446 print_summary,
4447 print_summary_to_json_file,
4448 dispatch_stats,
4449 )
4450 .await;
4451 detcore::host_timed_producers::emit_run_end_log();
4455 let result = (|| {
4456 refuse_in_guest_policy(backend, policy_refusal)?;
4457 settled?;
4458 refuse_in_guest_trap_patching(backend, patched)
4459 })();
4460 detcore::combine_finalization(result, finalized)?;
4461 return Ok(Output {
4462 status,
4463 stdout: output.stdout,
4464 stderr: output.stderr,
4465 });
4466 }
4467 #[cfg(not(feature = "liteinst"))]
4468 {
4469 backend.ensure_available()?;
4470 unreachable!("LiteInst availability must fail when the feature is disabled");
4471 }
4472 }
4473 ensure_backend_dispatch(backend)?;
4474
4475 let stats_request = backend_stats::request(print_summary_to_json_file);
4476 command.stdin(output_backend_stdin()?);
4477 command.stdout(Stdio::piped());
4478 command.stderr(Stdio::piped());
4479 let control = control
4480 .ok_or_else(|| Error::msg("ordinary ptrace requires its retained operation owner"))?;
4481 let tracer = hermit_run_ptrace::spawn_tracer(
4484 command,
4485 config.clone(),
4486 config
4487 .gdbserver
4488 .then(|| gdbserver::connection(config.gdbserver_port))
4489 .transpose()?,
4490 config.sequentialize_threads,
4491 stats_request,
4492 )
4493 .await?;
4494 let stats_source = tracer.backend_stats();
4495 let (output, global_state) = ptrace_completion::wait_with_output(tracer, control).await?;
4496 let terminal_outcome = global_state.terminal_outcome();
4497 let dispatch_stats =
4498 backend_stats::report_if_collected(backend, stats_request, stats_source.as_ref());
4499 let cleanup = global_state
4500 .clean_up_with_dispatch_stats(print_summary, print_summary_to_json_file, dispatch_stats)
4501 .await;
4502 let status = output.status;
4503 let completed = match terminal_outcome {
4504 Some(outcome) => Err(SchedulerEndedRun::new(outcome, Some(output))),
4505 None => Ok(output),
4506 };
4507 complete_ptrace_run(completed, cleanup, status)
4508}
4509
4510pub struct HermitData {
4512 data_dir: PathBuf,
4515}
4516
4517fn collect_recording_ids(
4518 entries: impl IntoIterator<Item = io::Result<fs::DirEntry>>,
4519 data_dir: &Path,
4520 mut file_type: impl FnMut(&fs::DirEntry) -> io::Result<fs::FileType>,
4521) -> Result<Vec<Id>, Error> {
4522 let mut recordings = Vec::new();
4523 for entry in entries {
4524 let entry = entry.with_context(|| {
4525 format!(
4526 "Failed to read an entry in recording inventory {}",
4527 data_dir.display()
4528 )
4529 })?;
4530 let path = entry.path();
4531 if file_type(&entry)
4532 .with_context(|| {
4533 format!(
4534 "Failed to inspect recording inventory entry {}",
4535 path.display()
4536 )
4537 })?
4538 .is_dir()
4539 && let Some(id) = entry
4540 .file_name()
4541 .to_str()
4542 .and_then(|name| name.parse::<Id>().ok())
4543 {
4544 recordings.push(id);
4545 }
4546 }
4547 Ok(recordings)
4548}
4549
4550impl Default for HermitData {
4551 fn default() -> Self {
4552 Self::new()
4553 }
4554}
4555
4556impl HermitData {
4557 pub fn new() -> Self {
4560 Self::with_dir(
4561 dirs::cache_dir()
4562 .map_or_else(|| PathBuf::from("/tmp/hermit"), |dir| dir.join("hermit")),
4563 )
4564 }
4565
4566 pub fn with_dir<P>(data_dir: P) -> Self
4569 where
4570 P: Into<PathBuf>,
4571 {
4572 Self {
4573 data_dir: data_dir.into(),
4574 }
4575 }
4576
4577 pub fn data_dir(&self) -> &Path {
4579 &self.data_dir
4580 }
4581
4582 pub fn record(&self, command: Command) -> Result<Recording, Error> {
4597 let store = Self {
4598 data_dir: self.data_dir.clone(),
4599 };
4600 let command = tracer_thread::SendCommand::new(command);
4601 ptrace_completion::run_on_tracer_thread(None, move |control| async move {
4602 let command = command.into_inner();
4603 let data = store.create_recording_dir()?;
4604 let exit_status = record_to_async(
4605 command,
4606 data.path(),
4607 MountProvenance::default(),
4608 None,
4609 None,
4610 control,
4611 )
4612 .await?;
4613 store.commit_recording(data, exit_status)
4614 })
4615 }
4616
4617 pub fn record_with_mountinfo(
4622 &self,
4623 command: Command,
4624 mountinfo_root_rewrites: Vec<detcore_model::config::MountInfoRootRewrite>,
4625 ) -> Result<Recording, Error> {
4626 let store = Self {
4627 data_dir: self.data_dir.clone(),
4628 };
4629 let command = tracer_thread::SendCommand::new(command);
4630 ptrace_completion::run_on_tracer_thread(None, move |control| async move {
4631 let command = command.into_inner();
4632 let data = store.create_recording_dir()?;
4633 let mount_ids = capture_mountinfo_identity_order()?;
4634 let exit_status = record_to_async(
4635 command,
4636 data.path(),
4637 MountProvenance {
4638 root_rewrites: mountinfo_root_rewrites,
4639 container_mount_table: None,
4640 mount_ids: None,
4641 },
4642 Some(mount_ids),
4643 None,
4644 control,
4645 )
4646 .await?;
4647 store.commit_recording(data, exit_status)
4648 })
4649 }
4650
4651 pub fn create_recording_dir(&self) -> Result<tempfile::TempDir, Error> {
4653 let tmp_data_dir = self.data_dir.join("tmp");
4654
4655 fs::create_dir_all(&tmp_data_dir).with_context(|| {
4656 format!(
4657 "Failed to create recording directory: {}",
4658 self.data_dir.display()
4659 )
4660 })?;
4661
4662 Ok(tempfile::TempDir::new_in(tmp_data_dir)?)
4663 }
4664
4665 pub fn commit_recording(
4667 &self,
4668 data: tempfile::TempDir,
4669 exit_status: ExitStatus,
4670 ) -> Result<Recording, Error> {
4671 let id = Id::unique();
4672
4673 fs::rename(data.keep(), self.data_dir.join(id.to_string()))?;
4675
4676 self.note_latest(&id, true)
4677 .with_context(|| format!("Failed to update {:?}", self.data_dir.join(LAST)))?;
4678
4679 Ok(Recording { id, exit_status })
4680 }
4681
4682 pub fn replay(&self, id: Id) -> Result<ExitStatus, Error> {
4687 let data = self.data_dir.join(id.to_string());
4688 replay_from(&data)
4689 }
4690
4691 pub fn replay_with_gdbserver(&self, id: Id, port: u16) -> Result<ExitStatus, Error> {
4696 let data = self.data_dir.join(id.to_string());
4697 replay_with_gdbserver(&data, port)
4698 }
4699
4700 pub fn recordings(&self) -> impl Iterator<Item = Id> + use<> {
4704 fs::read_dir(&self.data_dir)
4705 .ok()
4706 .into_iter()
4707 .flatten()
4708 .filter_map(|entry| {
4709 let entry = entry.ok()?;
4710
4711 if entry.file_type().ok()?.is_dir() {
4712 Some(entry.file_name().to_str()?.parse::<Id>().ok()?)
4713 } else {
4714 None
4715 }
4716 })
4717 }
4718
4719 pub fn try_recordings(&self) -> Result<Vec<Id>, Error> {
4725 let entries = match fs::read_dir(&self.data_dir) {
4726 Ok(entries) => entries,
4727 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
4728 Err(error) => {
4729 return Err(error).with_context(|| {
4730 format!(
4731 "Failed to read recording inventory in {}",
4732 self.data_dir.display()
4733 )
4734 });
4735 }
4736 };
4737
4738 collect_recording_ids(entries, &self.data_dir, fs::DirEntry::file_type)
4739 }
4740
4741 pub fn recording_metadata(&self, id: Id) -> Result<Metadata, Error> {
4743 Metadata::load(&self.data_dir.join(id.to_string()))
4744 }
4745
4746 pub fn remove(&self, id: Id) -> Result<(), Error> {
4748 let path = self.data_dir.join(id.to_string());
4749
4750 let metadata_path = [METADATA_NAME, RUN_MANIFEST_NAME]
4755 .into_iter()
4756 .map(|name| path.join(name))
4757 .find(|marker| marker.exists())
4758 .unwrap_or_else(|| path.join(METADATA_NAME));
4759 let metadata = fs::metadata(&metadata_path)
4760 .with_context(|| format!("Failed to find {:?}", metadata_path))?;
4761
4762 if !metadata.is_file() {
4763 return Err(anyhow!("{:?} is not a file", metadata_path));
4764 }
4765
4766 fs::remove_dir_all(path)?;
4769
4770 Ok(())
4771 }
4772
4773 pub fn last_id(&self) -> Result<Id, Error> {
4775 Ok(fs::read_to_string(self.data_dir.join(LAST))?.parse()?)
4776 }
4777
4778 pub fn note_latest(&self, id: &Id, asked: bool) -> Result<(), Error> {
4784 let tmp = self.data_dir.join("tmp");
4785 fs::create_dir_all(&tmp)?;
4786 let names = if asked {
4787 &[LAST_RUN, LAST][..]
4788 } else {
4789 &[LAST_RUN][..]
4790 };
4791 for name in names {
4792 let mut file = tempfile::NamedTempFile::new_in(&tmp)?;
4793 write!(file, "{}", id)?;
4794 file.persist(self.data_dir.join(name))?;
4795 }
4796 Ok(())
4797 }
4798}
4799
4800#[derive(Clone, Debug, Default)]
4803pub struct MountProvenance {
4804 pub root_rewrites: Vec<detcore_model::config::MountInfoRootRewrite>,
4807 pub container_mount_table: Option<detcore_model::container_mounts::ContainerMountTable>,
4811 pub mount_ids: Option<Vec<u64>>,
4818}
4819
4820const NS_GET_MNTNS_ID: libc::c_ulong = (2 << 30) | (8 << 16) | (0xb7 << 8) | 5;
4822
4823pub fn current_mount_namespace() -> Result<detcore_model::config::MountNamespaceIdentity, Error> {
4832 use std::os::fd::AsRawFd;
4833 use std::os::unix::fs::MetadataExt;
4834 let file = fs::File::open("/proc/thread-self/ns/mnt")
4835 .context("failed to open this thread's mount namespace")?;
4836 let metadata = file
4837 .metadata()
4838 .context("failed to read this process's mount namespace")?;
4839 let mut id: u64 = 0;
4840 let id =
4843 if unsafe { libc::ioctl(file.as_raw_fd(), NS_GET_MNTNS_ID, &mut id as *mut u64) } == 0 {
4844 Some(id)
4845 } else {
4846 let error = std::io::Error::last_os_error();
4847 match error.raw_os_error() {
4848 Some(libc::ENOTTY | libc::EINVAL) => None,
4849 _ => {
4850 return Err(Error::new(error)
4851 .context("failed to read this process's mount namespace ID"));
4852 }
4853 }
4854 };
4855 Ok(detcore_model::config::MountNamespaceIdentity {
4856 id,
4857 device: metadata.dev(),
4858 inode: metadata.ino(),
4859 })
4860}
4861
4862pub fn thread_mountinfo() -> Result<Vec<u8>, Error> {
4865 fs::read("/proc/thread-self/mountinfo")
4866 .context("Failed to read this thread's mount table (/proc/thread-self/mountinfo)")
4867}
4868
4869fn thread_mount_id_of(path: &Path) -> Option<u64> {
4874 held_mount_id(&hold_path(path, false).ok()?)
4875}
4876
4877fn hold_path(path: &Path, directory: bool) -> nix::Result<std::os::fd::OwnedFd> {
4883 use nix::fcntl::OFlag;
4884 let flags = OFlag::O_PATH
4885 | OFlag::O_CLOEXEC
4886 | if directory {
4887 OFlag::O_DIRECTORY
4888 } else {
4889 OFlag::empty()
4890 };
4891 nix::fcntl::open(path, flags, nix::sys::stat::Mode::empty())
4892}
4893
4894fn held_mount_id(held: &std::os::fd::OwnedFd) -> Option<u64> {
4897 let fdinfo = fs::read(format!("/proc/thread-self/fdinfo/{}", held.as_raw_fd())).ok()?;
4898 detcore_model::procfs::parse_fdinfo_mount_id(&fdinfo)
4899}
4900
4901pub fn capture_mountinfo_identity_order() -> Result<Vec<u64>, Error> {
4904 mountinfo_identity_order_from_raw(&thread_mountinfo()?)
4905}
4906
4907pub fn capture_mountinfo_identity_order_keeping(
4912 keep: &std::collections::BTreeSet<u64>,
4913) -> Result<Vec<u64>, Error> {
4914 mountinfo_identity_order_keeping_from_raw(&thread_mountinfo()?, keep)
4915}
4916
4917pub fn base_mountpoint_mount_ids() -> Vec<(Vec<u8>, u64)> {
4923 detcore_model::container_mounts::BASE_MOUNTPOINTS
4924 .iter()
4925 .filter_map(|mountpoint| {
4926 let id = thread_mount_id_of(Path::new(mountpoint))?;
4927 Some((mountpoint.as_bytes().to_vec(), id))
4928 })
4929 .collect()
4930}
4931
4932pub type UnplannedMountTable = (
4935 detcore_model::container_mounts::ContainerMountTable,
4936 Vec<u64>,
4937);
4938
4939pub fn unplanned_container_mount_table(launch_cwd: &Path) -> Result<UnplannedMountTable, Error> {
4966 unplanned_container_mount_table_with(launch_cwd, thread_mountinfo)
4967}
4968
4969fn unplanned_container_mount_table_with(
4977 launch_cwd: &Path,
4978 read: impl FnOnce() -> Result<Vec<u8>, Error>,
4979) -> Result<UnplannedMountTable, Error> {
4980 let working = hold_path(launch_cwd, true).with_context(|| {
4981 format!(
4982 "the guest's working directory {} cannot be opened for its mount table",
4983 launch_cwd.display()
4984 )
4985 })?;
4986 let tmp = hold_path(Path::new("/tmp"), true).ok();
4988 let base = detcore_model::container_mounts::BASE_MOUNTPOINTS
4989 .iter()
4990 .filter_map(|mountpoint| Some((*mountpoint, hold_path(Path::new(mountpoint), false).ok()?)))
4991 .collect::<Vec<_>>();
4992 let raw = read()?;
4993 let launch = std::iter::once(&working)
4994 .chain(tmp.as_ref())
4995 .map(|held| {
4996 held_mount_id(held)
4997 .ok_or_else(|| anyhow!("a launch directory's mount ID cannot be read"))
4998 })
4999 .collect::<Result<std::collections::BTreeSet<_>, Error>>()?;
5000 let base = base
5001 .iter()
5002 .filter_map(|(mountpoint, held)| {
5003 Some((mountpoint.as_bytes().to_vec(), held_mount_id(held)?))
5004 })
5005 .collect::<Vec<_>>();
5006 unplanned_container_mount_table_from(&raw, &launch, &base)
5007}
5008
5009fn unplanned_container_mount_table_from(
5010 raw: &[u8],
5011 launch: &std::collections::BTreeSet<u64>,
5012 base: &[(Vec<u8>, u64)],
5013) -> Result<UnplannedMountTable, Error> {
5014 use detcore_model::container_mounts::ContainerMountTable;
5015 let rows = detcore_model::container_mounts::parse_container_mounts(raw)
5016 .ok_or_else(|| anyhow!("this thread's mount table has a malformed row"))?;
5017 if let Some(missing) = launch
5018 .iter()
5019 .find(|id| !rows.iter().any(|row| row.raw_mount_id == **id))
5020 {
5021 anyhow::bail!(
5022 "the launch mount {missing} (holding the guest's working directory or /tmp) is not in \
5023 this thread's mount table, read after it was resolved; refusing a table that would \
5024 omit the mount the guest starts on"
5025 );
5026 }
5027 let never = rows
5028 .iter()
5029 .filter(|row| row.is_ephemeral_host_seed())
5030 .map(|row| row.raw_mount_id)
5031 .collect();
5032 let table = ContainerMountTable::select(&rows, launch, base, &never);
5033 let order = mountinfo_identity_order_keeping_from_raw(raw, &table.shown_mount_ids())?;
5034 Ok((table, order))
5035}
5036
5037fn mountinfo_identity_order_from_raw(raw: &[u8]) -> Result<Vec<u64>, Error> {
5042 mountinfo_identity_order_keeping_from_raw(raw, &std::collections::BTreeSet::new())
5043}
5044
5045pub fn mountinfo_identity_order_keeping_from_raw(
5049 raw: &[u8],
5050 keep: &std::collections::BTreeSet<u64>,
5051) -> Result<Vec<u64>, Error> {
5052 let mut kept = Vec::with_capacity(raw.len());
5053 for line in raw.split_inclusive(|byte| *byte == b'\n') {
5054 let body = line.strip_suffix(b"\n").unwrap_or(line);
5055 let id = body
5056 .split(|byte| *byte == b' ')
5057 .next()
5058 .and_then(|field| std::str::from_utf8(field).ok()?.parse::<u64>().ok());
5059 if !detcore_model::procfs::is_ephemeral_host_seed_mount(body)
5060 || id.is_some_and(|id| keep.contains(&id))
5061 {
5062 kept.extend_from_slice(line);
5063 }
5064 }
5065 mountinfo_identity_order_from(&kept)
5066}
5067
5068fn mountinfo_identity_order_from(contents: &[u8]) -> Result<Vec<u64>, Error> {
5069 use std::collections::BTreeSet;
5070
5071 let rows = detcore_model::procfs::parse_mountinfo(contents)
5072 .ok_or_else(|| anyhow!("malformed mountinfo"))?;
5073 let mut visible = Vec::new();
5074 let mut parents = Vec::new();
5075 let mut seen = BTreeSet::new();
5076 for row in rows {
5077 seen.insert(row.raw_mount_id);
5078 visible.push(row.raw_mount_id);
5079 parents.push(row.raw_parent_id);
5080 }
5081 for parent in parents {
5082 if seen.insert(parent) {
5083 visible.push(parent);
5084 }
5085 }
5086 Ok(visible)
5087}
5088
5089impl<'a> From<Option<&'a PathBuf>> for HermitData {
5090 fn from(data_dir: Option<&'a PathBuf>) -> Self {
5091 data_dir.map_or_else(Self::new, Self::with_dir)
5092 }
5093}
5094
5095pub fn record_to(command: Command, dir: &Path) -> Result<ExitStatus, Error> {
5102 let dir = dir.to_owned();
5103 let command = tracer_thread::SendCommand::new(command);
5104 ptrace_completion::run_on_tracer_thread(None, move |control| async move {
5105 let command = command.into_inner();
5106 record_to_async(
5107 command,
5108 &dir,
5109 MountProvenance::default(),
5110 None,
5111 None,
5112 control,
5113 )
5114 .await
5115 })
5116}
5117
5118pub fn record_to_with_mountinfo(
5126 command: Command,
5127 dir: &Path,
5128 mountinfo: MountProvenance,
5129 options: RecordOptions,
5130) -> Result<ExitStatus, Error> {
5131 let dir = dir.to_owned();
5132 let command = tracer_thread::SendCommand::new(command);
5133 ptrace_completion::run_on_tracer_thread(None, move |control| async move {
5134 let command = command.into_inner();
5135 let mut mountinfo = mountinfo;
5136 let mount_ids = match mountinfo.mount_ids.take() {
5137 Some(mount_ids) => mount_ids,
5138 None => capture_mountinfo_identity_order_keeping(
5139 &mountinfo
5140 .container_mount_table
5141 .as_ref()
5142 .map(detcore_model::container_mounts::ContainerMountTable::shown_mount_ids)
5143 .unwrap_or_default(),
5144 )?,
5145 };
5146 record_to_async(
5147 command,
5148 &dir,
5149 mountinfo,
5150 Some(mount_ids),
5151 Some(options),
5152 control,
5153 )
5154 .await
5155 })
5156}
5157
5158async fn record_to_async(
5161 command: Command,
5162 dir: &Path,
5163 mountinfo: MountProvenance,
5164 mountinfo_mount_ids: Option<Vec<u64>>,
5165 options: Option<RecordOptions>,
5166 control: std::rc::Rc<ptrace_completion::Control>,
5167) -> Result<ExitStatus, Error> {
5168 let report = SkidOvershootReport::begin(true);
5169 let result = async {
5170 Record::spawn_with_mountinfo(command, dir, mountinfo, mountinfo_mount_ids, options)
5171 .await?
5172 .wait(control)
5173 .await
5174 }
5175 .await;
5176 report.finish(result)
5177}
5178
5179pub fn record_with_output(command: Command, dir: &Path) -> Result<Output, Error> {
5185 let dir = dir.to_owned();
5186 let command = tracer_thread::SendCommand::new(command);
5187 ptrace_completion::run_on_tracer_thread(None, move |control| async move {
5188 let command = command.into_inner();
5189 record_with_output_async(
5190 command,
5191 &dir,
5192 MountProvenance::default(),
5193 None,
5194 None,
5195 control,
5196 )
5197 .await
5198 })
5199}
5200
5201pub fn record_with_output_with_mountinfo(
5207 command: Command,
5208 dir: &Path,
5209 mountinfo: MountProvenance,
5210 options: RecordOptions,
5211) -> Result<Output, Error> {
5212 let dir = dir.to_owned();
5213 let command = tracer_thread::SendCommand::new(command);
5214 ptrace_completion::run_on_tracer_thread(None, move |control| async move {
5215 let command = command.into_inner();
5216 let mut mountinfo = mountinfo;
5217 let mount_ids = match mountinfo.mount_ids.take() {
5218 Some(mount_ids) => mount_ids,
5219 None => capture_mountinfo_identity_order_keeping(
5220 &mountinfo
5221 .container_mount_table
5222 .as_ref()
5223 .map(detcore_model::container_mounts::ContainerMountTable::shown_mount_ids)
5224 .unwrap_or_default(),
5225 )?,
5226 };
5227 record_with_output_async(
5228 command,
5229 &dir,
5230 mountinfo,
5231 Some(mount_ids),
5232 Some(options),
5233 control,
5234 )
5235 .await
5236 })
5237}
5238
5239async fn record_with_output_async(
5242 mut command: Command,
5243 dir: &Path,
5244 mountinfo: MountProvenance,
5245 mountinfo_mount_ids: Option<Vec<u64>>,
5246 options: Option<RecordOptions>,
5247 control: std::rc::Rc<ptrace_completion::Control>,
5248) -> Result<Output, Error> {
5249 let report = SkidOvershootReport::begin(true);
5250 command.stdin(Stdio::null());
5251 command.stdout(Stdio::piped());
5252 command.stderr(Stdio::piped());
5253 let result = async {
5254 Record::spawn_with_mountinfo(command, dir, mountinfo, mountinfo_mount_ids, options)
5255 .await?
5256 .wait_with_output(control)
5257 .await
5258 }
5259 .await;
5260 report.finish(result)
5261}
5262
5263pub fn replay_from(dir: &Path) -> Result<ExitStatus, Error> {
5269 replay_plain(dir, None, &[])
5270}
5271
5272pub fn replay_with_gdbserver(dir: &Path, port: u16) -> Result<ExitStatus, Error> {
5277 replay_plain(dir, Some(port), &[])
5278}
5279
5280pub fn replay_with_gdbserver_and_mounts(
5285 dir: &Path,
5286 port: u16,
5287 mounts: &[Mount],
5288) -> Result<ExitStatus, Error> {
5289 replay_plain(dir, Some(port), mounts)
5290}
5291
5292fn replay_plain(dir: &Path, port: Option<u16>, mounts: &[Mount]) -> Result<ExitStatus, Error> {
5293 let dir = dir.to_owned();
5294 let mounts = mounts.to_vec();
5295 ptrace_completion::run_on_tracer_thread(None, move |control| async move {
5296 let report = SkidOvershootReport::begin(true);
5297 let result = async {
5298 Replay::spawn(&dir, false, port, &mounts)
5299 .await?
5300 .wait(control)
5301 .await
5302 }
5303 .await;
5304 report.finish(result)
5305 })
5306}
5307
5308pub fn replay_with_output(dir: &Path) -> Result<Output, Error> {
5313 replay_with_output_and_mounts(dir, &[])
5314}
5315
5316pub fn replay_with_output_and_mounts(dir: &Path, mounts: &[Mount]) -> Result<Output, Error> {
5321 let dir = dir.to_owned();
5322 let mounts = mounts.to_vec();
5323 ptrace_completion::run_on_tracer_thread(None, move |control| async move {
5324 let report = SkidOvershootReport::begin(true);
5325 let result = async {
5326 Replay::spawn(&dir, true, None, &mounts)
5327 .await?
5328 .wait_with_output(control)
5329 .await
5330 }
5331 .await;
5332 report.finish(result)
5333 })
5334}
5335
5336#[cfg(all(test, feature = "liteinst"))]
5337mod in_guest_delivery_harness;
5338
5339#[cfg(test)]
5340mod tests {
5341 use super::*;
5342
5343 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
5347 enum CapabilityCase {
5348 Ptrace,
5349 Kvm,
5350 Liteinst,
5351 InGuestTrap,
5352 Dbt,
5353 Sabre,
5354 E9patch,
5355 }
5356
5357 fn prepare_capability_case(config: DetConfig, case: CapabilityCase) -> DetConfig {
5358 let core = match case {
5359 CapabilityCase::Ptrace => Some(Backend::Ptrace),
5360 CapabilityCase::Kvm => Some(Backend::Kvm),
5361 CapabilityCase::Liteinst => Some(Backend::Liteinst),
5362 CapabilityCase::InGuestTrap => Some(Backend::InGuestTrap),
5363 CapabilityCase::Dbt | CapabilityCase::Sabre | CapabilityCase::E9patch => None,
5364 };
5365 if let Some(core) = core {
5366 return prepare_backend_config(config, core);
5367 }
5368 let capabilities = match case {
5369 CapabilityCase::Dbt => reverie::BackendCapabilities::DBT,
5370 CapabilityCase::Sabre => reverie::BackendCapabilities::SABRE,
5371 CapabilityCase::E9patch => reverie::BackendCapabilities::PTRACE,
5373 _ => unreachable!("core capability cases return through their real owner"),
5374 };
5375 super::prepare_backend_config_with_capabilities(config, capabilities)
5376 }
5377
5378 static SKID_OVERSHOOT_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
5379
5380 #[test]
5386 fn standard_streams_are_grouped_by_the_descriptors_handed_over() {
5387 let directory = tempfile::tempdir().unwrap();
5388 let open = |name: &str| {
5389 fs::OpenOptions::new()
5390 .read(true)
5391 .write(true)
5392 .create(true)
5393 .truncate(false)
5394 .open(directory.path().join(name))
5395 .unwrap()
5396 };
5397 let reserved = open("reserved-stdin");
5398 let output = open("output");
5399 let output_copy = output.try_clone().unwrap();
5400 let error = open("error");
5401 let fd = |file: &fs::File| Some(file.as_raw_fd());
5402 assert_eq!(
5403 standard_stream_aliases([fd(&reserved), fd(&output), fd(&output_copy)]),
5404 Some([0, 1, 1])
5405 );
5406 let stdin_copy = output.try_clone().unwrap();
5407 assert_eq!(
5408 standard_stream_aliases([fd(&stdin_copy), fd(&output), fd(&error)]),
5409 Some([0, 0, 2])
5410 );
5411 assert_eq!(
5412 standard_stream_aliases([None, fd(&output), fd(&error)]),
5413 Some([0, 1, 2])
5414 );
5415 }
5416
5417 fn check_with_denied(
5421 namespaces: NamespaceUse,
5422 denied: &[CapabilityProbe],
5423 ) -> (Option<String>, Vec<CapabilityProbe>) {
5424 let mut ran = Vec::new();
5425 let result = check_tracing_environment(namespaces, |probe| {
5426 ran.push(probe);
5427 Ok(!denied.contains(&probe))
5428 });
5429 (result.err().map(|error| error.to_string()), ran)
5430 }
5431
5432 #[test]
5437 fn denied_namespaces_refuse_only_a_run_that_creates_them() {
5438 let denied = [CapabilityProbe::Namespaces];
5439 let (refusal, ran) = check_with_denied(NamespaceUse::Creates, &denied);
5440 assert!(
5441 refusal.as_deref().is_some_and(|refusal| {
5442 refusal.contains("unshare(CLONE_NEWUSER | CLONE_NEWPID | CLONE_NEWIPC)")
5443 }),
5444 "{refusal:?}"
5445 );
5446 assert_eq!(ran, [CapabilityProbe::Namespaces]);
5447 let (refusal, ran) = check_with_denied(NamespaceUse::None, &denied);
5448 assert_eq!(refusal, None);
5449 assert_eq!(ran, [CapabilityProbe::Ptrace, CapabilityProbe::Seccomp]);
5450 }
5451
5452 #[test]
5455 fn a_run_without_namespaces_still_needs_ptrace_and_seccomp() {
5456 for denied in [CapabilityProbe::Ptrace, CapabilityProbe::Seccomp] {
5457 let (refusal, _) = check_with_denied(NamespaceUse::None, &[denied]);
5458 let expected = match denied {
5459 CapabilityProbe::Ptrace => "PTRACE_TRACEME",
5460 _ => "SECCOMP_SET_MODE_FILTER",
5461 };
5462 assert!(
5463 refusal
5464 .as_deref()
5465 .is_some_and(|refusal| refusal.contains(expected)),
5466 "{denied:?}: {refusal:?}"
5467 );
5468 }
5469 let (refusal, ran) = check_with_denied(NamespaceUse::Creates, &[]);
5470 assert_eq!(refusal, None);
5471 assert_eq!(
5472 ran,
5473 [
5474 CapabilityProbe::Namespaces,
5475 CapabilityProbe::Ptrace,
5476 CapabilityProbe::Seccomp
5477 ]
5478 );
5479 }
5480
5481 #[test]
5486 fn a_manifest_only_recording_can_be_removed() {
5487 let directory = tempfile::tempdir().unwrap();
5488 let data = HermitData::with_dir(directory.path());
5489 let id = Id::unique();
5490 let recording = directory.path().join(id.to_string());
5491 fs::create_dir_all(&recording).unwrap();
5492 assert!(data.remove(id).is_err());
5493 fs::write(recording.join(MANIFEST_NAME), "{}").unwrap();
5494 data.remove(id).unwrap();
5495 assert!(!recording.exists());
5496 }
5497
5498 #[test]
5502 fn identity_capture_excludes_host_seed_mounts_and_keeps_order() {
5503 let raw = b"10 1 8:1 / / rw - ext4 /dev/root rw\n\
5504 76 10 0:50 / /mnt/xarfuse/uid-1/e62a203d-seed-nspid4026531836_cgpid16161-ns-4026531832 rw - fuse.squashfuse_ll squashfuse_ll rw\n\
5505 20 10 0:7 / /test rw - tmpfs tmpfs rw\n\
5506 30 10 0:51 / /mnt/xarfuse/stable-release rw - fuse.squashfuse_ll squashfuse_ll rw\n";
5507 let order = mountinfo_identity_order_from_raw(raw).unwrap();
5508 assert!(!order.contains(&76), "seed mount ID captured: {order:?}");
5509 assert_eq!(order, [10, 20, 30, 1]);
5510 assert_eq!(
5511 mountinfo_identity_order_from(raw).unwrap(),
5512 [10, 76, 20, 30, 1],
5513 "control: without the filter the seed ID is captured"
5514 );
5515 }
5516
5517 const NAMESPACE: detcore_model::config::MountNamespaceIdentity =
5519 detcore_model::config::MountNamespaceIdentity {
5520 id: Some(9001),
5521 device: 4,
5522 inode: 4026531840,
5523 };
5524
5525 #[test]
5526 fn kvm_mountinfo_config_preserves_captured_provenance() {
5527 let rewrite = detcore_model::config::MountInfoRootRewrite {
5528 raw_mount_id: 20,
5529 deterministic_root: b"/".to_vec(),
5530 raw_root_prefix: Some(b"/private-source".to_vec()),
5531 deterministic_root_prefix: Some(b"/tmp".to_vec()),
5532 raw_mountpoint_prefix: Some(b"/private-source".to_vec()),
5533 deterministic_mountpoint_prefix: Some(b"/tmp".to_vec()),
5534 };
5535 let mut config = DetConfig {
5536 mountinfo_mount_ids: vec![10, 20, 30],
5537 mountinfo_mount_ids_captured: true,
5538 mountinfo_mount_namespace: Some(NAMESPACE),
5539 mount_id_assignment_order: vec![700, 701],
5540 mountinfo_root_rewrites: vec![rewrite],
5541 mountinfo_device_rewrites: vec![(libc::makedev(0, 1), libc::makedev(8, 1))],
5542 ..DetConfig::default()
5543 };
5544 let mut expected = config.clone();
5545 expected.mountinfo_device_rewrites.clear();
5546 let (mut namespaces, mut captures) = (0, 0);
5547 prepare_kvm_mountinfo_config(
5548 &mut config,
5549 || {
5550 namespaces += 1;
5551 Ok(NAMESPACE)
5552 },
5553 || {
5554 captures += 1;
5555 Ok(vec![10, 30])
5556 },
5557 )
5558 .unwrap();
5559 assert_eq!(namespaces, 1, "the backend's namespace must be checked");
5560 assert_eq!(
5561 captures, 0,
5562 "the producer's snapshot is kept; the namespace is not read again"
5563 );
5564 assert_eq!(
5565 serde_json::to_value(&config).unwrap(),
5566 serde_json::to_value(&expected).unwrap(),
5567 "root rewrites, listed and assignment orders and every other setting must remain intact"
5568 );
5569 }
5570
5571 #[test]
5575 fn kvm_mountinfo_config_refuses_malformed_captured_provenance() {
5576 let rewrite = |raw_mount_id| detcore_model::config::MountInfoRootRewrite {
5577 raw_mount_id,
5578 deterministic_root: b"/".to_vec(),
5579 raw_root_prefix: None,
5580 deterministic_root_prefix: None,
5581 raw_mountpoint_prefix: None,
5582 deterministic_mountpoint_prefix: None,
5583 };
5584 let valid = DetConfig {
5585 mountinfo_mount_ids: vec![10, 20, 1],
5586 mountinfo_mount_ids_captured: true,
5587 mountinfo_mount_namespace: Some(NAMESPACE),
5588 mount_id_assignment_order: vec![20],
5589 mountinfo_root_rewrites: vec![rewrite(20)],
5590 ..DetConfig::default()
5591 };
5592 prepare_kvm_mountinfo_config(
5593 &mut valid.clone(),
5594 || Ok(NAMESPACE),
5595 || panic!("captured provenance is not read again"),
5596 )
5597 .unwrap();
5598 let cases = [
5599 (
5600 "duplicate",
5601 DetConfig {
5602 mountinfo_mount_ids: vec![10, 10],
5603 ..valid.clone()
5604 },
5605 "malformed",
5606 ),
5607 (
5608 "empty",
5609 DetConfig {
5610 mountinfo_mount_ids: vec![],
5611 ..valid.clone()
5612 },
5613 "malformed",
5614 ),
5615 (
5616 "assigned twice",
5617 DetConfig {
5618 mount_id_assignment_order: vec![20, 20],
5619 ..valid.clone()
5620 },
5621 "malformed",
5622 ),
5623 (
5624 "zero assigned, unlisted",
5625 DetConfig {
5626 mount_id_assignment_order: vec![0],
5627 ..valid.clone()
5628 },
5629 "malformed",
5630 ),
5631 (
5632 "rewrite outside",
5633 DetConfig {
5634 mountinfo_root_rewrites: vec![rewrite(30)],
5635 ..valid.clone()
5636 },
5637 "root rewrite names mount 30",
5638 ),
5639 ];
5640 for (name, mut config, expected) in cases {
5641 let before = serde_json::to_value(&config).unwrap();
5642 let error = prepare_kvm_mountinfo_config(
5643 &mut config,
5644 || panic!("{name}: malformed provenance is refused before any read"),
5645 || panic!("{name}: captured provenance is not read again"),
5646 )
5647 .unwrap_err();
5648 assert!(error.to_string().contains(expected), "{name}: {error:#}");
5649 assert_eq!(serde_json::to_value(&config).unwrap(), before, "{name}");
5650 }
5651 }
5652
5653 #[test]
5656 fn current_mount_namespace_is_stable_and_names_a_kernel_id_where_reported() {
5657 let first = current_mount_namespace().unwrap();
5658 assert_eq!(current_mount_namespace().unwrap(), first);
5659 assert_ne!(first.inode, 0);
5660 assert_ne!(first.id, Some(0));
5661 }
5662
5663 #[test]
5671 fn kvm_mountinfo_config_keeps_a_snapshot_whose_host_row_left_the_namespace() {
5672 let producer = b"10 1 8:1 / / rw - ext4 /dev/root rw\n\
5673 20 10 0:22 / /proc rw - proc proc rw\n\
5674 30 10 0:50 / /data/a rw - tmpfs tmpfs rw\n";
5675 let now = b"10 1 8:1 / / rw - ext4 /dev/root rw\n\
5676 20 10 0:22 / /proc rw - proc proc rw\n";
5677 let order = mountinfo_identity_order_from(producer).unwrap();
5678 let mut config = DetConfig {
5679 mountinfo_mount_ids: order.clone(),
5680 mountinfo_mount_ids_captured: true,
5681 mountinfo_mount_namespace: Some(NAMESPACE),
5682 ..DetConfig::default()
5683 };
5684 prepare_kvm_mountinfo_config(
5685 &mut config,
5686 || Ok(NAMESPACE),
5687 || mountinfo_identity_order_from(now),
5688 )
5689 .unwrap();
5690 assert_eq!(config.mountinfo_mount_ids, order);
5691 assert_eq!(config.mountinfo_mount_ids, [10, 20, 30, 1]);
5692 assert!(detcore_model::procfs::mount_ids_are_ordered_subset(
5693 &mountinfo_identity_order_from(now).unwrap(),
5694 &config.mountinfo_mount_ids
5695 ));
5696 }
5697
5698 #[test]
5699 fn kvm_mountinfo_config_refuses_provenance_of_another_namespace() {
5700 let config = DetConfig {
5701 mountinfo_mount_ids: vec![10, 20, 1],
5702 mountinfo_mount_ids_captured: true,
5703 mountinfo_mount_namespace: Some(NAMESPACE),
5704 mountinfo_device_rewrites: vec![(libc::makedev(0, 1), libc::makedev(8, 1))],
5705 ..DetConfig::default()
5706 };
5707 for backend in [
5710 detcore_model::config::MountNamespaceIdentity {
5711 id: Some(9002),
5712 ..NAMESPACE
5713 },
5714 detcore_model::config::MountNamespaceIdentity {
5715 inode: NAMESPACE.inode + 1,
5716 ..NAMESPACE
5717 },
5718 detcore_model::config::MountNamespaceIdentity {
5719 device: NAMESPACE.device + 1,
5720 ..NAMESPACE
5721 },
5722 detcore_model::config::MountNamespaceIdentity {
5723 id: None,
5724 ..NAMESPACE
5725 },
5726 ] {
5727 let mut stale = config.clone();
5728 let before = serde_json::to_value(&stale).unwrap();
5729 let error = prepare_kvm_mountinfo_config(
5730 &mut stale,
5731 || Ok(backend),
5732 || panic!("captured provenance is not read again"),
5733 )
5734 .unwrap_err();
5735 assert!(
5736 error.to_string().contains("name another mount namespace"),
5737 "{error:#}"
5738 );
5739 assert_eq!(serde_json::to_value(&stale).unwrap(), before);
5740 }
5741 let mut unnamed = DetConfig {
5742 mountinfo_mount_namespace: None,
5743 ..config.clone()
5744 };
5745 let before = serde_json::to_value(&unnamed).unwrap();
5746 let error = prepare_kvm_mountinfo_config(
5747 &mut unnamed,
5748 || panic!("provenance without a namespace is refused before any read"),
5749 || panic!("provenance without a namespace is refused before any read"),
5750 )
5751 .unwrap_err();
5752 assert!(
5753 error
5754 .to_string()
5755 .contains("do not name their mount namespace"),
5756 "{error:#}"
5757 );
5758 assert_eq!(serde_json::to_value(&unnamed).unwrap(), before);
5759 let mut unreadable = config;
5760 let before = serde_json::to_value(&unreadable).unwrap();
5761 let error = prepare_kvm_mountinfo_config(
5762 &mut unreadable,
5763 || Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied).into()),
5764 || panic!("captured provenance is not read again"),
5765 )
5766 .unwrap_err();
5767 assert!(
5768 error
5769 .downcast_ref::<std::io::Error>()
5770 .is_some_and(|error| error.kind() == std::io::ErrorKind::PermissionDenied)
5771 );
5772 assert_eq!(serde_json::to_value(&unreadable).unwrap(), before);
5773 }
5774
5775 #[test]
5776 fn kvm_mountinfo_config_captures_validated_ambient_namespace() {
5777 let contents = b"80 20 8:1 / / rw - ext4 /dev/root rw\n\
5778 10 80 0:7 / /test rw - tmpfs tmpfs rw\n";
5779 let mut config = DetConfig::default();
5780 let mut expected = config.clone();
5781 expected.mountinfo_mount_ids = vec![80, 10, 20];
5782 expected.mountinfo_mount_ids_captured = true;
5783 prepare_kvm_mountinfo_config(
5784 &mut config,
5785 || panic!("an ambient capture has no producer namespace to compare"),
5786 || mountinfo_identity_order_from(contents),
5787 )
5788 .unwrap();
5789 assert_eq!(
5790 serde_json::to_value(&config).unwrap(),
5791 serde_json::to_value(&expected).unwrap(),
5792 "visible rows precede parent-only IDs and no guest setting changes"
5793 );
5794 assert!(detcore_model::procfs::mount_ids_are_ordered_subset(
5795 &[80, 10],
5796 &config.mountinfo_mount_ids
5797 ));
5798 assert!(!detcore_model::procfs::mount_ids_are_ordered_subset(
5799 &[10, 80],
5800 &config.mountinfo_mount_ids
5801 ));
5802 assert!(!detcore_model::procfs::mount_ids_are_ordered_subset(
5803 &[80, 99],
5804 &config.mountinfo_mount_ids
5805 ));
5806 assert!(!detcore_model::procfs::mount_ids_are_ordered_subset(
5807 &[80, 80],
5808 &config.mountinfo_mount_ids
5809 ));
5810
5811 let mut empty = DetConfig::default();
5812 prepare_kvm_mountinfo_config(
5813 &mut empty,
5814 || panic!("an ambient capture has no producer namespace to compare"),
5815 || mountinfo_identity_order_from(b""),
5816 )
5817 .unwrap();
5818 assert!(empty.mountinfo_mount_ids.is_empty());
5819 assert!(empty.mountinfo_mount_ids_captured);
5820 }
5821
5822 #[test]
5823 fn kvm_mountinfo_config_refuses_missing_or_malformed_capture() {
5824 let valid = b"10 20 8:1 / / rw - ext4 /dev/root rw\n";
5825 let config = DetConfig {
5826 mountinfo_device_rewrites: vec![(libc::makedev(0, 1), libc::makedev(8, 1))],
5827 ..DetConfig::default()
5828 };
5829 for contents in [
5830 b"malformed".as_slice(),
5831 b"10 20 8:1 / / rw - ext4 /dev/root rw\n10 20 8:1 / / rw - ext4 /dev/root rw\n",
5832 b"10 20 8:1 / / rw unknown:7 - ext4 /dev/root rw\n",
5833 ] {
5834 let mut candidate = config.clone();
5835 let before = serde_json::to_value(&candidate).unwrap();
5836 let error = prepare_kvm_mountinfo_config(
5837 &mut candidate,
5838 || panic!("an ambient capture has no producer namespace to compare"),
5839 || mountinfo_identity_order_from(contents),
5840 )
5841 .unwrap_err();
5842 assert!(format!("{error:#}").contains("malformed mountinfo"));
5843 assert_eq!(serde_json::to_value(&candidate).unwrap(), before);
5844 }
5845 let mut candidate = config;
5846 let before = serde_json::to_value(&candidate).unwrap();
5847 let error = prepare_kvm_mountinfo_config(
5848 &mut candidate,
5849 || panic!("an ambient capture has no producer namespace to compare"),
5850 || Err(std::io::Error::from(std::io::ErrorKind::PermissionDenied).into()),
5851 )
5852 .unwrap_err();
5853 assert!(
5854 error
5855 .downcast_ref::<std::io::Error>()
5856 .is_some_and(|error| error.kind() == std::io::ErrorKind::PermissionDenied)
5857 );
5858 assert_eq!(serde_json::to_value(&candidate).unwrap(), before);
5859 assert_eq!(mountinfo_identity_order_from(valid).unwrap(), [10, 20]);
5860 }
5861
5862 #[test]
5863 fn kvm_mountinfo_config_refuses_uncaptured_provenance_vectors() {
5864 for (listed, assigned, namespace) in [
5865 (vec![10], vec![], None),
5866 (vec![], vec![700], None),
5867 (vec![], vec![], Some(NAMESPACE)),
5868 ] {
5869 let mut config = DetConfig {
5870 mountinfo_mount_ids: listed,
5871 mount_id_assignment_order: assigned,
5872 mountinfo_mount_namespace: namespace,
5873 ..DetConfig::default()
5874 };
5875 let before = serde_json::to_value(&config).unwrap();
5876 let error = prepare_kvm_mountinfo_config(
5877 &mut config,
5878 || panic!("must not overwrite incomplete provenance"),
5879 || panic!("must not overwrite incomplete provenance"),
5880 )
5881 .unwrap_err();
5882 assert!(error.to_string().contains("no producer-captured namespace"));
5883 assert_eq!(serde_json::to_value(&config).unwrap(), before);
5884 }
5885 }
5886
5887 #[test]
5891 fn kvm_mountinfo_config_refuses_a_mount_table_of_another_namespace() {
5892 let namespace = b"10 1 8:1 / / rw - ext4 /dev/root rw\n\
5893 20 10 0:22 / /proc rw - proc proc rw\n\
5894 40 10 0:23 / /elsewhere rw - tmpfs tmpfs rw\n";
5895 let rows = detcore_model::container_mounts::parse_container_mounts(namespace).unwrap();
5896 let table = |installed: &[u64]| {
5897 detcore_model::container_mounts::ContainerMountTable::select(
5898 &rows,
5899 &installed.iter().copied().collect(),
5900 &[],
5901 &std::collections::BTreeSet::new(),
5902 )
5903 };
5904 let config = |table| DetConfig {
5905 mountinfo_mount_ids: vec![10, 20, 1],
5906 mountinfo_mount_ids_captured: true,
5907 mountinfo_mount_namespace: Some(NAMESPACE),
5908 container_mount_table: Some(table),
5909 ..DetConfig::default()
5910 };
5911 let snapshot = || -> Result<Vec<u64>, Error> { panic!("the snapshot is kept") };
5912 let mut matching = config(table(&[20]));
5913 prepare_kvm_mountinfo_config(&mut matching, || Ok(NAMESPACE), snapshot).unwrap();
5914 let mut stale = config(table(&[40]));
5915 let error =
5916 prepare_kvm_mountinfo_config(&mut stale, || Ok(NAMESPACE), snapshot).unwrap_err();
5917 assert!(error.to_string().contains("names mount 40"), "{error:#}");
5918 }
5919
5920 #[test]
5925 fn an_unplanned_table_shows_only_the_base_mountpoints() {
5926 let namespace = b"10 1 8:1 / / rw - ext4 /dev/root rw\n\
5927 20 10 0:22 / /proc rw - proc proc rw\n\
5928 30 10 0:23 / /dev rw - devtmpfs dev rw\n\
5929 40 10 0:24 / /data/eden rw - fuse.edenfs eden rw\n\
5930 41 30 0:25 / /dev/shm rw - tmpfs tmpfs rw\n\
5931 50 10 0:26 / /run/user/1000 rw - tmpfs tmpfs rw\n";
5932 let base = [
5934 (b"/".to_vec(), 10),
5935 (b"/proc".to_vec(), 20),
5936 (b"/dev".to_vec(), 30),
5937 (b"/dev/shm".to_vec(), 41),
5938 (b"/run".to_vec(), 10),
5939 ];
5940 for mountpoint in detcore_model::container_mounts::BASE_MOUNTPOINTS {
5941 assert!(
5942 !mountpoint.bytes().any(|byte| b" \t\n\\".contains(&byte)),
5943 "{mountpoint} needs mountinfo escaping"
5944 );
5945 }
5946 let no_launch = std::collections::BTreeSet::new();
5947 let (table, order) =
5948 unplanned_container_mount_table_from(namespace, &no_launch, &base).unwrap();
5949 assert_eq!(table.shown_mount_ids(), [10, 20, 30, 41].into());
5950 assert_eq!(table.shown_mount_id(40), 10);
5951 assert_eq!(table.shown_mount_id(50), 10);
5952 assert_eq!(order, [10, 20, 30, 40, 41, 50, 1]);
5953 let rendered = String::from_utf8(table.mountinfo()).unwrap();
5954 assert!(!rendered.contains("/data/eden") && !rendered.contains("/run/user"));
5955 let launch = [40].into();
5958 let (table, order) =
5959 unplanned_container_mount_table_from(namespace, &launch, &base).unwrap();
5960 assert_eq!(table.shown_mount_ids(), [10, 20, 30, 40, 41].into());
5961 assert_eq!(table.shown_mount_id(40), 40);
5962 assert_eq!(table.shown_mount_id(50), 10);
5963 assert_eq!(order, [10, 20, 30, 40, 41, 50, 1]);
5964 }
5965
5966 fn rerun_in_user_namespace(name: &str) -> bool {
5973 const CHILD: &str = "HERMIT_TEST_USER_NAMESPACE_CHILD";
5974 if std::env::var_os(CHILD).is_some() {
5975 return false;
5976 }
5977 let probe = std::process::Command::new("unshare")
5978 .args(["--user", "--map-root-user", "--mount", "true"])
5979 .output();
5980 if !probe.as_ref().is_ok_and(|probe| probe.status.success()) {
5981 eprintln!("SKIPPED {name}: user namespaces are unavailable here: {probe:?}");
5982 return true;
5983 }
5984 let output = std::process::Command::new("unshare")
5985 .args(["--user", "--map-root-user", "--mount", "--"])
5986 .arg(std::env::current_exe().unwrap())
5987 .args(["--exact", name, "--nocapture", "--test-threads=1"])
5988 .env(CHILD, "1")
5989 .output()
5990 .expect("run the test under unshare");
5991 let stdout = String::from_utf8_lossy(&output.stdout);
5992 assert!(
5993 output.status.success() && stdout.contains("1 passed"),
5994 "the child failed or ran no test:\n{stdout}\n{}",
5995 String::from_utf8_lossy(&output.stderr)
5996 );
5997 true
5998 }
5999
6000 #[test]
6009 fn a_thread_with_its_own_mount_namespace_captures_its_own_view() {
6010 use std::os::unix::fs::MetadataExt;
6011 if rerun_in_user_namespace(
6012 "tests::a_thread_with_its_own_mount_namespace_captures_its_own_view",
6013 ) {
6014 return;
6015 }
6016 let checked = std::thread::spawn(|| {
6017 assert_eq!(
6019 unsafe { libc::unshare(libc::CLONE_NEWNS) },
6020 0,
6021 "unshare(CLONE_NEWNS): {}",
6022 io::Error::last_os_error()
6023 );
6024 let own = current_mount_namespace().unwrap();
6025 let leader = fs::metadata("/proc/self/ns/mnt").unwrap();
6026 assert_ne!(
6027 own.inode,
6028 leader.ino(),
6029 "the thread kept the leader's namespace"
6030 );
6031 let (table, order) = unplanned_container_mount_table(Path::new(".")).unwrap();
6032 assert!(
6033 table
6034 .mounts
6035 .iter()
6036 .any(|row| row.mountpoint.as_slice() == b"/"),
6037 "the thread's table shows no /: {table:?}"
6038 );
6039 let root = thread_mount_id_of(Path::new("/")).unwrap();
6040 assert!(table.shown_mount_ids().contains(&root));
6041 assert!(table.shown_mount_ids().iter().all(|id| order.contains(id)));
6042 })
6043 .join();
6044 assert!(checked.is_ok(), "the thread's checks failed");
6045 }
6046
6047 #[test]
6056 fn launch_directories_are_held_before_the_mount_table_is_read() {
6057 if rerun_in_user_namespace(
6058 "tests::launch_directories_are_held_before_the_mount_table_is_read",
6059 ) {
6060 return;
6061 }
6062 let directory = tempfile::tempdir().unwrap();
6063 let launch = directory.path().join("launch");
6064 fs::create_dir(&launch).unwrap();
6065 let before = thread_mount_id_of(&launch).unwrap();
6066 let target = launch.clone();
6067 let (table, _) = unplanned_container_mount_table_with(&launch, move || {
6068 let status = std::process::Command::new("mount")
6069 .args(["-t", "tmpfs", "none"])
6070 .arg(&target)
6071 .status()
6072 .expect("run mount");
6073 assert!(status.success(), "mount a tmpfs over the launch directory");
6074 thread_mountinfo()
6075 })
6076 .unwrap();
6077 let over = thread_mount_id_of(&launch).unwrap();
6078 assert_ne!(over, before, "the tmpfs did not cover the launch directory");
6079 assert!(table.shown_mount_ids().contains(&before));
6080 assert!(!table.shown_mount_ids().contains(&over));
6081 let _ = std::process::Command::new("umount").arg(&launch).status();
6082 }
6083
6084 #[test]
6087 fn a_launch_mount_missing_from_the_read_is_refused() {
6088 let namespace = b"10 1 8:1 / / rw - ext4 /dev/root rw\n";
6089 let error = unplanned_container_mount_table_from(namespace, &[99].into(), &[])
6090 .unwrap_err()
6091 .to_string();
6092 assert!(error.contains("launch mount 99"), "{error}");
6093 }
6094
6095 #[test]
6096 fn skid_overshoot_report_covers_success_error_and_disabled_backends() {
6097 let _lock = SKID_OVERSHOOT_TEST_LOCK.lock().unwrap();
6098 let _ = reverie::take_skid_overshoot_count();
6099
6100 reverie::record_skid_overshoot();
6101 let empty_run = SkidOvershootReport::begin(true);
6102 assert_eq!(
6103 reverie::take_skid_overshoot_count(),
6104 0,
6105 "begin must clear evidence from a previous invocation"
6106 );
6107 empty_run.finish(Ok::<_, Error>(())).unwrap();
6108
6109 let success = SkidOvershootReport::begin(true);
6110 reverie::record_skid_overshoot();
6111 reverie::record_skid_overshoot();
6112 let error = success.finish(Ok::<_, Error>(7)).unwrap_err();
6113 assert_eq!(
6114 error
6115 .downcast_ref::<SkidOvershootError>()
6116 .expect("skid refusal must remain typed")
6117 .count(),
6118 2
6119 );
6120 assert!(
6121 error
6122 .to_string()
6123 .contains("observed 2 HERMIT_SKID_OVERSHOOT report(s)"),
6124 "{error:#}"
6125 );
6126 assert!(
6127 error
6128 .to_string()
6129 .contains("deterministic execution was not established"),
6130 "{error:#}"
6131 );
6132 assert_eq!(reverie::take_skid_overshoot_count(), 0);
6133
6134 let error = SkidOvershootReport::begin(true);
6135 reverie::record_skid_overshoot();
6136 let error = error
6137 .finish(Err::<(), _>(anyhow!("guest failed")))
6138 .unwrap_err();
6139 assert!(error.downcast_ref::<SkidOvershootError>().is_some());
6140 let error = format!("{error:#}");
6141 assert!(error.contains("guest failed"), "{error}");
6142 assert!(
6143 error.contains("observed 1 HERMIT_SKID_OVERSHOOT report(s)"),
6144 "{error}"
6145 );
6146 assert_eq!(reverie::take_skid_overshoot_count(), 0);
6147
6148 reverie::record_skid_overshoot();
6149 let disabled = SkidOvershootReport::begin(false);
6150 disabled.finish(Ok::<_, Error>(())).unwrap();
6151 assert_eq!(
6152 reverie::take_skid_overshoot_count(),
6153 1,
6154 "a backend that cannot produce PMU overshoots must not consume another run's evidence"
6155 );
6156 }
6157
6158 #[test]
6163 fn timer_delivering_backends_consume_skid_overshoot_reports() {
6164 for backend in [Backend::Ptrace, Backend::Kvm] {
6165 assert!(backend.consumes_skid_overshoot_reports(), "{backend:?}");
6166 }
6167 for backend in [Backend::Liteinst, Backend::InGuestTrap] {
6168 assert!(!backend.consumes_skid_overshoot_reports(), "{backend:?}");
6169 }
6170 }
6171
6172 #[cfg(feature = "liteinst")]
6173 fn write_guest_program(directory: &Path, name: &str, bytes: &[u8], mode: u32) -> PathBuf {
6174 let path = directory.join(name);
6175 fs::write(&path, bytes).unwrap();
6176 fs::set_permissions(&path, fs::Permissions::from_mode(mode)).unwrap();
6177 assert_eq!(
6180 fs::metadata(&path).unwrap().permissions().mode() & 0o7777,
6181 mode,
6182 "{}",
6183 path.display()
6184 );
6185 path
6186 }
6187
6188 #[cfg(feature = "liteinst")]
6189 fn shebang_to(interpreter: &Path) -> Vec<u8> {
6190 let mut script = b"#!".to_vec();
6191 script.extend_from_slice(interpreter.as_os_str().as_bytes());
6192 script.extend_from_slice(b" -e\necho script\n");
6193 script
6194 }
6195
6196 #[test]
6197 #[cfg(feature = "liteinst")]
6198 fn in_guest_liteinst_runs_a_dynamically_linked_x86_64_program() {
6199 use interp::startup_fixtures::dynamic_executable;
6200
6201 let directory = tempfile::tempdir().unwrap();
6202 let program = write_guest_program(
6203 directory.path(),
6204 "dynamic",
6205 &dynamic_executable(false),
6206 0o755,
6207 );
6208 assert_eq!(
6209 in_guest_liteinst_program_gap(&mut Command::new(&program)).unwrap(),
6210 None
6211 );
6212 }
6213
6214 #[test]
6215 #[cfg(feature = "liteinst")]
6216 fn in_guest_liteinst_refuses_programs_that_start_before_the_runtime() {
6217 use interp::startup_fixtures::*;
6218
6219 let directory = tempfile::tempdir().unwrap();
6220 let cases: [(&str, Vec<u8>, &str); 4] = [
6221 (
6222 "static",
6223 elf(EM_X86_64, &[(PT_LOAD, b"code")]),
6224 "is statically linked",
6225 ),
6226 (
6227 "preinit",
6228 dynamic_executable(true),
6229 "has a DT_PREINIT_ARRAY",
6230 ),
6231 (
6232 "aarch64",
6233 elf(EM_AARCH64, &[(PT_INTERP, INTERP)]),
6234 "is not a 64-bit x86-64 ELF image",
6235 ),
6236 (
6237 "text",
6238 b"plain text, no #! line\n".to_vec(),
6239 "is neither an ELF executable nor a #! script",
6240 ),
6241 ];
6242 for (name, bytes, reason) in cases {
6243 let program = write_guest_program(directory.path(), name, &bytes, 0o755);
6244 let gap = in_guest_liteinst_program_gap(&mut Command::new(&program))
6245 .unwrap()
6246 .unwrap_or_else(|| panic!("{name} was not refused"));
6247 assert!(
6248 gap.contains(reason) && gap.contains(&program.display().to_string()),
6249 "{name}: {gap}"
6250 );
6251 }
6252 }
6253
6254 #[tokio::test]
6258 #[cfg(feature = "liteinst")]
6259 async fn a_recorded_in_guest_failure_fails_the_run_and_latches_a_loss() {
6260 use std::os::fd::FromRawFd;
6261
6262 use reverie::GlobalTool;
6263 use reverie_rpc_transport::ConnectionAdmission;
6264
6265 let config = DetConfig::default().with_backend(|backend| {
6266 backend.process_exits_complete_asynchronously = true;
6267 });
6268 let global_state = detcore::GlobalState::init_global_state(&config).await;
6269 let exits = in_guest_exits::InGuestExitAdmission::default();
6270 let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, std::process::id() as i32, 0) };
6272 assert!(fd >= 0);
6273 let pidfd = unsafe { std::os::fd::OwnedFd::from_raw_fd(fd as i32) };
6274 assert!(exits.admit(pidfd).is_err());
6275 let error = settle_in_guest_exits(&exits, &global_state)
6276 .await
6277 .expect_err("a recorded failure must fail the run");
6278 assert!(
6279 error
6280 .to_string()
6281 .starts_with("in-guest LiteInst refused a guest connection:"),
6282 "{error}"
6283 );
6284 assert!(detcore::detlog::determinism_loss().is_some());
6285 }
6286
6287 #[test]
6288 fn in_guest_trap_is_in_guest_liteinst_and_forces_site_patching_off() {
6289 assert!(Backend::Liteinst.is_in_guest_liteinst());
6290 assert!(Backend::InGuestTrap.is_in_guest_liteinst());
6291 for backend in [Backend::Ptrace, Backend::Kvm] {
6292 assert!(!backend.is_in_guest_liteinst(), "{backend:?}");
6293 assert_eq!(backend.runtime_name(), backend.as_str(), "{backend:?}");
6294 assert_eq!(forced_site_patching_value(backend), None, "{backend:?}");
6295 }
6296 assert_eq!(Backend::InGuestTrap.as_str(), "in-guest-trap");
6297 assert_eq!(Backend::InGuestTrap.runtime_name(), "liteinst");
6298 assert_eq!(Backend::Liteinst.runtime_name(), "liteinst");
6299 assert_eq!(forced_site_patching_value(Backend::Liteinst), None);
6301 assert_eq!(forced_site_patching_value(Backend::InGuestTrap), Some("0"));
6302 assert_eq!(
6304 <Backend as ValueEnum>::from_str("in-guest-trap", false),
6305 Ok(Backend::InGuestTrap)
6306 );
6307 assert!(Backend::ALL.contains(&Backend::InGuestTrap));
6308 for backend in Backend::ALL {
6309 let name = backend
6310 .to_possible_value()
6311 .expect("every backend is a visible CLI value");
6312 assert_eq!(name.get_name(), backend.as_str(), "{backend:?}");
6313 }
6314 }
6315
6316 #[test]
6322 #[cfg(feature = "liteinst")]
6323 fn in_guest_trap_refuses_a_guest_environment_that_turns_site_patching_on() {
6324 use std::ffi::OsStr;
6325
6326 let name = reverie_liteinst::SITE_PATCHING_ENV;
6327 let mut unset = Command::new("/bin/true");
6328 unset.env_remove(name);
6329 assert_eq!(
6330 in_guest_trap_site_patching_conflict(&unset, Backend::InGuestTrap),
6331 None
6332 );
6333 force_in_guest_site_patching(&mut unset, Backend::InGuestTrap);
6334 assert_eq!(unset.get_env(name).as_deref(), Some(OsStr::new("0")));
6335
6336 let mut zero = Command::new("/bin/true");
6337 zero.env(name, "0");
6338 assert_eq!(
6339 in_guest_trap_site_patching_conflict(&zero, Backend::InGuestTrap),
6340 None
6341 );
6342
6343 for value in ["1", "", "yes", " 0"] {
6344 let mut command = Command::new("/bin/true");
6345 command.env(name, value);
6346 let conflict = in_guest_trap_site_patching_conflict(&command, Backend::InGuestTrap)
6347 .unwrap_or_else(|| panic!("{value:?} was accepted"));
6348 assert!(conflict.contains(&format!("{name}={value},")), "{conflict}");
6349 assert!(conflict.contains("--backend=liteinst"), "{conflict}");
6350 assert!(conflict.contains("drop the variable"), "{conflict}");
6351 let error = refuse_in_guest_liteinst_run(
6354 &mut command,
6355 &DetConfig::default(),
6356 Backend::InGuestTrap,
6357 )
6358 .unwrap_err();
6359 let refusal = error
6360 .downcast_ref::<LiteinstInGuestRefusal>()
6361 .expect("a typed in-guest refusal, which the CLI reports as exit 122");
6362 assert!(
6363 refusal.to_string().starts_with(
6364 "--backend=in-guest-trap (in-guest LiteInst, site patching off) refuses \
6365 this run: the guest environment sets"
6366 ),
6367 "{refusal}"
6368 );
6369 assert_eq!(command.get_env(name).as_deref(), Some(OsStr::new(value)));
6370
6371 let mut liteinst = Command::new("/bin/true");
6373 liteinst.env(name, value);
6374 assert_eq!(
6375 in_guest_trap_site_patching_conflict(&liteinst, Backend::Liteinst),
6376 None
6377 );
6378 force_in_guest_site_patching(&mut liteinst, Backend::Liteinst);
6379 assert_eq!(liteinst.get_env(name).as_deref(), Some(OsStr::new(value)));
6380 }
6381 }
6382
6383 #[test]
6389 #[cfg(feature = "liteinst")]
6390 fn an_in_guest_trap_run_that_patched_a_site_is_refused_after_it_ends() {
6391 let record = |candidates: Option<u64>, patched: Option<u64>| {
6392 reverie::DispatchStats::new(
6393 "in-guest-trap",
6394 reverie::DispatchCounters::ZERO,
6395 reverie::SiteCounters {
6396 candidates,
6397 patched,
6398 fell_back: Some(0),
6399 },
6400 )
6401 };
6402 let clean = record(Some(0), Some(0));
6403 assert_eq!(
6404 in_guest_trap_patching_evidence(Backend::InGuestTrap, Some(&clean), 1),
6405 None
6406 );
6407 for (candidates, patched, shown) in [
6408 (Some(3), Some(3), "3 patched and 3 candidate"),
6409 (Some(1), Some(0), "0 patched and 1 candidate"),
6410 (Some(0), None, "unmeasured patched and 0 candidate"),
6411 ] {
6412 let reason = in_guest_trap_patching_evidence(
6413 Backend::InGuestTrap,
6414 Some(&record(candidates, patched)),
6415 1,
6416 )
6417 .unwrap_or_else(|| panic!("{candidates:?} {patched:?} was accepted"));
6418 assert!(reason.contains(shown), "{reason}");
6419 }
6420 assert!(
6421 in_guest_trap_patching_evidence(Backend::InGuestTrap, None, 1)
6422 .is_some_and(|reason| reason.contains("no LiteInst dispatch record"))
6423 );
6424 assert!(
6426 in_guest_trap_patching_evidence(Backend::InGuestTrap, Some(&clean), 0)
6427 .is_some_and(|reason| reason.contains("no guest process reported"))
6428 );
6429 let patched = record(Some(3), Some(3));
6430 assert_eq!(
6431 in_guest_trap_patching_evidence(Backend::Liteinst, Some(&patched), 1),
6432 None
6433 );
6434 assert_eq!(
6435 in_guest_trap_patching_evidence(Backend::Liteinst, None, 0),
6436 None
6437 );
6438
6439 refuse_in_guest_trap_patching(Backend::InGuestTrap, None).unwrap();
6440 let error = refuse_in_guest_trap_patching(
6441 Backend::InGuestTrap,
6442 in_guest_trap_patching_evidence(Backend::InGuestTrap, Some(&patched), 1),
6443 )
6444 .unwrap_err();
6445 assert!(
6446 error.downcast_ref::<LiteinstInGuestRefusal>().is_some(),
6447 "{error:#}"
6448 );
6449 assert!(
6450 error
6451 .to_string()
6452 .starts_with("--backend=in-guest-trap (in-guest LiteInst, site patching off)"),
6453 "{error:#}"
6454 );
6455 assert!(
6456 detcore::detlog::determinism_loss().is_some_and(|loss| loss.contains("3 patched")),
6457 "the refusal must record a determinism loss"
6458 );
6459 }
6460
6461 #[test]
6465 #[cfg(feature = "liteinst")]
6466 fn only_in_guest_trap_asks_tool_images_to_check_their_runtime() {
6467 for backend in Backend::ALL {
6468 let checks = in_guest_site_patching_off(backend);
6469 assert_eq!(checks, backend == Backend::InGuestTrap, "{backend:?}");
6470 if checks {
6471 assert!(in_guest_stats_request(backend, &None).is_enabled());
6472 }
6473 }
6474 assert!(!in_guest_stats_request(Backend::Liteinst, &None).is_enabled());
6475 }
6476
6477 #[test]
6486 fn a_poisoned_reserved_signal_set_never_survives_preparation() {
6487 let poisoned = DetConfig {
6488 in_guest_reserved_signals: u64::MAX,
6489 ..DetConfig::default()
6490 };
6491 for backend in Backend::ALL {
6492 let prepared = prepare_backend_config(poisoned.clone(), backend);
6493 assert_eq!(prepared.in_guest_reserved_signals, 0, "{backend:?}");
6494 let expected = if backend.is_in_guest_liteinst() {
6495 in_guest_reserved_signals(
6496 &prepare_backend_config(DetConfig::default(), backend),
6497 backend,
6498 )
6499 } else {
6500 0
6501 };
6502 assert_eq!(
6503 in_guest_reserved_signals(&prepared, backend),
6504 expected,
6505 "{backend:?}"
6506 );
6507 }
6508 let loader = prepare_in_guest_loader_route_config(poisoned);
6509 assert_eq!(loader.in_guest_reserved_signals, 0);
6510 assert!(loader.in_guest_site_patching_off);
6511 assert_eq!(loader.backend, backend_capabilities(Backend::InGuestTrap));
6512 }
6513
6514 #[test]
6522 fn a_poisoned_producer_refusal_never_survives_preparation() {
6523 let poisoned = DetConfig {
6524 in_guest_refuses_host_timed_producers: true,
6525 ..DetConfig::default()
6526 };
6527 for backend in Backend::ALL {
6528 let prepared = prepare_backend_config(poisoned.clone(), backend);
6529 assert!(
6530 !prepared.in_guest_refuses_host_timed_producers,
6531 "{backend:?}"
6532 );
6533 assert_eq!(
6534 in_guest_refuses_host_timed_producers(backend),
6535 matches!(backend, Backend::Liteinst | Backend::InGuestTrap),
6536 "{backend:?}"
6537 );
6538 }
6539 let loader = prepare_in_guest_loader_route_config(poisoned);
6540 assert!(!loader.in_guest_refuses_host_timed_producers);
6541 assert!(loader.in_guest_site_patching_off);
6542 }
6543
6544 fn gate_ready(backend: Backend) -> DetConfig {
6548 let mut config = prepare_backend_config(
6549 DetConfig {
6550 sequentialize_threads: true,
6551 max_timeslice: None,
6552 ..DetConfig::default()
6553 },
6554 backend,
6555 );
6556 config.in_guest_site_patching_off = true;
6557 config
6558 }
6559
6560 #[test]
6569 fn a_poisoned_signal_delivery_gate_never_survives_preparation() {
6570 let poisoned = DetConfig {
6571 in_guest_signal_delivery: true,
6572 sequentialize_threads: true,
6573 max_timeslice: None,
6574 in_guest_site_patching_off: true,
6575 ..DetConfig::default()
6576 };
6577 for backend in Backend::ALL {
6578 let prepared = prepare_backend_config(poisoned.clone(), backend);
6579 assert!(!prepared.in_guest_signal_delivery, "{backend:?}");
6580 }
6581 let loader = prepare_in_guest_loader_route_config(poisoned);
6582 assert!(!loader.in_guest_signal_delivery);
6583 assert!(loader.in_guest_site_patching_off);
6584 assert_eq!(loader.backend, backend_capabilities(Backend::InGuestTrap));
6585 }
6586
6587 #[test]
6592 fn the_signal_delivery_gate_needs_every_condition() {
6593 let owned = true;
6594 for backend in Backend::ALL {
6595 let ready = gate_ready(backend);
6596 assert_eq!(
6597 in_guest_signal_delivery_gate(&ready, backend, owned),
6598 backend.is_in_guest_liteinst(),
6599 "{backend:?}"
6600 );
6601 }
6602 for backend in [Backend::Liteinst, Backend::InGuestTrap] {
6603 let ready = gate_ready(backend);
6604 assert!(detcore::two_phase_refusal(&ready).is_none(), "{backend:?}");
6605 let off = |change: &dyn Fn(&mut DetConfig)| {
6606 let mut config = ready.clone();
6607 change(&mut config);
6608 !in_guest_signal_delivery_gate(&config, backend, owned)
6609 };
6610 assert!(
6611 off(&|c| c.in_guest_site_patching_off = false),
6612 "{backend:?}"
6613 );
6614 assert!(off(&|c| c.sequentialize_threads = false), "{backend:?}");
6615 assert!(
6617 off(&|c| c.target_timeslice = std::num::NonZeroU64::new(1_000)),
6618 "{backend:?}"
6619 );
6620 assert!(
6621 off(&|c| c.max_timeslice = std::num::NonZeroU64::new(1_000)),
6622 "{backend:?}"
6623 );
6624 assert!(off(&|c| c.record_preemptions = true), "{backend:?}");
6625 assert!(
6626 off(&|c| c.replay_schedule_from = Some(PathBuf::from("schedule.json"))),
6627 "{backend:?}"
6628 );
6629 let mut pipe = ready.clone();
6630 pipe.pipe_wake_latency = Some(1_000);
6631 assert!(pipe.pipe_wake_latency_in_effect().is_some());
6632 assert!(
6633 !in_guest_signal_delivery_gate(&pipe, backend, owned),
6634 "{backend:?}"
6635 );
6636 assert!(
6638 !in_guest_signal_delivery_gate(&ready, backend, false),
6639 "{backend:?}"
6640 );
6641 }
6642 }
6643
6644 fn ownership_in_child(new_pid_namespace: bool) -> i32 {
6648 let observe = || i32::from(scheduler_owns_guest_pid_namespace());
6649 unsafe {
6652 let launcher = libc::fork();
6653 assert!(launcher >= 0, "fork failed");
6654 if launcher == 0 {
6655 if !new_pid_namespace {
6656 libc::_exit(observe());
6657 }
6658 if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWPID) != 0 {
6659 libc::_exit(2);
6660 }
6661 let init = libc::fork();
6662 if init == 0 {
6663 libc::_exit(observe());
6664 }
6665 let mut status = 0;
6666 if init < 0
6667 || libc::waitpid(init, &mut status, 0) != init
6668 || !libc::WIFEXITED(status)
6669 {
6670 libc::_exit(2);
6671 }
6672 libc::_exit(libc::WEXITSTATUS(status));
6673 }
6674 let mut status = 0;
6675 assert_eq!(libc::waitpid(launcher, &mut status, 0), launcher);
6676 assert!(libc::WIFEXITED(status), "wait status {status:#x}");
6677 libc::WEXITSTATUS(status)
6678 }
6679 }
6680
6681 #[test]
6687 fn only_a_pid_namespace_init_owns_the_guests_namespace() {
6688 assert!(!scheduler_owns_guest_pid_namespace());
6689 assert_eq!(
6690 ownership_in_child(false),
6691 0,
6692 "a plain child owns no namespace"
6693 );
6694 assert_eq!(
6695 ownership_in_child(true),
6696 1,
6697 "the init of a new PID namespace owns it (2: no user and PID namespace)"
6698 );
6699 }
6700
6701 #[test]
6706 #[cfg(feature = "liteinst")]
6707 fn no_launch_outside_a_new_pid_namespace_and_no_3c1a_launch_turns_the_gate_on() {
6708 for backend in Backend::ALL {
6709 let ready = gate_ready(backend);
6710 assert!(
6711 !in_guest_signal_delivery_gate(
6712 &ready,
6713 backend,
6714 scheduler_owns_guest_pid_namespace()
6715 ),
6716 "{backend:?}"
6717 );
6718 for owned in [false, true] {
6719 assert!(
6720 !(IN_GUEST_RUNTIME_DELIVERS_SIGNALS
6721 && in_guest_signal_delivery_gate(&ready, backend, owned)),
6722 "{backend:?} {owned}"
6723 );
6724 }
6725 }
6726 }
6727
6728 #[test]
6732 #[cfg(feature = "liteinst")]
6733 fn an_in_guest_launch_logs_the_cpu_limit_it_found_and_refuses_one_it_cannot_raise() {
6734 let limit = |rlim_cur, rlim_max| libc::rlimit { rlim_cur, rlim_max };
6735 let unchanged = rlimit_cpu_report_line(limit(libc::RLIM_INFINITY, libc::RLIM_INFINITY));
6736 assert!(
6737 unchanged.contains(
6738 "was soft unlimited hard unlimited, the value Detcore reports; the guest starts \
6739 with it unchanged"
6740 ),
6741 "{unchanged}"
6742 );
6743 let raised = rlimit_cpu_report_line(limit(1, libc::RLIM_INFINITY));
6744 assert!(
6745 raised.contains(
6746 "was soft 1s hard unlimited; Hermit sets it to soft unlimited hard unlimited"
6747 ),
6748 "{raised}"
6749 );
6750 let refused = rlimit_cpu_refusal(limit(3600, 7200), nix::errno::Errno::EPERM);
6751 assert!(
6752 refused.starts_with(
6753 "the guest would inherit a finite physical RLIMIT_CPU (soft 3600s, hard 7200s), \
6754 and Hermit cannot raise it (EPERM: "
6755 ),
6756 "{refused}"
6757 );
6758 assert!(refused.contains("D25"), "{refused}");
6759 }
6760
6761 #[test]
6767 #[cfg(feature = "liteinst")]
6768 fn the_launch_audit_reports_what_the_guest_check_found() {
6769 use std::os::fd::AsRawFd;
6770 let mut command = Command::new("/bin/true");
6771 let audit = LaunchAudit::install(&mut command).unwrap();
6772 let pipe = &audit.pipe;
6773 assert_eq!(pipe.read_report(), None);
6774 let write = |report: LaunchReport| {
6775 let bytes = report.encode();
6776 let written =
6778 unsafe { libc::write(pipe._write.as_raw_fd(), bytes.as_ptr().cast(), bytes.len()) };
6779 assert_eq!(written, bytes.len() as isize);
6780 };
6781 let report = |kind, bits, errno: i32, fd, rlimit| LaunchReport {
6782 kind,
6783 bits,
6784 errno: errno as u8,
6785 fd,
6786 rlimit,
6787 };
6788 for (sent, what) in [
6789 (
6790 report(
6791 audit_bits::PRODUCER,
6792 audit_bits::O_ASYNC | audit_bits::OWNER,
6793 0,
6794 0,
6795 (0, 0),
6796 ),
6797 "the guest would inherit descriptor 0, which is already a signal producer: it has \
6798 O_ASYNC and an owner",
6799 ),
6800 (
6801 report(
6802 audit_bits::PRODUCER,
6803 audit_bits::SOCKET | audit_bits::OWNER,
6804 0,
6805 1,
6806 (0, 0),
6807 ),
6808 "descriptor 1, which is already a signal producer: it is a socket with an owner",
6809 ),
6810 (
6811 report(audit_bits::PRODUCER, audit_bits::LEASE, 0, 300, (0, 0)),
6812 "descriptor 300, which is already a signal producer: it holds a lease",
6813 ),
6814 (
6815 report(audit_bits::UNREADABLE, 0, libc::EACCES, 7, (0, 0)),
6816 "descriptor 7, whose signal owner and flags cannot be read (EACCES",
6817 ),
6818 (
6819 report(audit_bits::UNLISTED, 0, libc::EMFILE, -1, (0, 0)),
6820 "descriptor table could not be listed from /proc/self/fd (EMFILE",
6821 ),
6822 (
6823 report(audit_bits::RLIMIT_CPU, 0, libc::EPERM, -1, (3600, 3600)),
6824 "finite physical RLIMIT_CPU (soft 3600s, hard 3600s), and Hermit cannot raise it \
6825 (EPERM",
6826 ),
6827 ] {
6828 write(sent);
6829 let received = pipe.read_report().unwrap();
6830 assert_eq!(received, sent);
6831 let reason = received.refusal().unwrap();
6832 assert!(reason.contains(what), "{reason}");
6833 }
6834 let passed = report(audit_bits::PASSED, 0, 0, -1, (1, libc::RLIM_INFINITY));
6835 write(passed);
6836 let received = pipe.read_report().unwrap();
6837 assert_eq!(received.refusal(), None);
6838 assert_eq!(
6839 rlimit_cpu_report_line(received.limit()),
6840 rlimit_cpu_report_line(libc::rlimit {
6841 rlim_cur: 1,
6842 rlim_max: libc::RLIM_INFINITY
6843 })
6844 );
6845 assert_eq!(pipe.read_report(), None);
6846 }
6847
6848 #[test]
6853 #[cfg(feature = "liteinst")]
6854 fn the_inherited_descriptor_scan_finds_a_producer_without_close_on_exec() {
6855 use std::os::fd::AsRawFd;
6856 let (read, write) = nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC).unwrap();
6857 drop(write);
6858 let fd = read.as_raw_fd();
6859 unsafe {
6862 assert_eq!(libc::fcntl(fd, libc::F_SETOWN, libc::getpid()), 0);
6863 let flags = libc::fcntl(fd, libc::F_GETFL);
6864 assert_eq!(libc::fcntl(fd, libc::F_SETFL, flags | libc::O_ASYNC), 0);
6865 }
6866 assert_eq!(check_inherited_descriptor(fd, -1), None);
6867 unsafe { assert_eq!(libc::fcntl(fd, libc::F_SETFD, 0), 0) };
6869 let expected = LaunchReport {
6870 kind: audit_bits::PRODUCER,
6871 bits: audit_bits::O_ASYNC | audit_bits::OWNER,
6872 errno: 0,
6873 fd,
6874 rlimit: (0, 0),
6875 };
6876 assert_eq!(check_inherited_descriptor(fd, -1), Some(expected));
6877 assert_eq!(
6878 check_inherited_descriptor(fd, fd),
6879 None,
6880 "the audit's own pipe"
6881 );
6882 assert_eq!(audit_inherited_descriptors(-1), Some(expected));
6885 unsafe { assert_eq!(libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC), 0) };
6887 }
6888
6889 #[test]
6895 #[cfg(feature = "liteinst")]
6896 fn an_inherited_signal_producer_is_refused_by_descriptor() {
6897 use detcore::host_timed_producers::ProducerState;
6898 let state = |o_async, owner, socket| ProducerState {
6899 o_async,
6900 owner,
6901 socket,
6902 };
6903 for (producer, lease, what) in [
6904 (state(true, true, false), false, "O_ASYNC and an owner"),
6905 (state(false, true, true), false, "a socket with an owner"),
6906 (state(false, false, false), true, "holds a lease"),
6907 (
6908 state(false, true, false),
6909 false,
6910 "it has an owner, which a directory",
6911 ),
6912 ] {
6913 let reason = inherited_producer_refusal(7, producer, lease).unwrap();
6914 assert!(
6915 reason.starts_with("the guest would inherit descriptor 7, "),
6916 "{reason}"
6917 );
6918 assert!(reason.contains(what), "{reason}");
6919 }
6920 for state in [
6921 state(true, false, false),
6922 state(false, false, true),
6923 state(true, false, true),
6924 ] {
6925 assert_eq!(
6926 inherited_producer_refusal(7, state, false),
6927 None,
6928 "{state:?}"
6929 );
6930 }
6931 }
6932
6933 #[test]
6942 fn in_guest_reserved_signals_follow_the_backend_the_subscription_and_site_patching() {
6943 let bit = |signal: libc::c_int| 1_u64 << (signal - 1);
6944 for backend in Backend::ALL {
6945 for patching_off in [false, true] {
6946 let mut config = prepare_backend_config(DetConfig::default(), backend);
6947 assert_eq!(config.in_guest_reserved_signals, 0, "{backend:?}");
6948 config.in_guest_site_patching_off = patching_off;
6949 let router = in_guest_guard_router_installed(&config, backend);
6950 let reserved = in_guest_reserved_signals(&config, backend);
6951 let label = format!("{backend:?} patching_off={patching_off}");
6952 if !backend.is_in_guest_liteinst() {
6953 assert!(!router, "{label}");
6954 assert_eq!(reserved, 0, "{label}");
6955 continue;
6956 }
6957 assert_eq!(router, !patching_off, "{label}");
6958 let trap = if patching_off { 0 } else { bit(libc::SIGTRAP) };
6959 assert!(<Detcore as reverie::Tool>::subscriptions(&config).has_rdtsc());
6961 assert_eq!(
6962 reserved,
6963 bit(libc::SIGSYS) | bit(libc::SIGSEGV) | trap,
6964 "{label}"
6965 );
6966
6967 let mut unsubscribed = config.clone();
6970 unsubscribed.passthru_opt = true;
6971 unsubscribed.virtualize_time = false;
6972 unsubscribed.virtualize_cpuid = false;
6973 let subscription = <Detcore as reverie::Tool>::subscriptions(&unsubscribed);
6974 assert!(!subscription.has_rdtsc() && !subscription.has_cpuid());
6975 assert_eq!(
6976 in_guest_reserved_signals(&unsubscribed, backend),
6977 bit(libc::SIGSYS) | trap,
6978 "{label}"
6979 );
6980 for (time, cpuid) in [(true, false), (false, true)] {
6981 let mut subscribed = unsubscribed.clone();
6982 subscribed.virtualize_time = time;
6983 subscribed.virtualize_cpuid = cpuid;
6984 assert_eq!(
6985 in_guest_reserved_signals(&subscribed, backend),
6986 bit(libc::SIGSYS) | bit(libc::SIGSEGV) | trap,
6987 "{label} time={time} cpuid={cpuid}"
6988 );
6989 }
6990 }
6991 }
6992 }
6993
6994 #[test]
7001 #[cfg(feature = "liteinst")]
7002 fn the_default_route_reserves_sigtrap_only_with_site_patching_on() {
7003 let bit = |signal: libc::c_int| 1_u64 << (signal - 1);
7004 for backend in Backend::ALL {
7005 let mut config = prepare_backend_config(DetConfig::default(), backend);
7006 config.in_guest_site_patching_off = in_guest_site_patching_off(backend);
7007 let reserved = in_guest_reserved_signals(&config, backend);
7008 let expected = match backend {
7009 Backend::Liteinst => bit(libc::SIGSYS) | bit(libc::SIGSEGV) | bit(libc::SIGTRAP),
7010 Backend::InGuestTrap => bit(libc::SIGSYS) | bit(libc::SIGSEGV),
7011 _ => 0,
7012 };
7013 assert_eq!(reserved, expected, "{backend:?}");
7014 assert_eq!(
7015 in_guest_guard_router_installed(&config, backend),
7016 backend == Backend::Liteinst,
7017 "{backend:?}"
7018 );
7019 }
7020 }
7021
7022 #[test]
7023 #[cfg(feature = "liteinst")]
7024 fn in_guest_liteinst_refuses_a_kernel_older_than_6_5() {
7025 for release in [
7026 "6.5.0",
7027 "6.5",
7028 "6.10.3-200.fc40.x86_64",
7029 "7.1.3-0_fbk0_rc18",
7030 "10.0",
7031 ] {
7032 assert_eq!(in_guest_liteinst_kernel_gap(release), None, "{release}");
7033 }
7034 for release in ["6.4.16", "5.15.0-91-generic", "4.19"] {
7035 let gap = in_guest_liteinst_kernel_gap(release)
7036 .unwrap_or_else(|| panic!("{release} was not refused"));
7037 assert!(gap.contains("Linux 6.5 or later"), "{gap}");
7038 assert!(gap.contains(release), "{gap}");
7039 }
7040 for release in ["", "unknown", "6"] {
7041 assert!(
7042 in_guest_liteinst_kernel_gap(release).is_some(),
7043 "{release:?}"
7044 );
7045 }
7046 }
7050
7051 #[test]
7052 #[cfg(feature = "liteinst")]
7053 fn in_guest_liteinst_refuses_secure_execution_images() {
7054 use interp::startup_fixtures::dynamic_executable;
7055
7056 let directory = tempfile::tempdir().unwrap();
7057 let bytes = dynamic_executable(false);
7058 let gap = |name: &str, mode: u32| {
7059 let program = write_guest_program(directory.path(), name, &bytes, mode);
7060 in_guest_liteinst_program_gap(&mut Command::new(&program)).unwrap()
7061 };
7062 for (name, mode) in [("setuid", 0o4755), ("setgid", 0o2755)] {
7063 let refusal = gap(name, mode).unwrap_or_else(|| panic!("{name} was not refused"));
7064 assert!(refusal.contains("secure-execution mode"), "{refusal}");
7065 }
7066 assert_eq!(gap("setgid-no-group-exec", 0o2744), None);
7069 }
7070
7071 #[test]
7072 #[cfg(feature = "liteinst")]
7073 fn in_guest_liteinst_checks_scripts_through_their_interpreter() {
7074 use interp::startup_fixtures::*;
7075
7076 let directory = tempfile::tempdir().unwrap();
7077 let dynamic = write_guest_program(
7078 directory.path(),
7079 "dynamic",
7080 &dynamic_executable(false),
7081 0o755,
7082 );
7083 let static_elf = write_guest_program(
7084 directory.path(),
7085 "static",
7086 &elf(EM_X86_64, &[(PT_LOAD, b"code")]),
7087 0o755,
7088 );
7089
7090 let script = write_guest_program(
7091 directory.path(),
7092 "to-static",
7093 &shebang_to(&static_elf),
7094 0o755,
7095 );
7096 let gap = in_guest_liteinst_program_gap(&mut Command::new(&script))
7097 .unwrap()
7098 .expect("a script run by a static interpreter was not refused");
7099 assert!(
7100 gap.contains(&static_elf.display().to_string()) && gap.contains("statically linked"),
7101 "{gap}"
7102 );
7103
7104 let relative = write_guest_program(directory.path(), "relative", b"#!static\n", 0o755);
7106 let gap =
7107 in_guest_liteinst_program_gap(Command::new(&relative).current_dir(directory.path()))
7108 .unwrap()
7109 .expect("a relative static interpreter was not refused");
7110 assert!(gap.contains("statically linked"), "{gap}");
7111
7112 write_guest_program(
7115 directory.path(),
7116 "interp",
7117 &dynamic_executable(false),
7118 0o755,
7119 );
7120 let interp_cr = write_guest_program(
7121 directory.path(),
7122 "interp\r",
7123 &elf(EM_X86_64, &[(PT_LOAD, b"code")]),
7124 0o755,
7125 );
7126 let mut crlf = b"#!".to_vec();
7127 crlf.extend_from_slice(directory.path().join("interp").as_os_str().as_bytes());
7128 crlf.extend_from_slice(b"\r\necho script\r\n");
7129 let crlf = write_guest_program(directory.path(), "crlf", &crlf, 0o755);
7130 let gap = in_guest_liteinst_program_gap(&mut Command::new(&crlf))
7131 .unwrap()
7132 .expect("a CRLF script run by a static interp\\r was not refused");
7133 assert!(
7134 gap.contains(&interp_cr.display().to_string()) && gap.contains("statically linked"),
7135 "{gap}"
7136 );
7137
7138 let mut truncated = b"#!/".to_vec();
7142 truncated.resize(256, b'a');
7143 for (name, bytes) in [
7144 ("empty", b"#! \t\nbody\n".to_vec()),
7145 ("truncated", truncated),
7146 ] {
7147 let program = write_guest_program(directory.path(), name, &bytes, 0o755);
7148 let gap = in_guest_liteinst_program_gap(&mut Command::new(&program))
7149 .unwrap()
7150 .unwrap_or_else(|| panic!("{name} was not refused"));
7151 assert!(
7152 gap.contains("the kernel's script handler declines it"),
7153 "{name}: {gap}"
7154 );
7155 }
7156
7157 let mut target = dynamic;
7159 for level in 1..=5 {
7160 target = write_guest_program(
7161 directory.path(),
7162 &format!("level-{level}"),
7163 &shebang_to(&target),
7164 0o755,
7165 );
7166 let gap = in_guest_liteinst_program_gap(&mut Command::new(&target)).unwrap();
7167 if level <= MAX_INTERPRETER_DEPTH {
7168 assert_eq!(gap, None, "level {level}");
7169 } else {
7170 let gap = gap.expect("five nested scripts were not refused");
7171 assert!(gap.contains("nest more than 4 deep"), "{gap}");
7172 }
7173 }
7174 }
7175
7176 #[test]
7177 #[cfg(feature = "liteinst")]
7178 fn in_guest_liteinst_refuses_a_fifo_without_blocking() {
7179 let directory = tempfile::tempdir().unwrap();
7180 let fifo = directory.path().join("fifo");
7181 let fifo_c = std::ffi::CString::new(fifo.as_os_str().as_bytes()).unwrap();
7182 assert_eq!(unsafe { libc::mkfifo(fifo_c.as_ptr(), 0o700) }, 0);
7183 let script = write_guest_program(directory.path(), "to-fifo", &shebang_to(&fifo), 0o755);
7184 let socket = directory.path().join("socket");
7187 let _listener = std::os::unix::net::UnixListener::bind(&socket).unwrap();
7188 let to_socket =
7189 write_guest_program(directory.path(), "to-socket", &shebang_to(&socket), 0o755);
7190 let device = PathBuf::from("/dev/null");
7191 let to_device =
7192 write_guest_program(directory.path(), "to-device", &shebang_to(&device), 0o755);
7193 let cases = [
7199 (
7200 "program",
7201 PathBuf::from("./fifo"),
7202 Some(directory.path().to_owned()),
7203 fs::canonicalize(&fifo).unwrap(),
7204 ),
7205 ("interpreter", script, None, fifo.clone()),
7206 ("socket interpreter", to_socket, None, socket.clone()),
7207 ("device interpreter", to_device, None, device),
7208 ];
7209 for (name, program, current_dir, shown) in cases {
7210 let (sender, receiver) = std::sync::mpsc::channel();
7211 std::thread::spawn(move || {
7212 let mut command = Command::new(&program);
7213 if let Some(directory) = current_dir {
7214 command.current_dir(directory);
7215 }
7216 let gap = in_guest_liteinst_program_gap(&mut command)
7217 .map_err(|error| format!("{error:#}"));
7218 let _ = sender.send(gap);
7219 });
7220 let gap = receiver
7221 .recv_timeout(std::time::Duration::from_secs(10))
7222 .unwrap_or_else(|_| panic!("{name}: the check blocked on the FIFO"))
7223 .unwrap()
7224 .unwrap_or_else(|| panic!("{name}: the FIFO was not refused"));
7225 assert!(
7226 gap.contains(&format!("{} is not a regular file", shown.display())),
7227 "{name}: {gap}"
7228 );
7229 }
7230 }
7231
7232 #[test]
7233 #[cfg(feature = "liteinst")]
7234 fn in_guest_liteinst_pins_the_program_the_launch_resolves() {
7235 use interp::startup_fixtures::*;
7236
7237 let directory = tempfile::tempdir().unwrap();
7238 let name = "hermit-3520-static";
7240 let static_elf = write_guest_program(
7241 directory.path(),
7242 name,
7243 &elf(EM_X86_64, &[(PT_LOAD, b"code")]),
7244 0o755,
7245 );
7246 let search = format!("/nonexistent-directory:{}", directory.path().display());
7247 let check = |build: &dyn Fn() -> Command| {
7251 let launch = build().find_program();
7252 let mut command = build();
7253 let arg0 = command.get_arg0().to_owned();
7254 let gap = in_guest_liteinst_program_gap(&mut command);
7255 match launch {
7256 Ok(program) => {
7257 let gap = gap.unwrap().expect("the static image was not refused");
7258 assert!(gap.contains("is statically linked"), "{gap}");
7259 assert_eq!(Path::new(command.get_program()), program);
7260 assert_eq!(command.get_arg0(), arg0);
7261 assert_eq!(command.find_program().unwrap(), program);
7263 assert_eq!(
7264 fs::canonicalize(&program).unwrap(),
7265 fs::canonicalize(&static_elf).unwrap()
7266 );
7267 true
7268 }
7269 Err(_) => {
7270 let error = gap.expect_err("the check found what the launch cannot");
7271 assert!(
7272 format!("{error:#}").contains("cannot find guest program"),
7273 "{error:#}"
7274 );
7275 false
7276 }
7277 }
7278 };
7279
7280 assert!(check(&|| {
7282 let mut command = Command::new(format!("./{name}"));
7283 command.current_dir(directory.path());
7284 command
7285 }));
7286 assert!(check(&|| {
7289 let mut command = Command::new(name);
7290 command.env_clear().env("PATH", &search);
7291 command
7292 }));
7293 assert!(check(&|| {
7297 let mut command = Command::new(name);
7298 command.env("PATH", &search);
7299 command
7300 }));
7301 assert!(!check(&|| {
7304 let mut command = Command::new(name);
7305 command.env_clear().current_dir(directory.path());
7306 command
7307 }));
7308 fs::set_permissions(&static_elf, fs::Permissions::from_mode(0o644)).unwrap();
7311 assert!(!check(&|| {
7312 let mut command = Command::new(name);
7313 command.env_clear().env("PATH", &search);
7314 command
7315 }));
7316 assert!(!check(&|| Command::new(&static_elf)));
7317 assert!(!check(&|| Command::new("hermit-no-such-program-3520")));
7318 }
7319
7320 #[test]
7321 fn skid_overshoot_error_refuses_a_successful_result() {
7322 let _lock = SKID_OVERSHOOT_TEST_LOCK.lock().unwrap();
7323 let _ = reverie::take_skid_overshoot_count();
7324 assert!(take_skid_overshoot_error().is_none());
7325
7326 reverie::record_skid_overshoot();
7327 let error = take_skid_overshoot_error().expect("recorded overshoot must be reported");
7328 assert!(
7329 error
7330 .to_string()
7331 .contains("observed 1 HERMIT_SKID_OVERSHOOT report(s)")
7332 );
7333 assert!(
7334 error
7335 .to_string()
7336 .contains("deterministic execution was not established")
7337 );
7338 assert_eq!(reverie::take_skid_overshoot_count(), 0);
7339 }
7340
7341 #[test]
7342 fn skid_overshoot_report_preserves_success_without_an_overshoot() {
7343 let _lock = SKID_OVERSHOOT_TEST_LOCK.lock().unwrap();
7344 let _ = reverie::take_skid_overshoot_count();
7345
7346 let report = SkidOvershootReport::begin(true);
7347 assert_eq!(report.finish(Ok::<_, Error>(7)).unwrap(), 7);
7348 assert_eq!(reverie::take_skid_overshoot_count(), 0);
7349 }
7350
7351 #[test]
7352 fn skid_overshoot_count_can_cross_the_verify_boundary_with_the_output() {
7353 let _lock = SKID_OVERSHOOT_TEST_LOCK.lock().unwrap();
7354 let _ = reverie::take_skid_overshoot_count();
7355
7356 let report = SkidOvershootReport::begin(true);
7357 reverie::record_skid_overshoot();
7358 let (value, count) = report.finish_with_count(Ok::<_, Error>(7)).unwrap();
7359 assert_eq!(value, 7);
7360 assert_eq!(count, 1);
7361 assert_eq!(reverie::take_skid_overshoot_count(), 0);
7362 }
7363
7364 #[test]
7379 #[cfg(feature = "liteinst")]
7380 fn liteinst_forced_shutdown_covers_every_status_hermit_reserves() {
7381 assert!(
7384 liteinst_requires_forced_shutdown(ExitStatus::Exited(130)),
7385 "128+SIGINT"
7386 );
7387 assert!(
7388 liteinst_requires_forced_shutdown(ExitStatus::Exited(143)),
7389 "128+SIGTERM"
7390 );
7391 assert!(
7392 liteinst_requires_forced_shutdown(ExitStatus::Exited(129)),
7393 "128+SIGHUP"
7394 );
7395
7396 assert!(liteinst_requires_forced_shutdown(ExitStatus::Exited(122)));
7398 assert!(liteinst_requires_forced_shutdown(ExitStatus::Exited(125)));
7399 assert!(liteinst_requires_forced_shutdown(ExitStatus::Exited(127)));
7400
7401 assert!(liteinst_requires_forced_shutdown(ExitStatus::Signaled(
7403 nix::sys::signal::Signal::SIGKILL,
7404 false
7405 )));
7406
7407 assert!(!liteinst_requires_forced_shutdown(ExitStatus::Exited(0)));
7412 assert!(!liteinst_requires_forced_shutdown(ExitStatus::Exited(1)));
7413 assert!(!liteinst_requires_forced_shutdown(ExitStatus::Exited(121)));
7414 assert!(!liteinst_requires_forced_shutdown(ExitStatus::Exited(128)));
7415 assert!(!liteinst_requires_forced_shutdown(ExitStatus::Exited(200)));
7416 assert!(
7422 liteinst_requires_forced_shutdown(ExitStatus::Exited(192)),
7423 "128 + SIGRTMAX is still a signal death and must force shutdown"
7424 );
7425 assert!(
7426 !liteinst_requires_forced_shutdown(ExitStatus::Exited(193)),
7427 "above SIGRTMAX the status is the guest's own and must NOT force shutdown"
7428 );
7429 }
7430
7431 #[test]
7435 fn a_cwd_under_tmp_is_explained_by_the_container_tmpfs_not_by_a_missing_directory() {
7436 let error = std::io::Error::from(std::io::ErrorKind::NotFound);
7437 let rendered = format!(
7438 "{}",
7439 super::kvm_cwd_resolution_error(std::path::Path::new("/tmp/agent-checkout"), &error)
7440 );
7441 assert!(
7442 rendered.contains("private, empty tmpfs"),
7443 "must name the mechanism: {rendered}"
7444 );
7445 assert!(
7446 rendered.contains("--workdir"),
7447 "must name a way out: {rendered}"
7448 );
7449 assert!(
7450 rendered.contains("/tmp/agent-checkout"),
7451 "must still name the path asked for: {rendered}"
7452 );
7453 }
7454
7455 #[test]
7459 fn the_tmpfs_explanation_is_withheld_where_it_does_not_apply() {
7460 let not_found = std::io::Error::from(std::io::ErrorKind::NotFound);
7461 let outside_path = std::path::Path::new("/nonexistent-home/wt");
7483 let outside = format!(
7484 "{}",
7485 super::kvm_cwd_resolution_error(outside_path, ¬_found)
7486 );
7487 assert!(!outside.contains("tmpfs"), "not a /tmp path: {outside}");
7488
7489 let tmp_itself = format!(
7492 "{}",
7493 super::kvm_cwd_resolution_error(std::path::Path::new("/tmp"), ¬_found)
7494 );
7495 assert!(!tmp_itself.contains("tmpfs"), "/tmp itself: {tmp_itself}");
7496
7497 let denied = std::io::Error::from(std::io::ErrorKind::PermissionDenied);
7499 let other = format!(
7500 "{}",
7501 super::kvm_cwd_resolution_error(std::path::Path::new("/tmp/x"), &denied)
7502 );
7503 assert!(!other.contains("tmpfs"), "not NotFound: {other}");
7504 }
7505
7506 #[test]
7516 fn an_unbuilt_backend_names_the_flag_and_claims_nothing_about_the_machine() {
7517 let feature_disabled_backends: &[(Backend, &str)] = &[
7518 #[cfg(not(feature = "liteinst"))]
7519 (Backend::Liteinst, "liteinst"),
7520 #[cfg(not(feature = "liteinst"))]
7522 (Backend::InGuestTrap, "liteinst"),
7523 ];
7524
7525 for &(backend, feature) in feature_disabled_backends {
7526 let reason = backend
7527 .unavailable_reason()
7528 .expect("a feature-disabled backend must report why it is unavailable");
7529 assert!(
7530 reason.contains(feature),
7531 "{backend:?}: a build-flag message must name the flag to rebuild with, got: {reason}"
7532 );
7533 assert!(
7534 reason.contains("has not been checked"),
7535 "{backend:?}: a build-flag message must not imply the machine was tested, got: {reason}"
7536 );
7537 }
7538 }
7539
7540 #[tokio::test]
7541 async fn virtual_timeout_with_debugger_is_refused_at_both_raw_library_dispatches() {
7542 for (gdbserver, bounded) in [(false, false), (true, false), (false, true), (true, true)] {
7543 let config = DetConfig {
7544 gdbserver,
7545 virtual_timeout: bounded.then(|| std::num::NonZeroU64::new(5_000_000_000).unwrap()),
7546 ..DetConfig::default()
7547 };
7548 let status = dispatch_backend(
7552 Command::new("/nonexistent/guest"),
7553 config.clone(),
7554 false,
7555 &None,
7556 Backend::Ptrace,
7557 None,
7558 )
7559 .await
7560 .unwrap_err();
7561 let output = dispatch_output_backend(
7562 Command::new("/nonexistent/guest"),
7563 config,
7564 false,
7565 &None,
7566 Backend::Ptrace,
7567 None,
7568 )
7569 .await
7570 .unwrap_err();
7571 for error in [status, output] {
7572 if gdbserver && bounded {
7573 assert!(
7574 error.is::<detcore_model::virtual_timeout::VirtualTimeoutWithDebugger>(),
7575 "{error}"
7576 );
7577 let classified =
7578 crate::error::SerializableError::from(error.context("raw library launch"));
7579 assert_eq!(classified.kind(), crate::error::FailureKind::PolicyRefusal);
7580 } else {
7581 assert!(
7582 error
7583 .to_string()
7584 .contains("ordinary ptrace requires its retained operation owner"),
7585 "{error}"
7586 );
7587 }
7588 }
7589 }
7590 }
7591 use std::fs;
7596 use std::path::PathBuf;
7597
7598 use super::Backend;
7599 #[cfg(feature = "liteinst")]
7600 use super::ExitStatus;
7601 use super::HermitData;
7602 use super::Id;
7603 use super::collect_recording_ids;
7604 use super::kvm_device_unavailable_reason;
7605 #[cfg(feature = "liteinst")]
7606 use super::liteinst_requires_forced_shutdown;
7607 use super::liteinst_unavailable_reason;
7608 use super::output_backend_stdin_file;
7609 use super::prepare_backend_config;
7610 use super::reserve_output_stdin_snapshot;
7611 use super::resolve_kvm_shebang;
7612
7613 #[test]
7614 fn recording_inventory_preserves_missing_and_non_recording_cases() {
7615 let parent = tempfile::tempdir().expect("failed to create recording inventory parent");
7616 let missing = parent.path().join("missing");
7617 assert!(
7618 HermitData::with_dir(&missing)
7619 .try_recordings()
7620 .expect("a missing data directory should be empty")
7621 .is_empty()
7622 );
7623
7624 let inventory = parent.path().join("inventory");
7625 fs::create_dir(&inventory).expect("failed to create recording inventory");
7626 let recording: Id = "0123456789abcdef0123456789abcdef"
7627 .parse()
7628 .expect("valid recording ID");
7629 fs::create_dir(inventory.join(recording.to_string()))
7630 .expect("failed to create recording directory");
7631 fs::create_dir(inventory.join("not-a-recording"))
7632 .expect("failed to create invalid-name directory");
7633 fs::write(
7634 inventory.join("fedcba9876543210fedcba9876543210"),
7635 b"not a directory",
7636 )
7637 .expect("failed to create valid-looking non-directory");
7638
7639 assert_eq!(
7640 HermitData::with_dir(&inventory)
7641 .try_recordings()
7642 .expect("recording inventory should be readable"),
7643 vec![recording]
7644 );
7645 assert_eq!(
7646 HermitData::with_dir(&inventory)
7647 .recordings()
7648 .collect::<Vec<_>>(),
7649 vec![recording]
7650 );
7651 }
7652
7653 #[test]
7654 fn recording_inventory_rejects_an_error_after_a_valid_entry() {
7655 let inventory = tempfile::tempdir().expect("failed to create recording inventory");
7656 let recording: Id = "0123456789abcdef0123456789abcdef"
7657 .parse()
7658 .expect("valid recording ID");
7659 fs::create_dir(inventory.path().join(recording.to_string()))
7660 .expect("failed to create recording directory");
7661 let valid_entry = fs::read_dir(inventory.path())
7662 .expect("failed to open recording inventory")
7663 .next()
7664 .expect("missing recording entry")
7665 .expect("failed to read recording entry");
7666
7667 let entries = vec![
7668 Ok(valid_entry),
7669 Err(std::io::Error::other("injected entry failure")),
7670 ];
7671 let error = collect_recording_ids(entries, inventory.path(), fs::DirEntry::file_type)
7672 .expect_err("a partial inventory must not be returned");
7673 assert!(
7674 error
7675 .to_string()
7676 .contains("Failed to read an entry in recording inventory"),
7677 "unexpected error: {error:#}"
7678 );
7679 }
7680
7681 #[test]
7682 fn recording_inventory_rejects_an_entry_inspection_error() {
7683 let inventory = tempfile::tempdir().expect("failed to create recording inventory");
7684 let recording = inventory.path().join("0123456789abcdef0123456789abcdef");
7685 fs::create_dir(&recording).expect("failed to create recording directory");
7686 let entry = fs::read_dir(inventory.path())
7687 .expect("failed to open recording inventory")
7688 .next()
7689 .expect("missing recording entry")
7690 .expect("failed to read recording entry");
7691
7692 let error = collect_recording_ids([Ok(entry)], inventory.path(), |_| {
7693 Err(std::io::Error::other("injected inspection failure"))
7694 })
7695 .expect_err("an uninspectable entry must fail the inventory");
7696 assert!(
7697 error
7698 .to_string()
7699 .contains("Failed to inspect recording inventory entry"),
7700 "unexpected error: {error:#}"
7701 );
7702 }
7703
7704 #[test]
7712 fn output_stdin_snapshot_replays_pipe_to_repeated_runs() {
7713 use std::io::Read;
7714 use std::io::Write;
7715 use std::os::fd::FromRawFd;
7716
7717 let mut fds = [0i32; 2];
7720 assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
7721 let read_end = unsafe { fs::File::from_raw_fd(fds[0]) };
7723 let mut write_end = unsafe { fs::File::from_raw_fd(fds[1]) };
7724
7725 let payload = b"int main(){return 0;}\n";
7726 write_end.write_all(payload).unwrap();
7727 drop(write_end);
7729
7730 reserve_output_stdin_snapshot(Some(read_end)).unwrap();
7731
7732 for run in 0..2 {
7735 let mut file = output_backend_stdin_file()
7736 .unwrap()
7737 .unwrap_or_else(|| panic!("run {run}: expected a replayable stdin snapshot"));
7738 let mut got = Vec::new();
7739 file.read_to_end(&mut got).unwrap();
7740 assert_eq!(got, payload, "run {run} stdin replay mismatch");
7741 }
7742
7743 let mut file = output_backend_stdin_file().unwrap().unwrap();
7744 let error = file
7745 .write_all(b"guest-must-not-change-the-snapshot")
7746 .expect_err("the replayed stdin must be read-only");
7747 assert_eq!(error.raw_os_error(), Some(libc::EBADF));
7748 }
7749
7750 #[test]
7751 #[cfg(feature = "liteinst")]
7752 fn liteinst_reserved_failures_require_scheduler_cancellation() {
7753 for status in 122..=127 {
7754 assert!(liteinst_requires_forced_shutdown(ExitStatus::Exited(
7755 status
7756 )));
7757 }
7758 assert!(!liteinst_requires_forced_shutdown(ExitStatus::Exited(121)));
7759 assert!(!liteinst_requires_forced_shutdown(ExitStatus::Exited(128)));
7760 }
7761
7762 #[test]
7763 fn liteinst_backend_config_preserves_the_requested_timeslice() {
7764 let config = super::DetConfig::default();
7767 assert!(config.max_timeslice.is_some());
7768 for backend in [Backend::Liteinst, Backend::InGuestTrap] {
7769 assert!(
7770 prepare_backend_config(config.clone(), backend)
7771 .max_timeslice
7772 .is_some(),
7773 "{backend:?}"
7774 );
7775 }
7776 assert!(
7777 prepare_backend_config(config, Backend::Ptrace)
7778 .max_timeslice
7779 .is_some()
7780 );
7781 }
7782
7783 #[test]
7784 fn in_guest_liteinst_cancels_killed_thread_rpcs() {
7785 let config = super::DetConfig::default();
7786 for backend in [Backend::Liteinst, Backend::InGuestTrap] {
7787 assert!(
7788 prepare_backend_config(config.clone(), backend)
7789 .backend
7790 .needs_killed_thread_rpc_cancellation,
7791 "{backend:?}"
7792 );
7793 }
7794 assert!(
7796 !prepare_backend_config(config, Backend::Ptrace)
7797 .backend
7798 .needs_killed_thread_rpc_cancellation
7799 );
7800 }
7801
7802 #[test]
7803 fn sabre_backend_configures_process_local_capabilities() {
7804 let config = super::DetConfig::default();
7805 let sabre = prepare_capability_case(config.clone(), CapabilityCase::Sabre).backend;
7806 assert!(sabre.tool_shares_guest_descriptor_table);
7807 assert!(sabre.rediscovers_descriptors_after_exec);
7808 assert!(sabre.loopback_pollers_yield_to_peers);
7809 assert!(sabre.guest_clock_reads_bypass_backend);
7810 assert!(sabre.needs_killed_thread_rpc_cancellation);
7811 assert!(sabre.reports_physical_process_exits);
7812 assert!(sabre.tracks_process_children);
7813 assert!(!sabre.runs_exit_robust_list);
7814 assert!(!sabre.requires_thread_directed_process_signals);
7815 let ptrace = prepare_capability_case(config, CapabilityCase::Ptrace).backend;
7816 assert!(!ptrace.tool_shares_guest_descriptor_table);
7817 assert!(!ptrace.rediscovers_descriptors_after_exec);
7818 assert!(!ptrace.loopback_pollers_yield_to_peers);
7819 assert!(!ptrace.guest_clock_reads_bypass_backend);
7820 assert!(!ptrace.needs_killed_thread_rpc_cancellation);
7821 assert!(!ptrace.reports_physical_process_exits);
7822 assert!(ptrace.tracks_process_children);
7823 assert!(ptrace.runs_exit_robust_list);
7824 assert!(!ptrace.requires_thread_directed_process_signals);
7825 }
7826
7827 #[test]
7828 fn kvm_reports_stored_metadata_timestamps_only_for_sequential_tool_threads() {
7829 let sequential = prepare_backend_config(
7830 super::DetConfig {
7831 sequentialize_threads: true,
7832 virtualize_metadata: true,
7833 ..super::DetConfig::default()
7834 },
7835 Backend::Kvm,
7836 );
7837 assert!(super::kvm_reports_stored_metadata_timestamps(&sequential));
7838
7839 let concurrent = super::DetConfig {
7842 sequentialize_threads: false,
7843 ..sequential.clone()
7844 };
7845 assert!(!super::kvm_reports_stored_metadata_timestamps(&concurrent));
7846
7847 let host_metadata = super::DetConfig {
7849 virtualize_metadata: false,
7850 ..sequential
7851 };
7852 assert!(!super::kvm_reports_stored_metadata_timestamps(
7853 &host_metadata
7854 ));
7855 }
7856
7857 #[test]
7858 fn kvm_backend_config_marks_concurrent_process_children() {
7859 let defaults = super::DetConfig::default();
7860 assert!(!defaults.sequentialize_threads);
7861 let nonsequential = prepare_capability_case(defaults, CapabilityCase::Kvm);
7862 assert!(nonsequential.backend.provides_process_signal_control);
7863 assert!(nonsequential.backend.emulates_child_waits);
7864 assert!(!may_observe_signal_dequeues(&nonsequential));
7865 assert!(!requires_signal_control(&nonsequential));
7866
7867 let config = super::DetConfig {
7868 sequentialize_threads: true,
7869 ..super::DetConfig::default()
7870 };
7871 let kvm = prepare_capability_case(config, CapabilityCase::Kvm);
7872 assert!(may_observe_signal_dequeues(&kvm));
7873 assert!(requires_signal_control(&kvm));
7874 assert!(kvm.backend.needs_killed_thread_rpc_cancellation);
7875 assert!(kvm.backend.tracks_process_children);
7876 assert!(!kvm.backend.runs_exit_robust_list);
7877 assert!(!kvm.backend.requires_thread_directed_process_signals);
7878
7879 for backend in [
7882 CapabilityCase::Ptrace,
7883 CapabilityCase::Dbt,
7884 CapabilityCase::Sabre,
7885 CapabilityCase::Liteinst,
7886 CapabilityCase::InGuestTrap,
7887 CapabilityCase::E9patch,
7888 ] {
7889 let config = super::DetConfig {
7890 sequentialize_threads: true,
7891 ..super::DetConfig::default()
7892 };
7893 let prepared = prepare_capability_case(config, backend);
7894 assert!(!may_observe_signal_dequeues(&prepared), "{backend:?}");
7895 assert!(!requires_signal_control(&prepared), "{backend:?}");
7896 }
7897 }
7898
7899 fn may_observe_signal_dequeues(config: &DetConfig) -> bool {
7902 <super::Detcore as reverie::Tool>::may_observe_signal_dequeues(config)
7903 }
7904
7905 fn requires_signal_control(config: &DetConfig) -> bool {
7908 <super::Detcore as reverie::Tool>::requires_signal_control(config)
7909 }
7910
7911 fn golden_backend_capabilities(backend: CapabilityCase) -> serde_json::Value {
7943 let sabre = backend == CapabilityCase::Sabre;
7944 let kvm = backend == CapabilityCase::Kvm;
7945 let dbt = backend == CapabilityCase::Dbt;
7946 let in_guest_liteinst = matches!(
7948 backend,
7949 CapabilityCase::Liteinst | CapabilityCase::InGuestTrap
7950 );
7951 serde_json::json!({
7952 "tool_shares_guest_descriptor_table": sabre || dbt,
7953 "rediscovers_descriptors_after_exec": sabre || dbt,
7954 "loopback_pollers_yield_to_peers": sabre,
7955 "guest_clock_reads_bypass_backend": sabre,
7956 "needs_killed_thread_rpc_cancellation": in_guest_liteinst || sabre || dbt || kvm,
7957 "reports_physical_process_exits": sabre,
7958 "process_exits_complete_asynchronously": in_guest_liteinst,
7959 "virtualizes_guest_sigalrm": in_guest_liteinst,
7960 "tracks_process_children": !dbt,
7961 "runs_exit_robust_list": matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch),
7962 "kernel_never_walks_robust_lists": kvm,
7966 "requires_thread_directed_process_signals": dbt,
7967 "supports_parked_write_signal_interruption":
7968 matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch | CapabilityCase::Kvm),
7969 "reports_child_exit_publication":
7970 matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch),
7971 "reports_kernel_child_exit_signals":
7972 matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch | CapabilityCase::Sabre),
7973 "provides_process_signal_control": kvm,
7974 "emulates_child_waits": kvm,
7975 "supports_blocked_wait_signal_interruption":
7978 matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch),
7979 "kernel_signal_state_is_guests": matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch),
7982 "kernel_signal_targets_are_predictable":
7983 matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch),
7984 "host_task_reads_reach_guest_threads":
7985 matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch),
7986 "host_timed_signals_are_physical":
7987 matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch),
7988 "delivers_preemption_timer_events":
7990 matches!(
7991 backend,
7992 CapabilityCase::Ptrace | CapabilityCase::E9patch | CapabilityCase::Kvm
7993 ),
7994 "runs_guest_code_between_injections": sabre || in_guest_liteinst,
7995 "promotes_open_files_before_native_lifecycle": dbt || in_guest_liteinst,
7996 "reports_sigreturns": matches!(backend, CapabilityCase::Ptrace | CapabilityCase::E9patch),
8000 "hardware_watchpoints": if cfg!(target_arch = "x86_64") {
8003 match backend {
8004 CapabilityCase::Ptrace | CapabilityCase::E9patch => 4,
8005 _ => 0,
8006 }
8007 } else {
8008 0
8009 },
8010 "provides_guest_backtraces": matches!(
8014 backend,
8015 CapabilityCase::Ptrace
8016 | CapabilityCase::E9patch
8017 | CapabilityCase::Sabre
8018 | CapabilityCase::Dbt
8019 ),
8020 "places_orphans": kvm,
8024 "publishable_parent_death_signals": if kvm { 0x7fc2_ffff_u64 } else { 0 },
8027 })
8028 }
8029
8030 #[test]
8031 fn backend_capabilities_match_the_golden_table() {
8032 for backend in [
8033 CapabilityCase::Ptrace,
8034 CapabilityCase::Kvm,
8035 CapabilityCase::E9patch,
8036 CapabilityCase::Liteinst,
8037 CapabilityCase::InGuestTrap,
8038 CapabilityCase::Sabre,
8039 CapabilityCase::Dbt,
8040 ] {
8041 let config = prepare_capability_case(super::DetConfig::default(), backend);
8042 assert_eq!(
8043 serde_json::to_value(config.backend).unwrap(),
8044 golden_backend_capabilities(backend),
8045 "{backend:?}"
8046 );
8047 }
8048 }
8049
8050 #[test]
8051 fn backend_capabilities_are_the_ones_each_backend_reports() {
8052 assert_eq!(
8053 super::backend_capabilities(Backend::Ptrace),
8054 <reverie_ptrace::PtraceBackend as reverie::Backend>::capabilities()
8055 );
8056 assert_eq!(
8057 super::backend_capabilities(Backend::Kvm),
8058 reverie_kvm::KvmBackend::capabilities()
8059 );
8060 #[cfg(feature = "liteinst")]
8061 {
8062 let backing = unsafe {
8066 std::ptr::addr_of!(super::reverie_liteinst_allocator_backing_v1)
8067 .cast::<[u64; 2]>()
8068 .read_volatile()
8069 };
8070 assert_eq!(backing, [1, 1], "host Hermit must declare Legacy backing");
8071 assert_eq!(
8072 super::backend_capabilities(Backend::Liteinst),
8073 <reverie_liteinst::LiteinstBackend as reverie::Backend>::capabilities()
8074 );
8075 assert_eq!(
8077 super::backend_capabilities(Backend::InGuestTrap),
8078 <reverie_liteinst::LiteinstBackend as reverie::Backend>::capabilities()
8079 );
8080 }
8081 }
8082
8083 fn env_free_detconfig(epoch: &str, seed: &str) -> DetConfig {
8087 use clap::Parser;
8088 let mut config = DetConfig::parse_from(["hermit", epoch, seed, "--sched-seed=0"]);
8089 config.sched_seed = None;
8090 config
8091 }
8092
8093 fn rich_detconfig() -> DetConfig {
8097 let mut config = env_free_detconfig("--epoch=2000-12-31T23:59:59Z", "--seed=7");
8098 config.rng_seed = Some(13);
8099 config.sched_seed = Some(11);
8100 config.clock_multiplier = Some(1.5);
8101 config.chaos = true;
8102 config.record_preemptions_to = Some(std::path::PathBuf::from(
8103 "/tmp/a,\"detect_host_clock_futex_timeouts\":true,\"backend_tracks_process_children\":{",
8104 ));
8105 config.mountinfo_root_rewrites = vec![detcore_model::config::MountInfoRootRewrite {
8106 raw_mount_id: 41,
8107 deterministic_root: b"/".to_vec(),
8108 raw_root_prefix: Some(b"/tmp/x".to_vec()),
8109 deterministic_root_prefix: Some(b"/tmp".to_vec()),
8110 raw_mountpoint_prefix: None,
8111 deterministic_mountpoint_prefix: None,
8112 }];
8113 config.stacktrace_event = vec![(3, Some(std::path::PathBuf::from("/s")))];
8114 config
8115 }
8116
8117 #[test]
8129 fn historical_dbt_capabilities_preserve_legacy_detconfig_bytes() {
8130 let sequential = |mut config: DetConfig| {
8131 config.sequentialize_threads = true;
8132 config
8133 };
8134 let default_config = || env_free_detconfig("--epoch=2026-01-01T00:00:00Z", "--seed=0");
8135 for (name, mut config, before) in [
8136 (
8137 "default",
8138 default_config(),
8139 include_str!("../tests/fixtures/dbt/detconfig-before-capabilities-default.json"),
8140 ),
8141 (
8142 "dbt",
8143 prepare_capability_case(sequential(default_config()), CapabilityCase::Dbt),
8144 include_str!("../tests/fixtures/dbt/detconfig-before-capabilities-dbt.json"),
8145 ),
8146 (
8147 "rich_dbt",
8148 prepare_capability_case(sequential(rich_detconfig()), CapabilityCase::Dbt),
8149 include_str!("../tests/fixtures/dbt/detconfig-before-capabilities-rich_dbt.json"),
8150 ),
8151 ] {
8152 config.backend.tool_shares_guest_descriptor_table = false;
8153 config.backend.rediscovers_descriptors_after_exec = false;
8154 config.backend.supports_blocked_wait_signal_interruption = false;
8156 config.backend.delivers_preemption_timer_events = false;
8157 config.backend.runs_guest_code_between_injections = false;
8158 config.backend.hardware_watchpoints = 0;
8161 assert_eq!(
8162 detcore::to_legacy_backend_json(&config).unwrap(),
8163 before,
8164 "the legacy form of the {name} config changed"
8165 );
8166 let decoded = detcore::from_legacy_backend_json(before).unwrap();
8168 assert_eq!(decoded.backend, config.backend, "{name}");
8169 assert_eq!(
8170 decoded.sequentialize_threads, config.sequentialize_threads,
8171 "{name}"
8172 );
8173 assert_eq!(
8174 detcore::to_legacy_backend_json(&decoded).unwrap(),
8175 before,
8176 "{name}"
8177 );
8178 }
8179 }
8180
8181 const LEGACY_BACKEND_KEYS: [&str; 15] = [
8184 "cpuid_virtualized_by_backend",
8185 "backend_supports_madvise",
8186 "discover_live_file_metadata",
8187 "detect_host_clock_futex_timeouts",
8188 "syscall_clobbers_virtualized_by_backend",
8189 "cancel_killed_thread_rpcs",
8190 "backend_reports_physical_process_exits",
8191 "backend_tracks_process_children",
8192 "backend_runs_exit_robust_list",
8193 "backend_requires_thread_directed_process_signals",
8194 "backend_is_kvm",
8195 "kvm_shared_dequeue_timers",
8196 "backend_supports_parked_write_signal_interruption",
8197 "backend_virtualizes_capability_prctls",
8198 "backend_defers_vfork_child_registration",
8199 ];
8200
8201 #[test]
8221 fn every_backend_encodes_its_current_legacy_projection() {
8222 for (backend, unsequenced, sequential) in [
8223 (CapabilityCase::Ptrace, "010000011000100", "010000011000100"),
8224 (
8225 CapabilityCase::E9patch,
8226 "010000011000100",
8227 "010000011000100",
8228 ),
8229 (
8230 CapabilityCase::Liteinst,
8231 "010001010000000",
8232 "010001010000000",
8233 ),
8234 (
8235 CapabilityCase::InGuestTrap,
8236 "010001010000000",
8237 "010001010000000",
8238 ),
8239 (CapabilityCase::Sabre, "011101110000000", "011101110000000"),
8240 (CapabilityCase::Dbt, "011001000100000", "011001000100000"),
8241 (CapabilityCase::Kvm, "010001010010100", "010001010011100"),
8242 ] {
8243 for (sequentialize_threads, expected) in [(false, unsequenced), (true, sequential)] {
8244 let config = prepare_capability_case(
8245 DetConfig {
8246 sequentialize_threads,
8247 ..DetConfig::default()
8248 },
8249 backend,
8250 );
8251 let encoded: serde_json::Map<String, serde_json::Value> =
8252 serde_json::from_str(&detcore::to_legacy_backend_json(&config).unwrap())
8253 .unwrap();
8254 let digits: String = LEGACY_BACKEND_KEYS
8255 .iter()
8256 .map(|key| match encoded.get(*key) {
8257 Some(serde_json::Value::Bool(true)) => '1',
8258 Some(serde_json::Value::Bool(false)) => '0',
8259 other => panic!("{key} encoded as {other:?}"),
8260 })
8261 .collect();
8262 assert_eq!(
8263 digits, expected,
8264 "{backend:?}, sequentialize_threads={sequentialize_threads}"
8265 );
8266 }
8267 }
8268 }
8269
8270 #[test]
8271 fn dbt_backend_config_translates_process_signals_to_host_threads() {
8272 let config = prepare_capability_case(super::DetConfig::default(), CapabilityCase::Dbt);
8273 assert!(config.backend.needs_killed_thread_rpc_cancellation);
8274 assert!(!config.backend.tracks_process_children);
8275 assert!(!config.backend.runs_exit_robust_list);
8276 assert!(config.backend.requires_thread_directed_process_signals);
8277 }
8278
8279 #[test]
8280 fn backend_write_signal_contract_is_explicit() {
8281 for (backend, supports_interruption) in [
8282 (CapabilityCase::Ptrace, true),
8283 (CapabilityCase::Dbt, false),
8284 (CapabilityCase::Kvm, true),
8285 (CapabilityCase::Sabre, false),
8286 (CapabilityCase::Liteinst, false),
8287 (CapabilityCase::InGuestTrap, false),
8288 (CapabilityCase::E9patch, true),
8289 ] {
8290 let config = prepare_capability_case(super::DetConfig::default(), backend);
8291 assert_eq!(
8292 config.backend.supports_parked_write_signal_interruption, supports_interruption,
8293 "unexpected parked-write signal support for {backend:?}"
8294 );
8295 }
8296 }
8297
8298 #[test]
8302 fn backend_timer_delivery_is_explicit() {
8303 for (backend, delivers) in [
8304 (CapabilityCase::Ptrace, true),
8305 (CapabilityCase::Dbt, false),
8306 (CapabilityCase::Kvm, true),
8307 (CapabilityCase::Sabre, false),
8308 (CapabilityCase::Liteinst, false),
8309 (CapabilityCase::InGuestTrap, false),
8310 (CapabilityCase::E9patch, true),
8311 ] {
8312 let config = prepare_capability_case(super::DetConfig::default(), backend);
8313 assert_eq!(
8314 config.backend.delivers_preemption_timer_events, delivers,
8315 "unexpected timer delivery for {backend:?}"
8316 );
8317 }
8318 }
8319
8320 #[test]
8323 fn backend_guest_code_between_injections_is_explicit() {
8324 for (backend, runs) in [
8325 (CapabilityCase::Ptrace, false),
8326 (CapabilityCase::Dbt, false),
8327 (CapabilityCase::Kvm, false),
8328 (CapabilityCase::Sabre, true),
8329 (CapabilityCase::Liteinst, true),
8330 (CapabilityCase::InGuestTrap, true),
8331 (CapabilityCase::E9patch, false),
8332 ] {
8333 let config = prepare_capability_case(super::DetConfig::default(), backend);
8334 assert_eq!(
8335 config.backend.runs_guest_code_between_injections, runs,
8336 "unexpected guest code between injections for {backend:?}"
8337 );
8338 }
8339 }
8340
8341 #[test]
8342 fn backend_blocked_wait_signal_contract_is_explicit() {
8343 for (backend, supports_interruption) in [
8344 (CapabilityCase::Ptrace, true),
8345 (CapabilityCase::Dbt, false),
8346 (CapabilityCase::Kvm, false),
8347 (CapabilityCase::Sabre, false),
8348 (CapabilityCase::Liteinst, false),
8349 (CapabilityCase::InGuestTrap, false),
8350 (CapabilityCase::E9patch, true),
8351 ] {
8352 let config = prepare_capability_case(super::DetConfig::default(), backend);
8353 assert_eq!(
8354 config.backend.supports_blocked_wait_signal_interruption, supports_interruption,
8355 "unexpected blocked-wait signal support for {backend:?}"
8356 );
8357 }
8358 }
8359
8360 #[test]
8365 fn a_launch_may_pass_a_terminal_unless_it_proves_it_has_none() {
8366 assert!(!super::may_pass_a_terminal(false, Some(0)));
8367 assert!(super::may_pass_a_terminal(true, Some(0)));
8368 assert!(super::may_pass_a_terminal(false, Some(34817)));
8369 assert!(super::may_pass_a_terminal(false, None));
8370
8371 let stat = "4242 (a) b (c)) S 1 4242 4242 34817 4242 4194560 0 0 0 0";
8372 assert_eq!(super::parse_controlling_terminal_device(stat), Some(34817));
8373 let stat = "4242 (hermit) S 1 4242 4242 0 -1 4194560 0 0 0 0";
8374 assert_eq!(super::parse_controlling_terminal_device(stat), Some(0));
8375 assert_eq!(
8376 super::parse_controlling_terminal_device("4242 (hermit"),
8377 None
8378 );
8379 assert_eq!(
8380 super::parse_controlling_terminal_device("4242 (hermit) S 1"),
8381 None
8382 );
8383 assert!(super::controlling_terminal_device().is_some());
8384 }
8385
8386 #[test]
8389 fn only_gated_backends_record_an_inherited_terminal() {
8390 for backend in [
8391 CapabilityCase::Dbt,
8392 CapabilityCase::Kvm,
8393 CapabilityCase::Sabre,
8394 CapabilityCase::Liteinst,
8395 CapabilityCase::InGuestTrap,
8396 ] {
8397 let config = prepare_capability_case(super::DetConfig::default(), backend);
8398 assert!(
8399 !config.guest_may_inherit_a_terminal,
8400 "unexpected terminal record for {backend:?}"
8401 );
8402 }
8403 for backend in [CapabilityCase::Ptrace, CapabilityCase::E9patch] {
8404 let config = prepare_capability_case(super::DetConfig::default(), backend);
8405 assert_eq!(
8406 config.guest_may_inherit_a_terminal,
8407 super::launcher_may_pass_a_terminal()
8408 );
8409 }
8410 }
8411
8412 #[test]
8413 fn liteinst_public_dispatch_runs_in_guest_detcore() {
8414 if Backend::Liteinst.ensure_available().is_err() {
8415 return;
8416 }
8417
8418 let config = super::DetConfig {
8422 max_timeslice: None,
8423 ..super::DetConfig::default()
8424 };
8425 let mut command = super::Command::new("/bin/echo");
8426 command.arg("hello");
8427 let output = super::run_with_output_backend(
8428 command,
8429 config.clone(),
8430 false,
8431 &None,
8432 Backend::Liteinst,
8433 )
8434 .expect("run /bin/echo through in-guest LiteInst");
8435 assert_eq!(
8437 output.status,
8438 super::ExitStatus::Exited(0),
8439 "guest did not exit 0.\n--- guest stderr ---\n{}\n--- guest stdout ---\n{}",
8440 String::from_utf8_lossy(&output.stderr),
8441 String::from_utf8_lossy(&output.stdout),
8442 );
8443 assert_eq!(output.stdout, b"hello\n");
8444
8445 let status = super::run_with_backend(
8446 super::Command::new("/bin/true"),
8447 config,
8448 false,
8449 &None,
8450 Backend::Liteinst,
8451 )
8452 .expect("run /bin/true through in-guest LiteInst");
8453 assert_eq!(status, super::ExitStatus::Exited(0));
8454 }
8455
8456 #[test]
8457 fn default_and_available_backends_reflect_host_probes() {
8458 assert_eq!(Backend::default(), Backend::Ptrace);
8459 let available = Backend::available().collect::<Vec<_>>();
8460 assert_eq!(
8461 available.contains(&Backend::Ptrace),
8462 Backend::Ptrace.is_available()
8463 );
8464 assert_eq!(
8465 available.contains(&Backend::Liteinst),
8466 liteinst_unavailable_reason().is_none()
8467 );
8468 assert_eq!(
8470 available.contains(&Backend::InGuestTrap),
8471 available.contains(&Backend::Liteinst)
8472 );
8473 assert_eq!(
8474 available.contains(&Backend::Kvm),
8475 kvm_device_unavailable_reason(std::path::Path::new("/dev/kvm")).is_none(),
8476 );
8477 }
8478
8479 #[test]
8480 fn dependency_probes_require_usable_paths() {
8481 let temp = tempfile::tempdir().unwrap();
8482 let reason = kvm_device_unavailable_reason(temp.path())
8483 .expect("a directory must not pass the read-write KVM device probe");
8484 assert!(reason.contains("read-write"));
8485 }
8486
8487 #[test]
8488 fn optional_backends_report_accurate_availability() {
8489 assert_eq!(
8490 Backend::Liteinst.ensure_available().is_ok(),
8491 liteinst_unavailable_reason().is_none()
8492 );
8493 assert_eq!(
8494 Backend::InGuestTrap.ensure_available().is_ok(),
8495 liteinst_unavailable_reason().is_none()
8496 );
8497
8498 match Backend::Kvm.ensure_available() {
8499 Ok(()) => assert!(
8500 kvm_device_unavailable_reason(std::path::Path::new("/dev/kvm")).is_none(),
8501 "KVM reported available without a usable /dev/kvm",
8502 ),
8503 Err(kvm_error) => {
8504 let message = kvm_error.to_string();
8505 assert!(
8506 message.contains("/dev/kvm"),
8507 "unexpected KVM availability error: {message}",
8508 );
8509 assert!(!message.contains("requires root privileges"));
8510 }
8511 }
8512 }
8513
8514 #[test]
8515 fn kvm_runs_dynamic_echo_through_detcore() {
8516 std::thread::Builder::new()
8521 .name("kvm_runs_dynamic_echo_through_detcore".to_owned())
8522 .stack_size(16 << 20)
8523 .spawn(kvm_runs_dynamic_echo_through_detcore_body)
8524 .unwrap()
8525 .join()
8526 .unwrap();
8527 }
8528
8529 fn kvm_runs_dynamic_echo_through_detcore_body() {
8530 use clap::Parser;
8531
8532 if kvm_device_unavailable_reason(std::path::Path::new("/dev/kvm")).is_some() {
8533 return;
8534 }
8535
8536 let mut command = super::Command::new("/bin/echo");
8537 command.arg("hello");
8538 let mut config = super::DetConfig::parse_from(["hermit-kvm-test"]);
8539 config.validate();
8540 let output = super::run_with_output_backend(command, config, false, &None, Backend::Kvm)
8541 .expect("run dynamic /bin/echo through KvmGuest<Detcore>");
8542
8543 assert_eq!(
8545 output.status,
8546 super::ExitStatus::Exited(0),
8547 "guest did not exit 0.\n--- guest stderr ---\n{}\n--- guest stdout ---\n{}",
8548 String::from_utf8_lossy(&output.stderr),
8549 String::from_utf8_lossy(&output.stdout),
8550 );
8551 assert_eq!(output.stdout, b"hello\n");
8552 assert!(output.stderr.is_empty());
8553 }
8554
8555 #[test]
8558 fn detcore_drives_kvm_guest_for_synthetic_syscall() {
8559 use clap::Parser;
8560
8561 const MEMORY_SIZE: usize = 0x10_000;
8562 const ENTRY_POINT: u64 = 0x1000;
8563 const FRAME_ADDRESS: u64 = 0x2000;
8564
8565 let mut backend = match reverie_kvm::KvmBackend::new(MEMORY_SIZE) {
8566 Ok(backend) => backend,
8567 Err(error) => {
8568 eprintln!("skipping KVM Detcore experiment: cannot init VM: {error}");
8569 return;
8570 }
8571 };
8572
8573 backend
8575 .install_syscall(
8576 ENTRY_POINT,
8577 FRAME_ADDRESS,
8578 reverie_kvm::SyscallRequest::new(libc::SYS_getpid as u64, [0; 6]),
8579 )
8580 .expect("install synthetic getpid guest");
8581
8582 let mut config =
8584 super::DetConfig::parse_from(["hermit-kvm-test", "--max-timeslice=disabled"]);
8585 config.validate();
8586
8587 let runtime = tokio::runtime::Builder::new_current_thread()
8588 .enable_all()
8589 .build()
8590 .expect("build tokio runtime");
8591
8592 let outcome = runtime.block_on(async {
8593 backend
8594 .run_with_tool::<super::Detcore, _>(
8595 config,
8596 |request: &reverie_kvm::SyscallRequest, _memory: &reverie_kvm::GuestMemory| {
8598 unsafe {
8601 libc::syscall(
8602 request.number() as libc::c_long,
8603 request.args()[0],
8604 request.args()[1],
8605 request.args()[2],
8606 request.args()[3],
8607 request.args()[4],
8608 request.args()[5],
8609 ) as i64
8610 }
8611 },
8612 )
8613 .await
8614 });
8615
8616 outcome.expect("Detcore drove the synthetic KVM guest to completion");
8619 }
8620
8621 const FAKE_ELF: &[u8] = b"\x7fELF\x02\x01\x01\x00 fake elf body";
8624
8625 fn image_of(mut file: fs::File) -> Vec<u8> {
8628 std::io::Seek::seek(&mut file, std::io::SeekFrom::Start(0)).unwrap();
8629 let mut image = Vec::new();
8630 std::io::Read::read_to_end(&mut file, &mut image).unwrap();
8631 image
8632 }
8633
8634 fn shebang_tmpdir(tag: &str) -> PathBuf {
8635 let dir = std::env::temp_dir().join(format!(
8636 "hermit-shebang-test-{}-{}",
8637 std::process::id(),
8638 tag
8639 ));
8640 let _ = fs::remove_dir_all(&dir);
8641 fs::create_dir_all(&dir).unwrap();
8642 dir
8643 }
8644
8645 #[test]
8646 fn resolve_shebang_plain_elf_is_unchanged() {
8647 let dir = shebang_tmpdir("plain");
8648 let prog = dir.join("prog");
8649 fs::write(&prog, FAKE_ELF).unwrap();
8650
8651 let argv = vec!["prog".to_owned(), "-a".to_owned()];
8652 let (path, out_argv, file) = resolve_kvm_shebang(&prog, argv).unwrap();
8653 assert_eq!(path, prog);
8654 assert_eq!(out_argv, vec!["prog".to_owned(), "-a".to_owned()]);
8655 assert_eq!(image_of(file), FAKE_ELF);
8656 fs::remove_dir_all(&dir).unwrap();
8657 }
8658
8659 #[test]
8660 fn resolve_shebang_single_level_kernel_order() {
8661 let dir = shebang_tmpdir("single");
8662 let interp = dir.join("fakebash");
8663 fs::write(&interp, FAKE_ELF).unwrap();
8664 let script = dir.join("script");
8665 fs::write(&script, format!("#!{} -x\necho hi\n", interp.display())).unwrap();
8667
8668 let argv = vec!["script".to_owned(), "arg1".to_owned()];
8669 let (path, out_argv, file) = resolve_kvm_shebang(&script, argv).unwrap();
8670 assert_eq!(path, interp);
8671 assert_eq!(
8673 out_argv,
8674 vec![
8675 interp.to_string_lossy().into_owned(),
8676 "-x".to_owned(),
8677 script.to_string_lossy().into_owned(),
8678 "arg1".to_owned(),
8679 ]
8680 );
8681 assert_eq!(image_of(file), FAKE_ELF);
8682 fs::remove_dir_all(&dir).unwrap();
8683 }
8684
8685 #[test]
8686 fn resolve_shebang_nested_accumulates_like_binfmt_script() {
8687 let dir = shebang_tmpdir("nested");
8688 let interp = dir.join("fakebash");
8689 fs::write(&interp, FAKE_ELF).unwrap();
8690 let mid = dir.join("mid"); fs::write(&mid, format!("#!{}\n", interp.display())).unwrap();
8692 let script = dir.join("script");
8693 fs::write(&script, format!("#!{} -e\n", mid.display())).unwrap();
8694
8695 let argv = vec!["script".to_owned(), "arg1".to_owned()];
8696 let (path, out_argv, file) = resolve_kvm_shebang(&script, argv).unwrap();
8697 assert_eq!(path, interp);
8698 assert_eq!(
8700 out_argv,
8701 vec![
8702 interp.to_string_lossy().into_owned(),
8703 mid.to_string_lossy().into_owned(),
8704 "-e".to_owned(),
8705 script.to_string_lossy().into_owned(),
8706 "arg1".to_owned(),
8707 ]
8708 );
8709 assert_eq!(image_of(file), FAKE_ELF);
8710 fs::remove_dir_all(&dir).unwrap();
8711 }
8712
8713 #[test]
8714 fn resolve_shebang_rejects_infinite_recursion() {
8715 let dir = shebang_tmpdir("loop");
8716 let a = dir.join("a");
8717 let b = dir.join("b");
8718 fs::write(&a, format!("#!{}\n", b.display())).unwrap();
8719 fs::write(&b, format!("#!{}\n", a.display())).unwrap();
8720
8721 let argv = vec!["a".to_owned()];
8722 assert!(resolve_kvm_shebang(&a, argv).is_err());
8723 fs::remove_dir_all(&dir).unwrap();
8724 }
8725
8726 mod reverie_pin_reader {
8730 include!("../reverie_pin.rs");
8731
8732 const A: &str = "1111111111111111111111111111111111111111";
8733 const B: &str = "2222222222222222222222222222222222222222";
8734
8735 fn dep(rev: &str) -> String {
8736 format!(
8737 "reverie = {{ git = \"https://github.com/rrnewton/reverie.git\", rev = \"{rev}\" }}\n"
8738 )
8739 }
8740
8741 #[test]
8742 fn a_single_revision_is_read() {
8743 assert_eq!(parse_reverie_pin(&dep(A)).as_deref(), Some(A));
8744 }
8745
8746 #[test]
8747 fn agreeing_revisions_are_read_once() {
8748 let text = format!("{}{}", dep(A), dep(A));
8749 assert_eq!(parse_reverie_pin(&text).as_deref(), Some(A));
8750 }
8751
8752 #[test]
8755 fn disagreeing_revisions_are_refused() {
8756 let text = format!("{}{}", dep(A), dep(B));
8757 assert_eq!(parse_reverie_pin(&text), None);
8758 }
8759
8760 #[test]
8764 fn a_commented_out_dependency_is_not_the_pin() {
8765 let text = format!("# {}{}", dep(B), dep(A));
8766 assert_eq!(
8767 parse_reverie_pin(&text).as_deref(),
8768 Some(A),
8769 "a commented-out revision must not win"
8770 );
8771 }
8772
8773 #[test]
8774 fn a_manifest_naming_no_revision_yields_none() {
8775 assert_eq!(parse_reverie_pin("[dependencies]\nlibc = \"0.2\"\n"), None);
8776 }
8777
8778 #[test]
8781 fn an_unparseable_revision_is_ambiguous() {
8782 let text = format!(
8783 "{}reverie-x = {{ git = \"https://github.com/rrnewton/reverie.git\", rev = \"beef\" }}\n",
8784 dep(A)
8785 );
8786 assert_eq!(parse_reverie_pin(&text), None);
8787 }
8788 }
8789
8790 #[test]
8798 fn sabre_detlog_forwarding_answers_each_target_as_the_cli_filter_does() {
8799 use detcore::detlog::ForwardPolicy;
8800 use detcore::detlog::info_enabled_for_target;
8801 use tracing::level_filters::LevelFilter;
8802 use tracing_subscriber::layer::SubscriberExt;
8803
8804 use crate::liteinst_bootstrap::EffectiveFilter;
8805
8806 let targets = [
8807 "",
8808 "detcore",
8809 "detcore::random",
8810 "detcore::randomness",
8811 "detcore::tool_local",
8812 "detcore::scheduler",
8813 "detcore::scheduler::runqueue",
8814 "detcore::syscalls::files",
8815 "hermit",
8816 "warn",
8817 ];
8818 let policy_under = |raw: &str, level: LevelFilter| {
8819 let subscriber = tracing_subscriber::registry()
8820 .with(EffectiveFilter::from_directives_lossy(raw, level).into_filter());
8821 tracing::subscriber::with_default(subscriber, || {
8822 let policy = ForwardPolicy::from_current_subscriber(raw);
8823 let subscriber_says: Vec<bool> = targets
8824 .iter()
8825 .map(|target| info_enabled_for_target(target))
8826 .collect();
8827 (policy, subscriber_says)
8828 })
8829 };
8830 for (raw, level) in [
8831 ("warn,detcore::random=info", LevelFilter::WARN),
8832 ("detcore::random=info", LevelFilter::WARN),
8833 ("detcore::random=warn", LevelFilter::INFO),
8834 (
8835 "warn,detcore=info,detcore::scheduler=off",
8836 LevelFilter::WARN,
8837 ),
8838 (
8839 "warn,detcore[work]=info,detcore::random=info",
8840 LevelFilter::WARN,
8841 ),
8842 ("", LevelFilter::INFO),
8843 ("", LevelFilter::WARN),
8844 ] {
8845 let (policy, subscriber_says) = policy_under(raw, level);
8846 for (target, enabled) in targets.iter().zip(subscriber_says) {
8847 assert_eq!(
8848 policy.forwards(target),
8849 enabled,
8850 "RUST_LOG={raw:?} --log={level}: policy {policy:?} disagrees on {target:?}"
8851 );
8852 }
8853 }
8854
8855 let (scoped, subscriber_says) =
8856 policy_under("warn,detcore::random=info", LevelFilter::WARN);
8857 assert!(!subscriber_says[1], "the generic detcore target is off");
8858 assert!(scoped.forwards_any());
8859 assert!(scoped.forwards("detcore::random"));
8860 assert!(!scoped.forwards("detcore::tool_local"));
8861 assert_eq!(policy_under("", LevelFilter::INFO).0, ForwardPolicy::all());
8862 assert!(!policy_under("", LevelFilter::WARN).0.forwards_any());
8863 }
8864
8865 #[cfg(feature = "liteinst")]
8870 #[test]
8871 fn liteinst_launch_hands_the_guest_its_per_target_policy() {
8872 use std::os::fd::AsRawFd;
8873
8874 use detcore::detlog::ForwardPolicy;
8875 use detcore::in_guest_launch::InGuestLaunch;
8876 use reverie::process::Command;
8877
8878 use super::LITEINST_DETLOG_FORWARD_ENV;
8879 use super::LITEINST_DETLOG_FORWARD_POLICY_ENV;
8880 use super::forward_in_guest_detlogs_to;
8881 use super::in_guest_liteinst_launch;
8882
8883 let (sink, _peer) = std::os::unix::net::UnixDatagram::pair().unwrap();
8884 forward_in_guest_detlogs_to(sink.into());
8885 let removed = |command: &Command| {
8886 [
8887 LITEINST_DETLOG_FORWARD_ENV,
8888 LITEINST_DETLOG_FORWARD_POLICY_ENV,
8889 detcore::LITEINST_CONFIG_FINGERPRINT_ENV,
8890 ]
8891 .into_iter()
8892 .all(|key| {
8893 command
8894 .get_envs()
8895 .any(|(name, value)| name == std::ffi::OsStr::new(key) && value.is_none())
8896 })
8897 };
8898
8899 let scoped = ForwardPolicy::decode("0,detcore::random=1").unwrap();
8900 let mut command = Command::new("/bin/true");
8901 let (launch, duplicate) = in_guest_liteinst_launch(&mut command, &scoped).unwrap();
8902 let duplicate = duplicate.expect("a policy that forwards a target requests forwarding");
8903 assert!(removed(&command));
8904 let launch = InGuestLaunch::decode(&launch.encode()).unwrap();
8905 let passed = launch.forward_request.expect("the descriptor is passed");
8906 assert_eq!(
8907 detcore::detlog::tool_output_env_target(&passed),
8908 Some(duplicate.as_raw_fd()),
8909 "{passed}"
8910 );
8911 let sent =
8912 ForwardPolicy::decode(&launch.forward_policy.expect("the policy goes too")).unwrap();
8913 assert_eq!(sent, scoped);
8914 assert!(sent.forwards("detcore::random"));
8915 assert!(!sent.forwards("detcore::tool_local"));
8916 assert_eq!(
8917 launch.coordinator_fingerprint,
8918 Some(detcore::config_wire_fingerprint())
8919 );
8920
8921 let nothing = ForwardPolicy::decode("0").unwrap();
8922 let mut command = Command::new("/bin/true");
8923 command
8924 .env(LITEINST_DETLOG_FORWARD_ENV, "9")
8925 .env(LITEINST_DETLOG_FORWARD_POLICY_ENV, "1")
8926 .env(detcore::LITEINST_CONFIG_FINGERPRINT_ENV, "0000000000000000");
8927 let (launch, duplicate) = in_guest_liteinst_launch(&mut command, ¬hing).unwrap();
8928 assert!(duplicate.is_none());
8929 assert!(removed(&command));
8930 assert_eq!(launch.forward_request, None);
8931 assert_eq!(launch.forward_policy, None);
8932 }
8933
8934 #[test]
8942 fn detlog_forward_policy_matches_real_detlog_emission_under_field_filters() {
8943 use std::sync::Arc;
8944 use std::sync::atomic::AtomicUsize;
8945 use std::sync::atomic::Ordering;
8946
8947 use detcore::detlog::ForwardPolicy;
8948 use tracing::level_filters::LevelFilter;
8949 use tracing_subscriber::layer::SubscriberExt;
8950
8951 use crate::liteinst_bootstrap::EffectiveFilter;
8952
8953 struct Count(Arc<AtomicUsize>);
8954 impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for Count {
8955 fn on_event(
8956 &self,
8957 _event: &tracing::Event<'_>,
8958 _context: tracing_subscriber::layer::Context<'_, S>,
8959 ) {
8960 self.0.fetch_add(1, Ordering::Relaxed);
8961 }
8962 }
8963
8964 let module = module_path!();
8965 for (raw, level) in [
8966 (format!("info,{module}[{{unused}}]=warn"), LevelFilter::INFO),
8967 (
8968 format!("warn,{module}[{{unused}}]=trace"),
8969 LevelFilter::WARN,
8970 ),
8971 (
8972 format!("info,{module}[{{message}}]=warn"),
8973 LevelFilter::INFO,
8974 ),
8975 (format!("info,{module}=warn"), LevelFilter::INFO),
8976 (format!("warn,{module}=info"), LevelFilter::WARN),
8977 ("info".to_owned(), LevelFilter::INFO),
8978 ("warn".to_owned(), LevelFilter::WARN),
8979 ] {
8980 let emitted = Arc::new(AtomicUsize::new(0));
8981 let subscriber = tracing_subscriber::registry()
8982 .with(EffectiveFilter::from_directives_lossy(&raw, level).into_filter())
8983 .with(Count(emitted.clone()));
8984 let forwards = tracing::subscriber::with_default(subscriber, || {
8985 detcore::detlog!("probe record");
8986 ForwardPolicy::from_current_subscriber(&raw).forwards(module)
8987 });
8988 assert_eq!(
8989 forwards,
8990 emitted.load(Ordering::Relaxed) == 1,
8991 "RUST_LOG={raw:?} --log={level}: the policy and a real detlog! disagree"
8992 );
8993 }
8994 }
8995}
8996
8997pub mod verify_run_record;