1use detcore_model::cpu_profile::CpuProfile;
16use detcore_model::cpu_profile::HostXstate;
17use nix::sched::CpuSet;
18use nix::sched::sched_getaffinity;
19use nix::sched::sched_setaffinity;
20use nix::unistd::Pid;
21
22#[cfg(target_arch = "x86_64")]
23use crate::xstate_probe;
24
25#[cfg(target_arch = "x86_64")]
28pub type Qualification = (xstate_probe::ProbeReport, HostXstate);
29#[cfg(not(target_arch = "x86_64"))]
32pub type Qualification = (std::convert::Infallible, HostXstate);
33
34pub type AdmissionRefusal = String;
36
37#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct CpuProfileRefusal {
41 pub profile: CpuProfile,
42 pub reason: AdmissionRefusal,
43}
44
45impl CpuProfileRefusal {
46 pub fn new(profile: CpuProfile, reason: impl Into<String>) -> Self {
47 Self {
48 profile,
49 reason: reason.into(),
50 }
51 }
52}
53
54impl std::fmt::Display for CpuProfileRefusal {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 write!(f, "--cpu-profile {} refused: {}", self.profile, self.reason)
57 }
58}
59
60impl std::error::Error for CpuProfileRefusal {}
61
62pub fn record_or_replay_request(gdbserver: bool) -> AdmissionRequest<'static> {
65 AdmissionRequest {
66 backend: "ptrace",
67 ptrace: true,
68 virtualize_cpuid: true,
69 virtualize_time: true,
70 strace_only: false,
71 gdbserver,
72 namespace_only: false,
73 }
74}
75
76pub fn first_difference(recorded: &HostXstate, replay: &HostXstate) -> Option<&'static str> {
79 if recorded.vendor != replay.vendor {
80 Some("the CPU vendor")
81 } else if recorded.signature != replay.signature {
82 Some("the CPU family, model or stepping")
83 } else if recorded.xcr0 != replay.xcr0 {
84 Some("XCR0")
85 } else if recorded.xsave_leaves != replay.xsave_leaves {
86 Some("the XSAVE layout (CPUID leaf 0xD)")
87 } else if (
88 recorded.xcomp_supp,
89 recorded.xcomp_perm,
90 recorded.xcomp_guest_perm,
91 ) != (
92 replay.xcomp_supp,
93 replay.xcomp_perm,
94 replay.xcomp_guest_perm,
95 ) {
96 Some("the ARCH_GET_XCOMP masks")
97 } else if recorded.xfeature_max != replay.xfeature_max {
98 Some("XFEATURE_MAX")
99 } else if (recorded.probe_cases, recorded.probe_forced_exit)
100 != (replay.probe_cases, replay.probe_forced_exit)
101 {
102 Some("the qualification it passed")
103 } else {
104 None
105 }
106}
107
108#[derive(Clone, Copy, Debug)]
110pub struct AdmissionRequest<'a> {
111 pub backend: &'a str,
113 pub ptrace: bool,
114 pub virtualize_cpuid: bool,
115 pub virtualize_time: bool,
116 pub strace_only: bool,
117 pub gdbserver: bool,
118 pub namespace_only: bool,
120}
121
122pub fn check_request(request: &AdmissionRequest<'_>) -> Result<(), AdmissionRefusal> {
124 if !request.ptrace {
125 let why = match request.backend {
126 "kvm" => {
127 "the KVM backend switches XCR0 between the guest's and the host's on every VM \
128 exit, a transition the startup qualification does not cover"
129 }
130 "dbt" => {
131 "Detcore answers DBT's CPUID, but the DBT runtime's configuration carries no \
132 CPU profile, so it answers only the x86-64-v2 table"
133 }
134 "sabre" | "liteinst" | "in-guest-trap" => {
135 "this backend does not trap CPUID, so the guest would see the host's"
136 }
137 _ => "only the ptrace backend is qualified",
138 };
139 return Err(format!("--backend={}: {why}", request.backend));
140 }
141 if request.namespace_only {
142 return Err(
143 "--namespace-only runs the guest without Detcore, so nothing answers its CPUID".into(),
144 );
145 }
146 if !request.virtualize_cpuid || request.strace_only {
147 return Err(
148 "the profile is a virtual CPUID table, and this run does not virtualize CPUID".into(),
149 );
150 }
151 if request.gdbserver {
152 return Err(
153 "--gdbserver writes the guest's FP registers, and the kernel's regset write forces \
154 XSAVE header bits the qualification binds"
155 .into(),
156 );
157 }
158 Ok(())
159}
160
161pub fn check_host_no_rdrand_request(
164 request: &AdmissionRequest<'_>,
165) -> Result<(), AdmissionRefusal> {
166 if !request.ptrace {
167 return Err(format!(
168 "--backend={} cannot provide this profile's required CPUID faulting; \
169 use --backend=ptrace",
170 request.backend
171 ));
172 }
173 if request.namespace_only {
174 return Err(
175 "--namespace-only runs without CPUID interception; remove --namespace-only".into(),
176 );
177 }
178 if request.strace_only {
179 return Err(
180 "--strace-only runs without CPUID and TSC interception; remove --strace-only".into(),
181 );
182 }
183 if !request.virtualize_cpuid {
184 return Err("CPUID must remain intercepted; remove --no-virtualize-cpuid".into());
185 }
186 if !request.virtualize_time {
187 return Err(
188 "the host's RDTSC/RDTSCP features require Hermit's virtual time; \
189 remove --no-virtualize-time"
190 .into(),
191 );
192 }
193 Ok(())
194}
195
196#[cfg(target_arch = "x86_64")]
197const CPUID_FAULTING_REMEDY: &str = "use a CPU and Linux kernel with CPUID faulting support (capable AMD CPUs need \
198 Linux 6.17 or its CPUID-faulting backport)";
199
200pub const CPUID_FAULTING_VM_SCRIPT: &str = "tools/cpuid-fault-kmod/enable-cpuid-faulting.sh";
204
205pub fn cpuid_faulting_vm_pointer(host: &dyn HostCpuid, release: &str) -> Option<String> {
209 let hypervisor = host.cpuid(1, 0)[2] & (1 << 31) != 0;
210 let before_6_17 = kernel_version(release).is_some_and(|version| version < (6, 17));
211 (hypervisor && before_6_17).then(|| {
212 format!(
213 "this host is a virtual machine on Linux {release}; if its hypervisor offers CPUID \
214 faulting, {CPUID_FAULTING_VM_SCRIPT} in the Hermit source tree can enable it"
215 )
216 })
217}
218
219#[cfg(target_arch = "x86_64")]
220fn native_cpuid_faulting_vm_pointer() -> Option<String> {
221 let uname = nix::sys::utsname::uname().ok()?;
222 cpuid_faulting_vm_pointer(&NativeCpuid, &uname.release().to_string_lossy())
223}
224
225pub fn missing_cpuid_faulting_note(messages: &[String]) -> Option<String> {
229 let missing = messages.iter().any(|message| {
230 message.contains("ptrace-cpuid-faulting-unavailable")
231 || message.contains("CPUID faulting is unavailable")
232 });
233 let pointed = messages
234 .iter()
235 .any(|message| message.contains(CPUID_FAULTING_VM_SCRIPT));
236 if missing && !pointed {
237 native_cpuid_faulting_vm_pointer()
238 } else {
239 None
240 }
241}
242
243#[cfg(target_arch = "x86_64")]
244fn check_host_no_rdrand_cpuid(host: &dyn HostCpuid) -> Result<(), AdmissionRefusal> {
245 let intel_hybrid = host.cpuid(0, 0)[0] >= 7 && host.cpuid(7, 0)[3] & (1 << 15) != 0;
246 let amd_heterogeneous = host.cpuid(0x8000_0000, 0)[0] >= 0x8000_0026
249 && host.cpuid(0x8000_0026, 0)[0] & (1 << 30) != 0;
250 if intel_hybrid || amd_heterogeneous {
251 return Err(
252 "this is a hybrid CPU whose CPUID features can change between core types; \
253 use a homogeneous CPU host for --cpu-profile=host-no-rdrand"
254 .into(),
255 );
256 }
257 Ok(())
258}
259
260#[cfg(target_arch = "x86_64")]
265pub fn qualify_host_no_rdrand() -> Result<(), AdmissionRefusal> {
266 const ARCH_GET_CPUID: libc::c_int = 0x1011;
267 const ARCH_SET_CPUID: libc::c_int = 0x1012;
268 let child = unsafe { libc::fork() };
270 if child < 0 {
271 return Err(format!(
272 "cannot probe CPUID faulting: fork failed ({}); resolve the process-resource \
273 failure and retry",
274 std::io::Error::last_os_error()
275 ));
276 }
277 if child == 0 {
278 unsafe {
280 let initial = libc::syscall(libc::SYS_arch_prctl, ARCH_GET_CPUID, 0);
281 if initial != 0 && initial != 1 {
282 libc::_exit(1);
283 }
284 if libc::syscall(libc::SYS_arch_prctl, ARCH_SET_CPUID, 0) != 0 {
285 libc::_exit(2);
286 }
287 if libc::syscall(libc::SYS_arch_prctl, ARCH_GET_CPUID, 0) != 0 {
288 libc::_exit(3);
289 }
290 libc::_exit(0);
291 }
292 }
293 let mut status = 0;
294 loop {
295 let waited = unsafe { libc::waitpid(child, &mut status, 0) };
297 if waited == child {
298 break;
299 }
300 let error = std::io::Error::last_os_error();
301 if error.raw_os_error() != Some(libc::EINTR) {
302 return Err(format!(
303 "cannot probe CPUID faulting: waitpid failed ({error}); resolve the child \
304 process failure and retry"
305 ));
306 }
307 }
308 let failure = if libc::WIFEXITED(status) {
309 match libc::WEXITSTATUS(status) {
310 0 => return check_host_no_rdrand_cpuid(&NativeCpuid),
311 1 => "ARCH_GET_CPUID did not return a valid initial state",
312 2 => "ARCH_SET_CPUID(0) failed",
313 3 => "ARCH_GET_CPUID did not confirm that CPUID was disabled",
314 _ => "the CPUID-faulting probe exited unexpectedly",
315 }
316 } else {
317 "the CPUID-faulting probe did not exit normally"
318 };
319 let vm_pointer = native_cpuid_faulting_vm_pointer()
320 .map(|pointer| format!("; {pointer}"))
321 .unwrap_or_default();
322 Err(format!(
323 "CPUID faulting is unavailable: {failure}; {CPUID_FAULTING_REMEDY}{vm_pointer}"
324 ))
325}
326
327static HOST_NO_RDRAND_CPUS: [std::sync::atomic::AtomicBool; libc::CPU_SETSIZE as usize] =
332 [const { std::sync::atomic::AtomicBool::new(false) }; libc::CPU_SETSIZE as usize];
333
334fn affinity_cpus(affinity: &CpuSet) -> Vec<usize> {
335 (0..CpuSet::count())
336 .filter(|cpu| affinity.is_set(*cpu).unwrap_or(false))
337 .collect()
338}
339
340fn host_profile_affinity() -> Result<CpuSet, AdmissionRefusal> {
341 sched_getaffinity(Pid::from_raw(0))
342 .map_err(|error| format!("cannot read the CPUID source thread's CPU affinity: {error}"))
343}
344
345fn pin_host_cpuid_source(cpu: usize) -> Result<(), AdmissionRefusal> {
349 let mut selected = CpuSet::new();
350 selected.set(cpu).map_err(|error| error.to_string())?;
351 sched_setaffinity(Pid::from_raw(0), &selected)
352 .map_err(|error| format!("cannot pin the CPUID source thread to CPU {cpu}: {error}"))?;
353 check_host_cpuid_source(cpu)
354}
355
356fn check_host_cpuid_source(cpu: usize) -> Result<(), AdmissionRefusal> {
357 if affinity_cpus(&host_profile_affinity()?) != [cpu] {
358 return Err(format!(
359 "the tracer's CPU affinity changed after host CPUID admission; \
360 keep the tracer pinned to its admitted CPUID source CPU {cpu}"
361 ));
362 }
363 Ok(())
364}
365
366pub fn admit_host_no_rdrand(request: &AdmissionRequest<'_>) -> Result<usize, AdmissionRefusal> {
372 admit_host_no_rdrand_with(request, qualify_host_no_rdrand)
373}
374
375fn admit_host_no_rdrand_with(
376 request: &AdmissionRequest<'_>,
377 probe: impl FnOnce() -> Result<(), AdmissionRefusal>,
378) -> Result<usize, AdmissionRefusal> {
379 check_host_no_rdrand_request(request)?;
380 let original = host_profile_affinity()?;
381 let cpu = affinity_cpus(&original)
382 .first()
383 .copied()
384 .ok_or("the CPUID source thread has no allowed CPU")?;
385 let admission = pin_host_cpuid_source(cpu).and_then(|()| probe());
386 sched_setaffinity(Pid::from_raw(0), &original).map_err(|error| {
389 format!("cannot restore the caller's CPU affinity after CPUID admission: {error}; probe result: {admission:?}")
390 })?;
391 admission?;
392 HOST_NO_RDRAND_CPUS[cpu].store(true, std::sync::atomic::Ordering::Release);
393 Ok(cpu)
394}
395
396pub fn check_record_or_replay(profile: CpuProfile) -> Result<(), CpuProfileRefusal> {
400 if profile == CpuProfile::HostNoRdrand {
401 return Err(CpuProfileRefusal::new(
402 profile,
403 "record/replay is not supported because recording metadata does not bind \
404 the host CPUID state; use `hermit run --cpu-profile=host-no-rdrand --verify` \
405 for repeated runs, or record with --cpu-profile=x86-64-v2 or x86-64-v3",
406 ));
407 }
408 Ok(())
409}
410
411pub fn kernel_version(release: &str) -> Option<(u32, u32)> {
413 let mut parts = release.split(['.', '-']);
414 let major = parts.next()?.parse().ok()?;
415 let minor = parts.next()?.parse().ok()?;
416 Some((major, minor))
417}
418
419pub fn check_kernel(release: &str) -> Result<(), AdmissionRefusal> {
422 match kernel_version(release) {
423 Some(version) if version >= (5, 16) => Ok(()),
424 Some((major, minor)) => Err(format!(
425 "Linux {major}.{minor} predates the 5.16 xstate queries the profile answers from"
426 )),
427 None => Err(format!("cannot read the kernel version from {release:?}")),
428 }
429}
430
431pub trait HostCpuid {
433 fn cpuid(&self, leaf: u32, subleaf: u32) -> [u32; 4];
434}
435
436pub struct NativeCpuid;
438
439#[cfg(target_arch = "x86_64")]
440impl HostCpuid for NativeCpuid {
441 fn cpuid(&self, leaf: u32, subleaf: u32) -> [u32; 4] {
442 let r = std::arch::x86_64::__cpuid_count(leaf, subleaf);
443 [r.eax, r.ebx, r.ecx, r.edx]
444 }
445}
446
447const REGISTER_NAMES: [&str; 4] = ["EAX", "EBX", "ECX", "EDX"];
448
449pub fn check_host_cpuid(
454 host: &dyn HostCpuid,
455 advertised: &[(u32, u32, usize, u32)],
456) -> Result<(), AdmissionRefusal> {
457 let max_basic = host.cpuid(0, 0)[0];
458 let max_extended = host.cpuid(0x8000_0000, 0)[0];
459 let mut missing = Vec::new();
460 for &(leaf, subleaf, register, bits) in advertised {
461 let supported = if leaf >= 0x8000_0000 {
462 leaf <= max_extended
463 } else {
464 leaf <= max_basic
465 };
466 let native = if supported {
467 host.cpuid(leaf, subleaf)[register]
468 } else {
469 0
470 };
471 let absent = bits & !native;
472 for bit in 0..32 {
473 if absent & (1 << bit) != 0 {
474 missing.push(format!(
475 "leaf {leaf:#x}.{subleaf} {} bit {bit}",
476 REGISTER_NAMES[register]
477 ));
478 }
479 }
480 }
481 if !missing.is_empty() {
482 return Err(format!(
483 "the host lacks feature bits the profile advertises: {}",
484 missing.join(", ")
485 ));
486 }
487 if host.cpuid(1, 0)[2] & (1 << 27) == 0 {
488 return Err("the host kernel has not enabled XSAVE (OSXSAVE is clear)".into());
489 }
490 if max_basic >= 7 && host.cpuid(7, 0)[3] & (1 << 15) != 0 {
491 return Err(
492 "this is a hybrid part (leaf 7 EDX bit 15), whose core types the qualification \
493 would each have to sample"
494 .into(),
495 );
496 }
497 let vendor = host.cpuid(0, 0);
498 let amd = vendor[1] == 0x6874_7541;
499 let xsaveerptr = max_extended >= 0x8000_0008 && host.cpuid(0x8000_0008, 0)[1] & (1 << 2) != 0;
500 if amd && !xsaveerptr {
501 return Err(
502 "this AMD part lacks XSAVEERPTR, so Linux runs `fnclex; emms; fildl` before every \
503 FPU restore, which the qualification cannot exercise"
504 .into(),
505 );
506 }
507 Ok(())
508}
509
510#[cfg(target_arch = "x86_64")]
514const XFEATURE_TILEDATA: u64 = 1 << 18;
515
516#[cfg(target_arch = "x86_64")]
517const ARCH_GET_XCOMP_SUPP: libc::c_int = 0x1021;
518#[cfg(target_arch = "x86_64")]
519const ARCH_GET_XCOMP_PERM: libc::c_int = 0x1022;
520#[cfg(target_arch = "x86_64")]
521const ARCH_REQ_XCOMP_PERM: libc::c_int = 0x1023;
522#[cfg(target_arch = "x86_64")]
523const ARCH_GET_XCOMP_GUEST_PERM: libc::c_int = 0x1024;
524
525#[cfg(target_arch = "x86_64")]
526fn arch_prctl_mask(cmd: libc::c_int) -> Result<u64, AdmissionRefusal> {
527 let mut mask = 0u64;
528 let rc = unsafe { libc::syscall(libc::SYS_arch_prctl, cmd, &mut mask as *mut u64) };
530 if rc != 0 {
531 return Err(format!(
532 "arch_prctl({cmd:#x}) failed: {}",
533 std::io::Error::last_os_error()
534 ));
535 }
536 Ok(mask)
537}
538
539#[cfg(target_arch = "x86_64")]
544fn xfeature_max() -> Result<u32, AdmissionRefusal> {
545 for index in 0..64u64 {
546 let rc = unsafe { libc::syscall(libc::SYS_arch_prctl, ARCH_REQ_XCOMP_PERM, index) };
548 let errno = std::io::Error::last_os_error().raw_os_error();
549 match (rc, errno) {
550 (-1, Some(libc::EINVAL)) => return Ok(index as u32),
551 (-1, Some(libc::EOPNOTSUPP)) => {}
552 _ => {
553 return Err(format!(
554 "ARCH_REQ_XCOMP_PERM for component {index} answered {rc} ({errno:?}), \
555 not EOPNOTSUPP or EINVAL"
556 ));
557 }
558 }
559 }
560 Err("ARCH_REQ_XCOMP_PERM never answered EINVAL for indices 0-63".into())
561}
562
563#[cfg(target_arch = "x86_64")]
565fn capture(probe: &xstate_probe::ProbeReport) -> Result<HostXstate, AdmissionRefusal> {
566 let host = host_facts(probe.cases as u32, probe.forced_exit)?;
567 if host.xcr0 != probe.xcr0 {
568 return Err(format!(
569 "XCR0 changed during admission ({:#x}, then {:#x})",
570 probe.xcr0, host.xcr0
571 ));
572 }
573 Ok(host)
574}
575
576#[cfg(target_arch = "x86_64")]
579fn host_facts(probe_cases: u32, probe_forced_exit: bool) -> Result<HostXstate, AdmissionRefusal> {
580 let host = NativeCpuid;
581 let leaf0 = host.cpuid(0, 0);
582 let xcr0 = unsafe { std::arch::x86_64::_xgetbv(0) };
584 let mut xsave_leaves = Vec::new();
585 for subleaf in std::iter::once(0).chain(2..64) {
586 if subleaf == 0 || xcr0 & (1 << subleaf) != 0 {
587 let [eax, ebx, ecx, edx] = host.cpuid(0xd, subleaf);
588 xsave_leaves.push([subleaf, eax, ebx, ecx, edx]);
589 }
590 }
591 let xcomp_supp = arch_prctl_mask(ARCH_GET_XCOMP_SUPP)?;
592 refuse_dynamic_features(xcr0, xcomp_supp)?;
593 Ok(HostXstate {
594 vendor: [leaf0[1], leaf0[3], leaf0[2]],
595 signature: host.cpuid(1, 0)[0],
596 xcr0,
597 xsave_leaves,
598 xcomp_supp,
599 xcomp_perm: arch_prctl_mask(ARCH_GET_XCOMP_PERM)?,
600 xcomp_guest_perm: arch_prctl_mask(ARCH_GET_XCOMP_GUEST_PERM)?,
601 xfeature_max: xfeature_max()?,
602 probe_cases,
603 probe_forced_exit,
604 })
605}
606
607#[cfg(target_arch = "x86_64")]
614pub fn precheck_replay(
615 profile: CpuProfile,
616 recorded: Option<&HostXstate>,
617 recorded_digest: Option<&str>,
618) -> Result<HostXstate, CpuProfileRefusal> {
619 check_record_or_replay(profile)?;
620 let refuse = |why: String| CpuProfileRefusal::new(profile, why);
621 let recorded = recorded.cloned().ok_or_else(|| {
622 refuse("the recording names the profile but carries no host facts".into())
623 })?;
624 check_host_cpuid(&NativeCpuid, &detcore::v3_feature_words()).map_err(refuse)?;
625 let host = host_facts(recorded.probe_cases, recorded.probe_forced_exit).map_err(refuse)?;
626 if let Some(fact) = first_difference(&recorded, &host) {
627 return Err(refuse(format!(
628 "this host differs from the recording's in {fact}, so the guest would see a \
629 different CPU"
630 )));
631 }
632 let config = detcore::Config {
633 cpu_profile: profile,
634 cpu_profile_host: Some(recorded.clone()),
635 ..Default::default()
636 };
637 let digest = crate::record::hex_digest(&detcore::virtual_cpuid_digest(&config));
638 if recorded_digest != Some(digest.as_str()) {
639 return Err(refuse(format!(
640 "the recording's CPUID table digest {recorded_digest:?} is not this replayer's \
641 {digest}"
642 )));
643 }
644 Ok(recorded)
645}
646
647static ADMITTED: std::sync::OnceLock<Qualification> = std::sync::OnceLock::new();
652
653pub fn admit(request: &AdmissionRequest<'_>) -> Result<Qualification, AdmissionRefusal> {
659 admit_with(request, qualify_host)
660}
661
662fn admit_with(
665 request: &AdmissionRequest<'_>,
666 qualify: impl FnOnce() -> Result<Qualification, AdmissionRefusal>,
667) -> Result<Qualification, AdmissionRefusal> {
668 check_request(request)?;
669 if let Some(admitted) = ADMITTED.get() {
670 return Ok(admitted.clone());
671 }
672 let qualified = qualify()?;
673 safeptrace::forbid_fpu_register_writes();
681 Ok(ADMITTED.get_or_init(|| qualified).clone())
682}
683
684pub fn refuse_unadmitted_launch(
690 config: &detcore::Config,
691 ptrace: bool,
692) -> Result<(), CpuProfileRefusal> {
693 if config.cpu_profile.is_default() {
694 return Ok(());
695 }
696 let refuse = |why: &str| Err(CpuProfileRefusal::new(config.cpu_profile, why));
697 if config.cpu_profile == CpuProfile::HostNoRdrand {
698 if config.network_trace.mode != detcore_model::network_trace::NetworkTraceMode::Off {
701 check_record_or_replay(config.cpu_profile)?;
702 }
703 let request = AdmissionRequest {
704 backend: if ptrace { "ptrace" } else { "non-ptrace" },
705 ptrace,
706 virtualize_cpuid: config.virtualize_cpuid,
707 virtualize_time: config.virtualize_time,
708 strace_only: false,
709 gdbserver: config.gdbserver,
710 namespace_only: false,
711 };
712 check_host_no_rdrand_request(&request)
713 .map_err(|why| CpuProfileRefusal::new(config.cpu_profile, why))?;
714 let cpu = config.cpu_profile_source.filter(|cpu| {
715 HOST_NO_RDRAND_CPUS
716 .get(*cpu)
717 .is_some_and(|admitted| admitted.load(std::sync::atomic::Ordering::Acquire))
718 });
719 let Some(cpu) = cpu else {
720 return refuse(
721 "this process did not verify CPUID faulting on the configuration's source CPU; \
722 call admit_host_no_rdrand and retain its source in cpu_profile_source",
723 );
724 };
725 return pin_host_cpuid_source(cpu)
726 .map_err(|reason| CpuProfileRefusal::new(config.cpu_profile, reason));
727 }
728 if !ptrace {
729 return refuse("only the ptrace backend is qualified");
730 }
731 if !config.virtualize_cpuid || config.gdbserver {
732 return refuse("the launch does not virtualize CPUID, or serves a gdbserver");
733 }
734 match (&config.cpu_profile_host, ADMITTED.get()) {
735 (None, _) => return refuse("the configuration carries no qualified host facts"),
736 (Some(_), None) => return refuse("this process did not qualify the host"),
737 (Some(facts), Some((_, admitted))) if facts != admitted => {
738 return refuse(
739 "the configuration's host facts are not the ones this process qualified",
740 );
741 }
742 _ => {}
743 }
744 if !safeptrace::fpu_register_writes_forbidden() {
745 return refuse("ptrace floating-point register writes are not forbidden");
746 }
747 Ok(())
748}
749
750#[cfg(target_arch = "x86_64")]
754pub fn qualify_host() -> Result<Qualification, AdmissionRefusal> {
755 let release = nix::sys::utsname::uname()
756 .map_err(|e| format!("uname failed: {e}"))?
757 .release()
758 .to_string_lossy()
759 .into_owned();
760 check_kernel(&release)?;
761 check_host_cpuid(&NativeCpuid, &detcore::v3_feature_words())?;
762 let xcr0 = unsafe { std::arch::x86_64::_xgetbv(0) };
764 refuse_dynamic_features(xcr0, 0)?;
765 let probe = xstate_probe::qualify(xstate_probe::ProbeOptions::default())?;
766 let host = capture(&probe)?;
767 Ok((probe, host))
768}
769
770#[cfg(target_arch = "x86_64")]
774fn refuse_dynamic_features(xcr0: u64, xcomp_supp: u64) -> Result<(), AdmissionRefusal> {
775 if (xcr0 | xcomp_supp) & XFEATURE_TILEDATA != 0 {
776 return Err(
777 "the host has a user-dynamic xstate feature (AMX TILEDATA), whose permission \
778 requests change process state"
779 .into(),
780 );
781 }
782 Ok(())
783}
784
785#[cfg(not(target_arch = "x86_64"))]
788pub fn qualify_host_no_rdrand() -> Result<(), AdmissionRefusal> {
789 Err(non_x86_refusal())
790}
791
792#[cfg(not(target_arch = "x86_64"))]
793fn native_cpuid_faulting_vm_pointer() -> Option<String> {
794 None
795}
796
797#[cfg(not(target_arch = "x86_64"))]
799pub fn precheck_replay(
800 profile: CpuProfile,
801 _recorded: Option<&HostXstate>,
802 _recorded_digest: Option<&str>,
803) -> Result<HostXstate, CpuProfileRefusal> {
804 check_record_or_replay(profile)?;
805 Err(CpuProfileRefusal::new(profile, non_x86_refusal()))
806}
807
808#[cfg(not(target_arch = "x86_64"))]
810pub fn qualify_host() -> Result<Qualification, AdmissionRefusal> {
811 Err(non_x86_refusal())
812}
813
814#[cfg(not(target_arch = "x86_64"))]
816fn non_x86_refusal() -> AdmissionRefusal {
817 format!(
818 "the nondefault CPU profiles describe x86-64 CPUs, and this host is {}",
819 std::env::consts::ARCH
820 )
821}
822
823#[cfg(test)]
824mod tests {
825 use std::collections::BTreeMap;
826
827 #[test]
830 fn a_dynamic_xstate_feature_is_refused() {
831 assert_eq!(super::refuse_dynamic_features(0x2e7, 0x2e7), Ok(()));
832 for (xcr0, supp) in [(0x602e7, 0x2e7), (0x2e7, 0x602e7), (0x402e7, 0x402e7)] {
833 assert!(
834 super::refuse_dynamic_features(xcr0, supp)
835 .unwrap_err()
836 .contains("AMX TILEDATA"),
837 "{xcr0:#x} {supp:#x}"
838 );
839 }
840 }
841
842 use super::*;
843
844 fn request(backend: &str) -> AdmissionRequest<'_> {
845 AdmissionRequest {
846 backend,
847 ptrace: backend == "ptrace",
848 virtualize_cpuid: true,
849 virtualize_time: true,
850 strace_only: false,
851 gdbserver: false,
852 namespace_only: false,
853 }
854 }
855
856 #[test]
857 fn only_a_ptrace_run_with_virtual_cpuid_and_no_gdbserver_is_considered() {
858 assert_eq!(check_request(&request("ptrace")), Ok(()));
859 for backend in [
860 "kvm",
861 "dbt",
862 "sabre",
863 "liteinst",
864 "in-guest-trap",
865 "e9patch",
866 ] {
867 let refusal = check_request(&request(backend)).unwrap_err();
868 assert!(
869 refusal.starts_with(&format!("--backend={backend}")),
870 "{refusal}"
871 );
872 }
873 assert!(check_request(&request("kvm")).unwrap_err().contains("XCR0"));
874 assert!(
875 check_request(&request("dbt"))
876 .unwrap_err()
877 .contains("carries no CPU profile")
878 );
879 let mut r = request("ptrace");
880 r.virtualize_cpuid = false;
881 assert!(check_request(&r).is_err());
882 let mut r = request("ptrace");
883 r.strace_only = true;
884 assert!(check_request(&r).is_err());
885 let mut r = request("ptrace");
886 r.gdbserver = true;
887 assert!(check_request(&r).unwrap_err().contains("--gdbserver"));
888 let mut r = request("ptrace");
889 r.namespace_only = true;
890 assert!(check_request(&r).unwrap_err().contains("--namespace-only"));
891 }
892
893 #[test]
894 fn host_profile_requires_interception_and_virtual_time() {
895 let mut host = qualifying_host();
896 assert_eq!(check_host_no_rdrand_cpuid(&host), Ok(()));
897 host.0.get_mut(&(7, 0)).unwrap()[3] |= 1 << 15;
898 assert!(
899 check_host_no_rdrand_cpuid(&host)
900 .unwrap_err()
901 .contains("use a homogeneous CPU host")
902 );
903 host.0.get_mut(&(7, 0)).unwrap()[3] &= !(1 << 15);
904 host.0.insert((0x8000_0000, 0), [0x8000_0026, 0, 0, 0]);
905 host.0.insert((0x8000_0026, 0), [1 << 30, 0, 0, 0]);
906 assert!(
907 check_host_no_rdrand_cpuid(&host)
908 .unwrap_err()
909 .contains("use a homogeneous CPU host")
910 );
911 host.0.get_mut(&(0x8000_0026, 0)).unwrap()[0] = 0;
912 assert_eq!(check_host_no_rdrand_cpuid(&host), Ok(()));
913 host.0.get_mut(&(0x8000_0000, 0)).unwrap()[0] = 0x8000_0025;
915 host.0.get_mut(&(0x8000_0026, 0)).unwrap()[0] = 1 << 30;
916 assert_eq!(check_host_no_rdrand_cpuid(&host), Ok(()));
917 assert_eq!(check_host_no_rdrand_request(&request("ptrace")), Ok(()));
918 for backend in [
919 "kvm",
920 "dbt",
921 "sabre",
922 "liteinst",
923 "in-guest-trap",
924 "e9patch",
925 ] {
926 let refusal = check_host_no_rdrand_request(&request(backend)).unwrap_err();
927 assert!(
928 refusal.contains(&format!("--backend={backend}")),
929 "{refusal}"
930 );
931 assert!(refusal.contains("use --backend=ptrace"), "{refusal}");
932 }
933 for (r, option) in [
934 (
935 AdmissionRequest {
936 virtualize_cpuid: false,
937 ..request("ptrace")
938 },
939 "--no-virtualize-cpuid",
940 ),
941 (
942 AdmissionRequest {
943 virtualize_time: false,
944 ..request("ptrace")
945 },
946 "--no-virtualize-time",
947 ),
948 (
949 AdmissionRequest {
950 strace_only: true,
951 ..request("ptrace")
952 },
953 "--strace-only",
954 ),
955 (
956 AdmissionRequest {
957 namespace_only: true,
958 ..request("ptrace")
959 },
960 "--namespace-only",
961 ),
962 ] {
963 let refusal =
964 admit_host_no_rdrand_with(&r, || panic!("invalid request probed")).unwrap_err();
965 assert!(refusal.contains(&format!("remove {option}")), "{refusal}");
966 }
967 let mut gdb = request("ptrace");
969 gdb.gdbserver = true;
970 assert_eq!(check_host_no_rdrand_request(&gdb), Ok(()));
971 }
972
973 #[test]
974 fn host_profile_refuses_absent_faulting_and_requires_admission_to_launch() {
975 let original = host_profile_affinity().unwrap();
976 let mut config = detcore::Config {
977 cpu_profile: CpuProfile::HostNoRdrand,
978 ..Default::default()
979 };
980 let unadmitted = refuse_unadmitted_launch(&config, true).unwrap_err();
981 assert!(unadmitted.reason.contains("did not verify CPUID faulting"));
982 let simulated = "CPUID faulting is unavailable: ARCH_SET_CPUID(0) failed";
983 let refusal =
984 admit_host_no_rdrand_with(&request("ptrace"), || Err(simulated.into())).unwrap_err();
985 assert_eq!(refusal, simulated);
986 assert_eq!(
987 affinity_cpus(&host_profile_affinity().unwrap()),
988 affinity_cpus(&original)
989 );
990 assert!(refuse_unadmitted_launch(&config, true).is_err());
991
992 let source = admit_host_no_rdrand_with(&request("ptrace"), || Ok(())).unwrap();
995 assert_eq!(source, affinity_cpus(&original)[0]);
996 assert_eq!(
997 affinity_cpus(&host_profile_affinity().unwrap()),
998 affinity_cpus(&original)
999 );
1000 assert!(refuse_unadmitted_launch(&config, true).is_err());
1001 config.cpu_profile_source = Some(source);
1002 for mode in [
1003 detcore_model::network_trace::NetworkTraceMode::Record,
1004 detcore_model::network_trace::NetworkTraceMode::Replay,
1005 ] {
1006 config.network_trace.mode = mode;
1007 let refusal = refuse_unadmitted_launch(&config, true).unwrap_err();
1008 assert!(refusal.reason.contains("record/replay is not supported"));
1009 }
1010 config.network_trace.mode = detcore_model::network_trace::NetworkTraceMode::Off;
1011 let other_config = config.clone();
1012 let original_cpus = affinity_cpus(&original);
1013 std::thread::spawn(move || {
1014 let other_cpu = *original_cpus.last().unwrap();
1017 let mut other = CpuSet::new();
1018 other.set(other_cpu).unwrap();
1019 sched_setaffinity(Pid::from_raw(0), &other).unwrap();
1020 assert_eq!(refuse_unadmitted_launch(&other_config, true), Ok(()));
1021 assert_eq!(affinity_cpus(&host_profile_affinity().unwrap()), [source]);
1022 })
1023 .join()
1024 .unwrap();
1025 assert_eq!(refuse_unadmitted_launch(&config, true), Ok(()));
1026 assert_eq!(affinity_cpus(&host_profile_affinity().unwrap()), [source]);
1027 assert!(refuse_unadmitted_launch(&config, false).is_err());
1028 config.virtualize_cpuid = false;
1029 assert!(refuse_unadmitted_launch(&config, true).is_err());
1030 config.virtualize_cpuid = true;
1031 config.virtualize_time = false;
1032 assert!(refuse_unadmitted_launch(&config, true).is_err());
1033 sched_setaffinity(Pid::from_raw(0), &original).unwrap();
1034 }
1035
1036 #[test]
1037 fn host_profile_recordings_are_refused_before_v3_host_checks() {
1038 for profile in [CpuProfile::X86_64V2, CpuProfile::X86_64V3] {
1039 assert_eq!(check_record_or_replay(profile), Ok(()));
1040 }
1041 let refused = check_record_or_replay(CpuProfile::HostNoRdrand).unwrap_err();
1042 assert_eq!(refused.profile, CpuProfile::HostNoRdrand);
1043 assert!(refused.reason.contains("record/replay is not supported"));
1044 assert!(
1045 refused
1046 .reason
1047 .contains("hermit run --cpu-profile=host-no-rdrand --verify")
1048 );
1049 assert_eq!(
1050 precheck_replay(CpuProfile::HostNoRdrand, None, None),
1051 Err(refused)
1052 );
1053 assert_eq!(
1054 CpuProfileRefusal::new(CpuProfile::HostNoRdrand, "no faulting").to_string(),
1055 "--cpu-profile host-no-rdrand refused: no faulting"
1056 );
1057 }
1058
1059 #[test]
1065 fn only_the_admitted_facts_launch_and_the_write_ban_comes_first() {
1066 let host = HostXstate {
1067 vendor: [1, 2, 3],
1068 signature: 0x00b1_0f11,
1069 xcr0: 0x2e7,
1070 xsave_leaves: vec![[0, 0x2e7, 0x988, 0x988, 0]],
1071 xcomp_supp: 0x2e7,
1072 xcomp_perm: 0x2e7,
1073 xcomp_guest_perm: 0x2e7,
1074 xfeature_max: 20,
1075 probe_cases: 33,
1076 probe_forced_exit: false,
1077 };
1078 let mut config = detcore::Config {
1079 cpu_profile: detcore_model::cpu_profile::CpuProfile::X86_64V3,
1080 cpu_profile_host: Some(host.clone()),
1081 ..Default::default()
1082 };
1083 let unadmitted = refuse_unadmitted_launch(&config, true).unwrap_err();
1084 assert!(
1085 unadmitted.reason.contains("did not qualify"),
1086 "{unadmitted}"
1087 );
1088 assert_eq!(
1089 refuse_unadmitted_launch(&detcore::Config::default(), false),
1090 Ok(())
1091 );
1092
1093 let refused = admit_with(&request("kvm"), || {
1094 panic!("a refused request probed the host")
1095 });
1096 assert!(refused.is_err());
1097 assert!(!safeptrace::fpu_register_writes_forbidden());
1098 assert!(ADMITTED.get().is_none());
1099
1100 let report = xstate_probe::ProbeReport {
1101 cpu: 3,
1102 xcr0: 0x2e7,
1103 cases: 33,
1104 cells: 396,
1105 repetitions: 32,
1106 certified_pairs: 12672,
1107 forced_exit: false,
1108 reload_bound: 0,
1109 cpuid_min_cycles: 0,
1110 calibration: String::new(),
1111 retried_references: 0,
1112 retried_reloads: 0,
1113 retried_exits: 0,
1114 counter_reopens: 0,
1115 };
1116 let admitted = admit_with(&request("ptrace"), || {
1117 assert!(!safeptrace::fpu_register_writes_forbidden());
1119 Ok((report.clone(), host.clone()))
1120 });
1121 assert_eq!(admitted, Ok((report, host.clone())));
1122 assert!(safeptrace::fpu_register_writes_forbidden());
1123 assert_eq!(refuse_unadmitted_launch(&config, true), Ok(()));
1124 assert!(refuse_unadmitted_launch(&config, false).is_err());
1125 let mut other = host;
1126 other.probe_cases = 0;
1127 config.cpu_profile_host = Some(other);
1128 let forged = refuse_unadmitted_launch(&config, true).unwrap_err();
1129 assert!(
1130 forged
1131 .reason
1132 .contains("not the ones this process qualified"),
1133 "{forged}"
1134 );
1135 config.cpu_profile_host = None;
1136 assert!(refuse_unadmitted_launch(&config, true).is_err());
1137 }
1138
1139 #[test]
1140 fn a_replay_host_is_compared_fact_by_fact() {
1141 let recorded = HostXstate {
1142 vendor: [1, 2, 3],
1143 signature: 0x00b1_0f11,
1144 xcr0: 0x2e7,
1145 xsave_leaves: vec![[0, 0x2e7, 0x988, 0x988, 0]],
1146 xcomp_supp: 0x2e7,
1147 xcomp_perm: 0x2e7,
1148 xcomp_guest_perm: 0x2e7,
1149 xfeature_max: 20,
1150 probe_cases: 33,
1151 probe_forced_exit: false,
1152 };
1153 assert_eq!(first_difference(&recorded, &recorded.clone()), None);
1154 let differs = |f: fn(&mut HostXstate)| {
1155 let mut other = recorded.clone();
1156 f(&mut other);
1157 first_difference(&recorded, &other)
1158 };
1159 assert_eq!(differs(|h| h.vendor[0] = 9), Some("the CPU vendor"));
1160 assert_eq!(
1161 differs(|h| h.signature = 0x00a1_0f11),
1162 Some("the CPU family, model or stepping")
1163 );
1164 assert_eq!(differs(|h| h.xcr0 = 0x207), Some("XCR0"));
1165 assert_eq!(
1166 differs(|h| h.xsave_leaves[0][2] = 0xa88),
1167 Some("the XSAVE layout (CPUID leaf 0xD)")
1168 );
1169 assert_eq!(
1170 differs(|h| h.xcomp_perm = 0x207),
1171 Some("the ARCH_GET_XCOMP masks")
1172 );
1173 assert_eq!(differs(|h| h.xfeature_max = 19), Some("XFEATURE_MAX"));
1174 assert_eq!(
1175 differs(|h| h.probe_forced_exit = true),
1176 Some("the qualification it passed")
1177 );
1178 assert_eq!(
1179 CpuProfileRefusal::new(CpuProfile::X86_64V3, "why").to_string(),
1180 "--cpu-profile x86-64-v3 refused: why"
1181 );
1182 }
1183
1184 #[test]
1185 fn kernels_before_5_16_are_refused() {
1186 assert_eq!(kernel_version("7.1.3-0_fbk0_0_g295ad0959f34"), Some((7, 1)));
1187 assert_eq!(check_kernel("5.16.0"), Ok(()));
1188 assert_eq!(check_kernel("6.4.3-0_fbk20"), Ok(()));
1189 assert!(check_kernel("5.15.120").is_err());
1190 assert!(check_kernel("4.18.0-553.el8").is_err());
1191 assert!(check_kernel("garbage").is_err());
1192 }
1193
1194 struct FakeHost(BTreeMap<(u32, u32), [u32; 4]>);
1195
1196 impl HostCpuid for FakeHost {
1197 fn cpuid(&self, leaf: u32, subleaf: u32) -> [u32; 4] {
1198 self.0.get(&(leaf, subleaf)).copied().unwrap_or([0; 4])
1199 }
1200 }
1201
1202 fn qualifying_host() -> FakeHost {
1204 let mut regs = BTreeMap::new();
1205 regs.insert((0, 0), [0xd, 0x6874_7541, 0x444d_4163, 0x6974_6e65]);
1206 regs.insert((0x8000_0000, 0), [0x8000_0008, 0, 0, 0]);
1207 regs.insert((0x8000_0008, 0), [0, 1 << 2, 0, 0]);
1208 for &(leaf, subleaf, register, bits) in &detcore::v3_feature_words() {
1209 regs.entry((leaf, subleaf)).or_insert([0; 4])[register] |= bits;
1210 }
1211 regs.get_mut(&(1, 0)).unwrap()[2] |= 1 << 27;
1212 FakeHost(regs)
1213 }
1214
1215 #[test]
1216 fn a_host_with_every_v3_bit_qualifies_and_removing_any_one_is_named() {
1217 let words = detcore::v3_feature_words();
1218 let host = qualifying_host();
1219 assert_eq!(check_host_cpuid(&host, &words), Ok(()));
1220 assert!(words.iter().all(|&(leaf, _, register, bits)| !(leaf == 1
1222 && register == 2
1223 && bits & (1 << 31) != 0)));
1224 let mut checked = 0;
1225 for &(leaf, subleaf, register, bits) in &words {
1226 for bit in (0..32).filter(|b| bits & (1 << b) != 0) {
1227 let mut host = qualifying_host();
1228 host.0.get_mut(&(leaf, subleaf)).unwrap()[register] &= !(1 << bit);
1229 if leaf == 1 && register == 2 && bit == 27 {
1230 continue; }
1232 let refusal = check_host_cpuid(&host, &words).unwrap_err();
1233 assert!(
1234 refusal.contains(&format!(
1235 "leaf {leaf:#x}.{subleaf} {} bit {bit}",
1236 REGISTER_NAMES[register]
1237 )),
1238 "{refusal}"
1239 );
1240 checked += 1;
1241 }
1242 }
1243 let advertised: u32 = words.iter().map(|&(_, _, _, bits)| bits.count_ones()).sum();
1244 assert_eq!(checked, advertised - 1);
1246 }
1247
1248 #[test]
1251 fn cpuid_faulting_vm_pointer_needs_a_hypervisor_and_a_kernel_before_6_17() {
1252 let mut vm = qualifying_host();
1253 vm.0.get_mut(&(1, 0)).unwrap()[2] |= 1 << 31;
1254 let pointer = cpuid_faulting_vm_pointer(&vm, "6.16.1-0_fbk5_0_g0e127f210adc").unwrap();
1255 assert!(pointer.contains(CPUID_FAULTING_VM_SCRIPT), "{pointer}");
1256 assert!(pointer.contains("Linux 6.16.1-0_fbk5"), "{pointer}");
1257 assert!(cpuid_faulting_vm_pointer(&vm, "5.19.0").is_some());
1258 assert_eq!(cpuid_faulting_vm_pointer(&vm, "6.17.0"), None);
1259 assert_eq!(cpuid_faulting_vm_pointer(&vm, "7.1.3-0_fbk0"), None);
1260 assert_eq!(cpuid_faulting_vm_pointer(&vm, "garbage"), None);
1261 let bare_metal = qualifying_host();
1262 assert_eq!(cpuid_faulting_vm_pointer(&bare_metal, "6.16.1"), None);
1263
1264 assert_eq!(
1267 missing_cpuid_faulting_note(&["permission denied".to_string()]),
1268 None
1269 );
1270 assert_eq!(
1271 missing_cpuid_faulting_note(&[format!(
1272 "CPUID faulting is unavailable: ARCH_SET_CPUID(0) failed; {pointer}"
1273 )]),
1274 None
1275 );
1276 }
1277
1278 #[test]
1279 fn osxsave_hybrid_parts_and_old_amd_are_refused() {
1280 let words = detcore::v3_feature_words();
1281 let mut host = qualifying_host();
1282 host.0.get_mut(&(1, 0)).unwrap()[2] &= !(1 << 27);
1283 assert!(check_host_cpuid(&host, &words).is_err());
1284 let mut host = qualifying_host();
1285 host.0.get_mut(&(7, 0)).unwrap()[3] |= 1 << 15;
1286 assert!(
1287 check_host_cpuid(&host, &words)
1288 .unwrap_err()
1289 .contains("hybrid")
1290 );
1291 let mut host = qualifying_host();
1292 host.0.get_mut(&(0x8000_0008, 0)).unwrap()[1] = 0;
1293 assert!(
1294 check_host_cpuid(&host, &words)
1295 .unwrap_err()
1296 .contains("XSAVEERPTR")
1297 );
1298 }
1299}