1pub mod asm;
37pub mod irq_counter;
38
39use std::sync::Arc;
40use std::sync::atomic::AtomicI32;
41use std::sync::atomic::AtomicU64;
42use std::sync::atomic::Ordering;
43
44use asm::ArmCtx;
45use asm::ArmStatus;
46use asm::Kind;
47use asm::Mode;
48use asm::PeerCtx;
49use asm::PeerState;
50use irq_counter::CpuModel;
51use irq_counter::IrqCounter;
52
53pub const MIN_REPETITIONS: u32 = 32;
55pub const MAX_REPETITIONS: u32 = 4096;
57const MAX_ATTEMPTS: u32 = 200;
62pub const COVERED_XCR0: u64 = 0b10_1110_0111;
65const AVX512_COMPONENTS: u64 = 0b1110_0000;
66const PKRU_COMPONENT: u32 = 9;
67const SIGUSR1: i32 = 10;
68pub const MXCSR_VALUES: [u32; 2] = [0x1f80, 0x3f80];
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub struct Case {
77 pub kind: Kind,
78 pub component: u32,
79 pub targets: u64,
80 pub live: u64,
81}
82
83impl Case {
84 pub fn name(&self) -> String {
85 match self.kind {
86 Kind::XrstorInit | Kind::XrstorHeaderSet | Kind::CompactedXrstorInit => {
87 format!("{:?} of component {}", self.kind, self.component)
88 }
89 kind => format!("{kind:?}"),
90 }
91 }
92}
93
94pub type ProbeRefusal = String;
96
97pub fn check_xcr0(xcr0: u64) -> Result<(), ProbeRefusal> {
101 if xcr0 & !COVERED_XCR0 != 0 {
102 return Err(format!(
103 "XCR0 {xcr0:#x} enables components the XINUSE probe does not cover \
104 (it covers {COVERED_XCR0:#x})"
105 ));
106 }
107 if xcr0 & 0b111 != 0b111 {
108 return Err(format!(
109 "XCR0 {xcr0:#x} does not enable x87, SSE and AVX, which x86-64-v3 needs"
110 ));
111 }
112 let avx512 = xcr0 & AVX512_COMPONENTS;
113 if avx512 != 0 && avx512 != AVX512_COMPONENTS {
114 return Err(format!(
115 "XCR0 {xcr0:#x} enables only some of the AVX-512 components"
116 ));
117 }
118 Ok(())
119}
120
121pub fn pkru_usable(xcr0: u64, ospke: bool) -> Result<bool, ProbeRefusal> {
127 let component = xcr0 & (1 << PKRU_COMPONENT) != 0;
128 if component && !ospke {
129 return Err(format!(
130 "XCR0 {xcr0:#x} enables PKRU but the kernel has not enabled protection keys \
131 (OSPKE is clear)"
132 ));
133 }
134 Ok(component)
135}
136
137fn native_ospke() -> bool {
139 std::arch::x86_64::__cpuid_count(7, 0).ecx & (1 << 4) != 0
140}
141
142pub fn case_list(xcr0: u64) -> Result<Vec<Case>, ProbeRefusal> {
145 check_xcr0(xcr0)?;
146 let avx512 = xcr0 & AVX512_COMPONENTS != 0;
147 let pkru = xcr0 & (1 << PKRU_COMPONENT) != 0;
148 let others = |targets: u64| xcr0 & !targets;
149 let mut cases = Vec::new();
150 for component in 0..64 {
151 if xcr0 & (1 << component) == 0 {
152 continue;
153 }
154 let targets = 1u64 << component;
155 for kind in [
156 Kind::XrstorInit,
157 Kind::XrstorHeaderSet,
158 Kind::CompactedXrstorInit,
159 ] {
160 cases.push(Case {
161 kind,
162 component,
163 targets,
164 live: others(targets),
165 });
166 }
167 }
168 let mut push = |kind: Kind, targets: u64| {
169 let targets = targets & xcr0;
170 cases.push(Case {
171 kind,
172 component: 0,
173 targets,
174 live: others(targets),
175 })
176 };
177 if pkru {
178 push(Kind::WrpkruZero, 1 << PKRU_COMPONENT);
179 push(Kind::Sigreturn, 1 << PKRU_COMPONENT);
180 }
181 push(Kind::Vzeroupper, 0b100_0100);
183 push(Kind::LegacyPxor, 0b10);
185 push(Kind::Vex128Zero, 0b100_0110);
187 if avx512 {
188 push(Kind::Hi16Zero, 0b1000_0000);
189 push(Kind::PxorVzeroupperHi16, 0b1100_0110);
190 push(Kind::OpmaskZero, 0b10_0000);
191 }
192 push(Kind::X87FullInit, 0b1);
193 push(Kind::X87DirtyFullInit, 0b1);
194 push(Kind::FxrstorInit, 0b11);
196 push(Kind::VzeroallAll, 0b1100_0110);
197 Ok(cases)
198}
199
200pub fn expected_case_count(xcr0: u64) -> usize {
203 let avx512 = xcr0 & AVX512_COMPONENTS != 0;
204 let pkru = xcr0 & (1 << PKRU_COMPONENT) != 0;
205 3 * xcr0.count_ones() as usize + 2 * pkru as usize + 3 + 3 * avx512 as usize + 4
206}
207
208#[derive(Clone, Debug, PartialEq, Eq)]
210pub struct Layout {
211 pub xcr0: u64,
212 pub size: usize,
214 pub components: Vec<(u32, usize, usize)>,
216}
217
218impl Layout {
219 pub fn host() -> Result<Self, ProbeRefusal> {
220 let xcr0 = unsafe { std::arch::x86_64::_xgetbv(0) };
223 let d0 = std::arch::x86_64::__cpuid_count(0xd, 0);
224 let mut components = Vec::new();
225 for i in 2..64u32 {
226 if xcr0 & (1 << i) != 0 {
227 let d = std::arch::x86_64::__cpuid_count(0xd, i);
228 components.push((i, d.ebx as usize, d.eax as usize));
229 }
230 }
231 let layout = Layout {
232 xcr0,
233 size: d0.ebx as usize,
234 components,
235 };
236 if layout.size == 0 || layout.size + 64 > USED_BYTES {
237 return Err(format!(
238 "the XSAVE area for XCR0 {xcr0:#x} is {} bytes, outside what the probe holds",
239 layout.size
240 ));
241 }
242 Ok(layout)
243 }
244
245 pub fn is_initial(&self, image: &[u8], pkru: u32, component: u32) -> bool {
248 let header = u64::from_le_bytes(image[512..520].try_into().unwrap());
249 if header & (1 << component) == 0 {
250 return true;
251 }
252 match component {
253 0 => {
254 let fcw = u16::from_le_bytes([image[0], image[1]]);
255 fcw == 0x37f && image[2..24].iter().all(|b| *b == 0) && all_zero(&image[32..160])
256 }
257 1 => all_zero(&image[160..416]),
258 PKRU_COMPONENT => pkru == 0,
259 _ => self
260 .components
261 .iter()
262 .find(|(i, _, _)| *i == component)
263 .is_some_and(|(_, offset, size)| all_zero(&image[*offset..*offset + *size])),
264 }
265 }
266}
267
268fn all_zero(bytes: &[u8]) -> bool {
269 bytes.iter().all(|b| *b == 0)
270}
271
272const AREA_BYTES: usize = 16384;
273
274#[repr(C, align(64))]
275struct Area([u8; AREA_BYTES]);
276
277impl Area {
278 fn boxed() -> Box<Area> {
279 Box::new(Area([0; AREA_BYTES]))
280 }
281}
282
283fn clear(area: &mut Area) {
286 area.0[..USED_BYTES].fill(0);
287}
288
289const USED_BYTES: usize = 8192;
292
293fn fill_standard_image(area: &mut Area, mxcsr: u32, header: u64) {
295 clear(area);
296 area.0[0..2].copy_from_slice(&0x37fu16.to_le_bytes());
297 area.0[24..28].copy_from_slice(&mxcsr.to_le_bytes());
298 area.0[28..32].copy_from_slice(&0xffffu32.to_le_bytes()); area.0[512..520].copy_from_slice(&header.to_le_bytes());
300}
301
302fn fill_compacted_image(area: &mut Area, mxcsr: u32, xcr0: u64) {
304 clear(area);
305 area.0[0..2].copy_from_slice(&0x37fu16.to_le_bytes());
306 area.0[24..28].copy_from_slice(&mxcsr.to_le_bytes());
307 area.0[520..528].copy_from_slice(&((1u64 << 63) | xcr0).to_le_bytes());
308}
309
310fn fill_fx_image(area: &mut Area, mxcsr: u32) {
312 clear(area);
313 area.0[0..2].copy_from_slice(&0x37fu16.to_le_bytes());
314 area.0[24..28].copy_from_slice(&mxcsr.to_le_bytes());
315}
316
317static PROBE_TID: AtomicU64 = AtomicU64::new(0);
318static PROBE_SIGNALS: AtomicU64 = AtomicU64::new(0);
319static PEER_SIGNALS: AtomicU64 = AtomicU64::new(0);
320
321extern "C" fn count_signal(_signal: libc::c_int) {
322 let tid = unsafe { libc::gettid() } as u64;
324 if tid == PROBE_TID.load(Ordering::SeqCst) {
325 PROBE_SIGNALS.fetch_add(1, Ordering::SeqCst);
326 } else {
327 PEER_SIGNALS.fetch_add(1, Ordering::SeqCst);
328 }
329}
330
331fn switches_and_faults() -> Result<i64, ProbeRefusal> {
333 let mut usage = std::mem::MaybeUninit::<libc::rusage>::zeroed();
334 if unsafe { libc::getrusage(libc::RUSAGE_THREAD, usage.as_mut_ptr()) } != 0 {
336 return Err(format!(
337 "getrusage failed: {}",
338 std::io::Error::last_os_error()
339 ));
340 }
341 let u = unsafe { usage.assume_init() };
343 Ok(u.ru_nvcsw + u.ru_nivcsw + u.ru_minflt + u.ru_majflt)
344}
345
346fn cpu_timeslices(cpu: usize) -> Result<u64, ProbeRefusal> {
352 use std::io::BufRead;
353 let file = std::fs::File::open("/proc/schedstat")
354 .map_err(|e| format!("cannot read /proc/schedstat: {e}"))?;
355 let want = format!("cpu{cpu} ");
356 let mut text = String::new();
357 for line in std::io::BufReader::with_capacity(4096, file).lines() {
358 let line = line.map_err(|e| format!("cannot read /proc/schedstat: {e}"))?;
359 let done = line.starts_with(&want);
360 text.push_str(&line);
361 text.push('\n');
362 if done {
363 break;
364 }
365 }
366 parse_schedstat_timeslices(&text, cpu)
367}
368
369pub fn parse_schedstat_timeslices(text: &str, cpu: usize) -> Result<u64, ProbeRefusal> {
373 let mut lines = text.lines();
374 let version = lines
375 .next()
376 .and_then(|l| l.strip_prefix("version "))
377 .and_then(|v| v.trim().parse::<u32>().ok())
378 .ok_or("/proc/schedstat has no version line")?;
379 if !(15..=17).contains(&version) {
380 return Err(format!(
381 "/proc/schedstat version {version} is not one whose cpu line the probe reads (15-17)"
382 ));
383 }
384 let want = format!("cpu{cpu}");
385 for line in lines {
386 let mut fields = line.split_whitespace();
387 if fields.next() == Some(want.as_str()) {
388 return fields
389 .nth(8)
390 .and_then(|f| f.parse::<u64>().ok())
391 .ok_or_else(|| format!("/proc/schedstat line for {want} has no timeslice count"));
392 }
393 }
394 Err(format!("/proc/schedstat has no line for {want}"))
395}
396
397#[derive(Clone, Debug, PartialEq, Eq)]
399pub struct ProbeReport {
400 pub cpu: usize,
401 pub xcr0: u64,
402 pub cases: usize,
403 pub cells: usize,
404 pub repetitions: u32,
405 pub certified_pairs: u64,
406 pub forced_exit: bool,
407 pub reload_bound: u64,
408 pub cpuid_min_cycles: u64,
409 pub calibration: String,
410 pub retried_references: u64,
412 pub retried_reloads: u64,
413 pub retried_exits: u64,
414 pub counter_reopens: u32,
417}
418
419#[derive(Clone, Copy, Debug)]
421pub struct ProbeOptions {
422 pub repetitions: u32,
423 pub forced_exit: Option<bool>,
425}
426
427impl Default for ProbeOptions {
428 fn default() -> Self {
429 ProbeOptions {
430 repetitions: MIN_REPETITIONS,
431 forced_exit: None,
432 }
433 }
434}
435
436pub fn qualify(options: ProbeOptions) -> Result<ProbeReport, ProbeRefusal> {
439 if !(MIN_REPETITIONS..=MAX_REPETITIONS).contains(&options.repetitions) {
440 return Err(format!(
441 "the probe needs {MIN_REPETITIONS} to {MAX_REPETITIONS} repetitions, not {}",
442 options.repetitions
443 ));
444 }
445 let _serialized = ProbeLock::acquire()?;
448 std::thread::Builder::new()
449 .name("xinuse-probe".into())
450 .spawn(move || Probe::run(options))
451 .map_err(|e| format!("cannot start the probe thread: {e}"))?
452 .join()
453 .map_err(|_| "the probe thread panicked".to_owned())?
454}
455
456struct ProbeLock {
459 _file: std::fs::File,
460}
461
462impl ProbeLock {
463 fn acquire() -> Result<Self, ProbeRefusal> {
464 use std::os::fd::AsRawFd;
465 let uid = unsafe { libc::getuid() };
467 let path = std::env::temp_dir().join(format!("hermit-xinuse-probe-{uid}.lock"));
468 let file = std::fs::OpenOptions::new()
469 .create(true)
470 .truncate(false)
471 .write(true)
472 .open(&path)
473 .map_err(|e| format!("cannot open the probe lock {}: {e}", path.display()))?;
474 if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) } != 0 {
476 return Err(format!(
477 "cannot lock {}: {}",
478 path.display(),
479 std::io::Error::last_os_error()
480 ));
481 }
482 Ok(ProbeLock { _file: file })
483 }
484}
485
486fn pin_to(cpu: usize) -> Result<(), ProbeRefusal> {
487 unsafe {
489 let mut set: libc::cpu_set_t = std::mem::zeroed();
490 libc::CPU_SET(cpu, &mut set);
491 if libc::sched_setaffinity(0, std::mem::size_of_val(&set), &set) != 0 {
492 return Err(format!(
493 "cannot pin the probe to CPU {cpu}: {}",
494 std::io::Error::last_os_error()
495 ));
496 }
497 }
498 Ok(())
499}
500
501fn probe_cpu() -> Result<usize, ProbeRefusal> {
510 let allowed = allowed_cpus()?;
511 let read = || {
512 std::fs::read_to_string("/proc/stat").map_err(|e| format!("cannot read /proc/stat: {e}"))
513 };
514 let before = read()?;
515 std::thread::sleep(std::time::Duration::from_millis(100));
516 let after = read()?;
517 choose_probe_cpu(&allowed, &before, &after)
518 .ok_or_else(|| "the probe may run on no CPU".to_owned())
519}
520
521const IDLE_ENOUGH: f64 = 0.95;
524
525fn choose_probe_cpu(allowed: &[usize], before: &str, after: &str) -> Option<usize> {
527 let times = |text: &str, cpu: usize| -> Option<(u64, u64)> {
529 let want = format!("cpu{cpu} ");
530 let line = text.lines().find(|line| line.starts_with(&want))?;
531 let fields: Vec<u64> = line
532 .split_whitespace()
533 .skip(1)
534 .filter_map(|field| field.parse().ok())
535 .collect();
536 Some((fields.get(3)? + fields.get(4)?, fields.iter().sum()))
537 };
538 let idle = |cpu: usize| -> (u64, u64) {
539 match (times(after, cpu), times(before, cpu)) {
540 (Some((idle_a, all_a)), Some((idle_b, all_b))) => {
541 (idle_a.saturating_sub(idle_b), all_a.saturating_sub(all_b))
542 }
543 _ => (0, 0),
544 }
545 };
546 allowed
547 .iter()
548 .copied()
549 .find(|cpu| {
550 let (idle, all) = idle(*cpu);
551 all > 0 && idle as f64 >= IDLE_ENOUGH * all as f64
552 })
553 .or_else(|| {
554 allowed
555 .iter()
556 .copied()
557 .max_by_key(|cpu| (idle(*cpu).0, std::cmp::Reverse(*cpu)))
558 })
559}
560
561fn allowed_cpus() -> Result<Vec<usize>, ProbeRefusal> {
563 unsafe {
565 let mut set: libc::cpu_set_t = std::mem::zeroed();
566 if libc::sched_getaffinity(0, std::mem::size_of_val(&set), &mut set) != 0 {
567 return Err(format!(
568 "cannot read the probe's CPU affinity: {}",
569 std::io::Error::last_os_error()
570 ));
571 }
572 Ok((0..libc::CPU_SETSIZE as usize)
573 .filter(|cpu| libc::CPU_ISSET(*cpu, &set))
574 .collect())
575 }
576}
577
578fn current_cpu() -> Result<usize, ProbeRefusal> {
579 let cpu = unsafe { libc::sched_getcpu() };
581 usize::try_from(cpu).map_err(|_| "sched_getcpu failed".to_owned())
582}
583
584fn await_peer_start(
588 tid: &AtomicU64,
589 handle: std::thread::JoinHandle<Result<u32, ProbeRefusal>>,
590) -> Result<std::thread::JoinHandle<Result<u32, ProbeRefusal>>, ProbeRefusal> {
591 while tid.load(Ordering::SeqCst) == 0 && !handle.is_finished() {
592 std::thread::yield_now();
593 }
594 if tid.load(Ordering::SeqCst) != 0 {
595 return Ok(handle);
596 }
597 let ended = handle
598 .join()
599 .map_err(|_| "the peer thread panicked".to_owned())?;
600 Err(match ended {
601 Err(why) => format!("the peer thread did not start: {why}"),
602 Ok(status) => format!("the peer thread ended at once: {status}"),
603 })
604}
605
606fn close_peer_reply(fd: &AtomicI32) {
608 let fd = fd.swap(-1, Ordering::SeqCst);
609 if fd >= 0 {
610 unsafe { libc::close(fd) };
612 }
613}
614
615fn minimum(samples: usize, f: impl Fn() -> u64) -> u64 {
616 (0..samples).map(|_| f()).min().unwrap_or(u64::MAX)
617}
618
619struct Probe {
620 cpu: usize,
621 layout: Layout,
622 counter: IrqCounter,
623 save: [Box<Area>; 3],
624 first: Box<Area>,
625 image: Box<Area>,
626 peer_image: Box<Area>,
627 ones: Box<Area>,
628 to_peer: [i32; 2],
629 from_peer: [i32; 2],
630 peer_reply_fd: Arc<AtomicI32>,
634 pkru: bool,
636 peer: Box<PeerCtx>,
637 tgid: i32,
638 tid: i32,
639 seq: u8,
640 forced_exit: bool,
641 reload_bound: u64,
642 report: ProbeReport,
643}
644
645impl Probe {
646 fn run(options: ProbeOptions) -> Result<ProbeReport, ProbeRefusal> {
647 let cpu = probe_cpu()?;
648 pin_to(cpu)?;
649 let layout = Layout::host()?;
650 let pkru = pkru_usable(layout.xcr0, native_ospke())?;
651 let cases = case_list(layout.xcr0)?;
652 if cases.len() != expected_case_count(layout.xcr0) {
653 return Err(format!(
654 "the probe built {} cases for XCR0 {:#x}, expected {}",
655 cases.len(),
656 layout.xcr0,
657 expected_case_count(layout.xcr0)
658 ));
659 }
660 let (counter, calibration) = open_calibrated_counter(cpu, 0)?;
661 let hypervisor = std::arch::x86_64::__cpuid(1).ecx & (1 << 31) != 0;
662 let forced_exit = options.forced_exit.unwrap_or(hypervisor);
663
664 let mut save = [Area::boxed(), Area::boxed(), Area::boxed()];
665 let syscall_min = minimum(1001, || unsafe { asm::hermit_xinuse_syscall_cycles() });
668 let area = save[0].0.as_mut_ptr();
669 let save_restore_min = minimum(1001, || unsafe {
670 asm::hermit_xinuse_save_restore_cycles(area, layout.xcr0)
671 });
672 let reload_bound = syscall_min + save_restore_min;
673 let cpuid_min = minimum(1001, || unsafe { asm::hermit_xinuse_cpuid_cycles() });
674 if forced_exit && cpuid_min < reload_bound {
675 return Err(format!(
676 "v3 is not qualified under this hypervisor: CPUID does not exit \
677 (fastest of 1001 took {cpuid_min} cycles, under the {reload_bound}-cycle \
678 cost of a kernel round trip plus a full save and restore)"
679 ));
680 }
681
682 let mut ones = Area::boxed();
683 for (i, b) in ones.0.iter_mut().enumerate().take(64) {
684 *b = (i as u8) | 0x81;
685 }
686 let mut to_peer = [0i32; 2];
687 let mut from_peer = [0i32; 2];
688 if unsafe { libc::pipe2(to_peer.as_mut_ptr(), libc::O_CLOEXEC) } != 0
690 || unsafe { libc::pipe2(from_peer.as_mut_ptr(), libc::O_CLOEXEC) } != 0
691 {
692 return Err(format!(
693 "cannot create the handshake pipes: {}",
694 std::io::Error::last_os_error()
695 ));
696 }
697 let (tgid, tid) = unsafe { (libc::getpid(), libc::gettid()) };
699 PROBE_TID.store(tid as u64, Ordering::SeqCst);
700 install_signal_counter()?;
701 let peer_image = Area::boxed();
702 let peer = Box::new(PeerCtx {
703 state: PeerState::DirtyPkruDiffers as u32,
704 fd_in: to_peer[0],
705 fd_out: from_peer[1],
706 avx512: (layout.xcr0 & AVX512_COMPONENTS != 0) as u32,
707 image: peer_image.0.as_ptr(),
708 xcr0: layout.xcr0,
709 ones: ones.0.as_ptr(),
710 tgid,
711 tid: 0,
712 expected_cpu: cpu as u32,
713 wrong_cpu: 0,
714 status: 0,
715 rounds: 0,
716 byte: 0,
717 reply: 0,
718 pkru: u8::from(pkru),
719 _pad: 0,
720 cpu_out: 0,
721 saved_pkru: 0,
722 saved_mxcsr: 0,
723 });
724 let mut probe = Probe {
725 cpu,
726 report: ProbeReport {
727 cpu,
728 xcr0: layout.xcr0,
729 cases: cases.len(),
730 cells: 0,
731 repetitions: options.repetitions,
732 certified_pairs: 0,
733 forced_exit,
734 reload_bound,
735 cpuid_min_cycles: cpuid_min,
736 calibration,
737 retried_references: 0,
738 retried_reloads: 0,
739 retried_exits: 0,
740 counter_reopens: 0,
741 },
742 layout,
743 counter,
744 save,
745 first: Area::boxed(),
746 image: Area::boxed(),
747 peer_image,
748 ones,
749 to_peer,
750 from_peer,
751 peer_reply_fd: Arc::new(AtomicI32::new(from_peer[1])),
752 pkru,
753 peer,
754 tgid,
755 tid,
756 seq: 0,
757 forced_exit,
758 reload_bound,
759 };
760 tracing::info!(
761 "xinuse probe: calibrated on CPU {cpu} ({}); reload bound {reload_bound} cycles",
762 probe.report.calibration
763 );
764 let started = std::time::Instant::now();
765 let result = probe.with_peer(|probe| probe.matrix(&cases, options.repetitions));
766 tracing::info!(
767 "xinuse probe: matrix took {:?}: {} cells, retried {} reference, {} reload and {} exit trials",
768 started.elapsed(),
769 probe.report.cells,
770 probe.report.retried_references,
771 probe.report.retried_reloads,
772 probe.report.retried_exits
773 );
774 close_peer_reply(&probe.peer_reply_fd);
775 for fd in to_peer.into_iter().chain([from_peer[0]]) {
776 unsafe { libc::close(fd) };
778 }
779 result.map(|()| probe.report)
780 }
781
782 fn with_peer(
784 &mut self,
785 f: impl FnOnce(&mut Self) -> Result<(), ProbeRefusal>,
786 ) -> Result<(), ProbeRefusal> {
787 let cpu = self.cpu;
788 let peer = &mut *self.peer as *mut PeerCtx as usize;
789 let tid_slot = std::sync::Arc::new(AtomicU64::new(0));
790 let tid_out = tid_slot.clone();
791 let reply_fd = self.peer_reply_fd.clone();
792 let handle = std::thread::Builder::new()
793 .name("xinuse-peer".into())
794 .spawn(move || -> Result<u32, ProbeRefusal> {
795 pin_to(cpu)?;
796 let tid = unsafe { libc::gettid() };
798 let peer = peer as *mut PeerCtx;
799 unsafe { (*peer).tid = tid };
803 tid_out.store(tid as u64, Ordering::SeqCst);
804 let status = unsafe { asm::hermit_xinuse_peer_loop(peer) };
806 if ArmStatus::from_raw(status) != Some(ArmStatus::Ok) {
807 close_peer_reply(&reply_fd);
810 }
811 Ok(status)
812 })
813 .map_err(|e| format!("cannot start the peer thread: {e}"))?;
814 let handle = await_peer_start(&tid_slot, handle)?;
815 let result = f(self);
816 let exit = [asm::PEER_EXIT];
817 let wrote = unsafe { libc::write(self.to_peer[1], exit.as_ptr().cast(), 1) };
819 let joined = handle
820 .join()
821 .map_err(|_| "the peer thread panicked".to_owned())?;
822 let status = joined?;
823 result?;
824 if wrote != 1 {
825 return Err("could not stop the peer thread".into());
826 }
827 match ArmStatus::from_raw(status) {
828 Some(ArmStatus::Ok) => {}
829 other => return Err(format!("the peer thread failed: {other:?}")),
830 }
831 if self.peer.wrong_cpu != 0 {
832 return Err(format!(
833 "the peer ran off CPU {cpu} in {} of {} rounds",
834 self.peer.wrong_cpu, self.peer.rounds
835 ));
836 }
837 Ok(())
838 }
839
840 fn matrix(&mut self, cases: &[Case], repetitions: u32) -> Result<(), ProbeRefusal> {
841 let peer_states = [
842 PeerState::DirtyPkruDiffers,
843 PeerState::DirtyWrpkruZero,
844 PeerState::XrstorInitAll,
845 PeerState::DirtyPkruHeaderLoaded,
846 PeerState::DirtyPkruSigreturn,
847 PeerState::InitValuesAllHeaderBits,
848 ];
849 let pkru = self.layout.xcr0 & (1 << PKRU_COMPONENT) != 0;
850 for case in cases {
851 for peer in peer_states {
852 if !pkru
853 && matches!(
854 peer,
855 PeerState::DirtyWrpkruZero
856 | PeerState::DirtyPkruHeaderLoaded
857 | PeerState::DirtyPkruSigreturn
858 )
859 {
860 continue;
861 }
862 for mxcsr in MXCSR_VALUES {
863 self.cell(case, peer, mxcsr, repetitions)?;
864 self.report.cells += 1;
865 }
866 }
867 }
868 let expected = self.report.cells as u64 * u64::from(repetitions);
869 if self.report.certified_pairs != expected {
870 return Err(format!(
871 "the probe certified {} pairs, expected {expected}",
872 self.report.certified_pairs
873 ));
874 }
875 Ok(())
876 }
877
878 fn cell(
879 &mut self,
880 case: &Case,
881 peer: PeerState,
882 mxcsr: u32,
883 repetitions: u32,
884 ) -> Result<(), ProbeRefusal> {
885 let where_ = || format!("case {}, peer {peer:?}, MXCSR {mxcsr:#x}", case.name());
886 let size = self.layout.size;
887 for repetition in 0..repetitions {
888 self.trial(case, Mode::Reference, peer, mxcsr, 0)
889 .map_err(|e| format!("{e} ({})", where_()))?;
890 self.check_reached(case, &where_)?;
891 self.trial(case, Mode::Reload, peer, mxcsr, 1)
892 .map_err(|e| format!("{e} ({})", where_()))?;
893 self.compare(0, 1, "a host FPU reload", &where_)?;
894 if self.forced_exit {
895 self.trial(case, Mode::ForcedExit, peer, mxcsr, 2)
896 .map_err(|e| format!("{e} ({})", where_()))?;
897 self.compare(0, 2, "a hypervisor exit", &where_)?;
898 }
899 if repetition == 0 {
900 self.first.0[..size + 4].copy_from_slice(&self.save[0].0[..size + 4]);
901 } else if self.first.0[..size + 4] != self.save[0].0[..size + 4] {
902 return Err(format!(
903 "XINUSE is not stable across repetitions: the reference image changed \
904 at repetition {repetition} ({})",
905 where_()
906 ));
907 }
908 self.report.certified_pairs += 1;
909 }
910 Ok(())
911 }
912
913 fn trial(
915 &mut self,
916 case: &Case,
917 mode: Mode,
918 peer: PeerState,
919 mxcsr: u32,
920 slot: usize,
921 ) -> Result<(), ProbeRefusal> {
922 for _attempt in 0..MAX_ATTEMPTS {
923 self.prepare_image(case, mxcsr);
924 self.prepare_peer(peer);
925 clear(&mut self.save[slot]);
926 self.seq = (self.seq + 1) % asm::PEER_EXIT;
927 let mut ctx = ArmCtx {
928 kind: case.kind as u32,
929 component: case.component,
930 mxcsr,
931 mode: mode as u32,
932 xcr0: self.layout.xcr0,
933 save_area: self.save[slot].0.as_mut_ptr(),
934 image: self.image.0.as_ptr(),
935 fd_to_peer: self.to_peer[1],
936 fd_from_peer: self.from_peer[0],
937 tgid: self.tgid,
938 tid: self.tid,
939 ones: self.ones.0.as_ptr(),
940 pkru_out: 0,
941 avx512: (self.layout.xcr0 & AVX512_COMPONENTS != 0) as u32,
942 cpuid_cycles: 0,
943 seq: self.seq,
944 reply: 0,
945 pkru: u8::from(self.pkru),
946 _pad: 0,
947 status: 0,
948 saved_pkru: 0,
949 saved_mxcsr: 0,
950 };
951 let probe_signals = PROBE_SIGNALS.load(Ordering::SeqCst);
952 let peer_signals = PEER_SIGNALS.load(Ordering::SeqCst);
953 let timeslices = match mode {
954 Mode::Reload => cpu_timeslices(self.cpu)?,
955 _ => 0,
956 };
957 let switches = switches_and_faults()?;
958 let irqs = match self.counter.read() {
959 Ok(irqs) => irqs,
960 Err(e) => {
961 self.reopen_counter(e)?;
962 continue;
963 }
964 };
965 let status = unsafe { asm::hermit_xinuse_arm(&mut ctx) };
967 let irqs_after = match self.counter.read() {
968 Ok(irqs) => irqs,
969 Err(e) => {
970 self.reopen_counter(e)?;
971 continue;
972 }
973 };
974 let switches_after = switches_and_faults()?;
975 let timeslices_after = match mode {
976 Mode::Reload => cpu_timeslices(self.cpu)?,
977 _ => 0,
978 };
979 if current_cpu()? != self.cpu {
980 return Err(format!("the probe thread ran off CPU {}", self.cpu));
981 }
982 match ArmStatus::from_raw(status) {
983 Some(ArmStatus::Ok) => {}
984 other => return Err(format!("a probe arm failed: {other:?}")),
985 }
986 if case.kind == Kind::Sigreturn
989 && PROBE_SIGNALS.load(Ordering::SeqCst) != probe_signals + 1
990 {
991 return Err("the sigreturn case's signal was not delivered".into());
992 }
993 if mode == Mode::Reload
994 && peer == PeerState::DirtyPkruSigreturn
995 && PEER_SIGNALS.load(Ordering::SeqCst) != peer_signals + 1
996 {
997 return Err("the peer's sigreturn signal was not delivered".into());
998 }
999 self.save[slot].0[self.layout.size..self.layout.size + 4]
1000 .copy_from_slice(&ctx.pkru_out.to_le_bytes());
1001 let certified = match mode {
1002 Mode::Reference => switches_after == switches && irqs_after == irqs,
1003 Mode::Reload => timeslices_after - timeslices == 2,
1006 Mode::ForcedExit => {
1007 switches_after == switches
1008 && irqs_after == irqs
1009 && ctx.cpuid_cycles >= self.reload_bound
1010 }
1011 };
1012 if certified {
1013 return Ok(());
1014 }
1015 match mode {
1016 Mode::Reference => self.report.retried_references += 1,
1017 Mode::Reload => self.report.retried_reloads += 1,
1018 Mode::ForcedExit => self.report.retried_exits += 1,
1019 }
1020 }
1021 Err(format!(
1022 "no {mode:?} trial was certified in {MAX_ATTEMPTS} attempts; the result is inconclusive"
1023 ))
1024 }
1025
1026 fn reopen_counter(&mut self, why: irq_counter::CounterRefusal) -> Result<(), ProbeRefusal> {
1031 self.report.counter_reopens += 1;
1032 if self.report.counter_reopens > MAX_COUNTER_REOPENS {
1033 return Err(format!(
1034 "{why}, and it stayed unreliable after {MAX_COUNTER_REOPENS} reopenings"
1035 ));
1036 }
1037 let (counter, calibration) =
1038 open_calibrated_counter(self.cpu, self.report.counter_reopens)?;
1039 self.counter = counter;
1040 self.report.calibration = calibration;
1041 Ok(())
1042 }
1043
1044 fn prepare_image(&mut self, case: &Case, mxcsr: u32) {
1045 match case.kind {
1046 Kind::XrstorInit => fill_standard_image(&mut self.image, mxcsr, 0),
1047 Kind::XrstorHeaderSet => {
1048 fill_standard_image(&mut self.image, mxcsr, 1 << case.component)
1049 }
1050 Kind::CompactedXrstorInit => {
1051 fill_compacted_image(&mut self.image, mxcsr, self.layout.xcr0)
1052 }
1053 Kind::FxrstorInit => fill_fx_image(&mut self.image, mxcsr),
1054 _ => {}
1055 }
1056 }
1057
1058 fn prepare_peer(&mut self, peer: PeerState) {
1059 let header = match peer {
1060 PeerState::DirtyPkruHeaderLoaded => 1 << PKRU_COMPONENT,
1061 PeerState::InitValuesAllHeaderBits => self.layout.xcr0,
1062 _ => 0,
1063 };
1064 fill_standard_image(&mut self.peer_image, 0x1f80, header);
1065 self.peer.state = peer as u32;
1068 }
1069
1070 fn check_reached(&self, case: &Case, where_: &dyn Fn() -> String) -> Result<(), ProbeRefusal> {
1072 let image = &self.save[0].0[..self.layout.size];
1073 let pkru = u32::from_le_bytes(
1074 self.save[0].0[self.layout.size..self.layout.size + 4]
1075 .try_into()
1076 .unwrap(),
1077 );
1078 for component in 0..64 {
1079 let bit = 1u64 << component;
1080 if case.targets & bit != 0 && !self.layout.is_initial(image, pkru, component) {
1081 return Err(format!(
1082 "the probe case did not bring component {component} to its initial state ({})",
1083 where_()
1084 ));
1085 }
1086 if case.live & bit != 0 && self.layout.is_initial(image, pkru, component) {
1087 return Err(format!(
1088 "the probe case left component {component} initial where it should be live ({})",
1089 where_()
1090 ));
1091 }
1092 }
1093 Ok(())
1094 }
1095
1096 fn compare(
1097 &self,
1098 reference: usize,
1099 other: usize,
1100 transition: &str,
1101 where_: &dyn Fn() -> String,
1102 ) -> Result<(), ProbeRefusal> {
1103 let n = self.layout.size + 4;
1104 if self.save[reference].0[..n] == self.save[other].0[..n] {
1105 return Ok(());
1106 }
1107 let header =
1108 |slot: usize| u64::from_le_bytes(self.save[slot].0[512..520].try_into().unwrap());
1109 let offset = (0..n)
1110 .find(|i| self.save[reference].0[*i] != self.save[other].0[*i])
1111 .unwrap_or(0);
1112 Err(format!(
1113 "XINUSE is not stable across {transition}: XSTATE_BV {:#x} against {:#x}, \
1114 first difference at save-area byte {offset} ({})",
1115 header(reference),
1116 header(other),
1117 where_()
1118 ))
1119 }
1120}
1121
1122fn install_signal_counter() -> Result<(), ProbeRefusal> {
1123 unsafe {
1125 let mut action: libc::sigaction = std::mem::zeroed();
1126 action.sa_sigaction = count_signal as *const () as usize;
1127 libc::sigemptyset(&mut action.sa_mask);
1128 if libc::sigaction(SIGUSR1, &action, std::ptr::null_mut()) != 0 {
1129 return Err(format!(
1130 "cannot install the probe's SIGUSR1 handler: {}",
1131 std::io::Error::last_os_error()
1132 ));
1133 }
1134 let mut set: libc::sigset_t = std::mem::zeroed();
1135 libc::sigemptyset(&mut set);
1136 libc::sigaddset(&mut set, SIGUSR1);
1137 if libc::pthread_sigmask(libc::SIG_UNBLOCK, &set, std::ptr::null_mut()) != 0 {
1138 return Err("cannot unblock SIGUSR1 for the probe".into());
1139 }
1140 }
1141 Ok(())
1142}
1143
1144const MAX_COUNTER_REOPENS: u32 = 5;
1146
1147fn open_calibrated_counter(cpu: usize, reopens: u32) -> Result<(IrqCounter, String), ProbeRefusal> {
1150 let mut reopens = reopens;
1151 loop {
1152 let counter = IrqCounter::open(CpuModel::host()).map_err(|e| e.to_string())?;
1153 match calibrate(&counter, cpu) {
1154 Ok(calibration) => return Ok((counter, calibration)),
1155 Err(CalibrationFailure::CounterLost(why)) if reopens < MAX_COUNTER_REOPENS => {
1156 reopens += 1;
1157 tracing::debug!("xinuse probe: reopening the interrupt counter: {why}");
1158 }
1159 Err(CalibrationFailure::CounterLost(why)) => {
1160 return Err(format!(
1161 "{why}, and it stayed unreliable after {MAX_COUNTER_REOPENS} reopenings"
1162 ));
1163 }
1164 Err(CalibrationFailure::Refused(why)) => return Err(why),
1165 }
1166 }
1167}
1168
1169enum CalibrationFailure {
1170 CounterLost(irq_counter::CounterRefusal),
1172 Refused(ProbeRefusal),
1173}
1174
1175fn calibrate(counter: &IrqCounter, cpu: usize) -> Result<String, CalibrationFailure> {
1191 let refused = |e: irq_counter::CounterRefusal| CalibrationFailure::Refused(e.to_string());
1192 let lost = CalibrationFailure::CounterLost;
1193 let mut spin = CALIBRATION_SPIN;
1194 let mut lost_spins = Switches::default();
1195 let (mut switched, mut slow_reads, mut clean) = (0, 0, 0);
1196 let mut sum = CalibrationSum::default();
1197 for _attempt in 0..CALIBRATION_ATTEMPTS {
1198 std::thread::sleep(CALIBRATION_PAUSE);
1199 let tsc = || unsafe { std::arch::x86_64::_rdtsc() };
1201 let measured_from = tsc();
1202 let before = irq_counter::snapshot_proc_interrupts().map_err(refused)?;
1203 let switches = Switches::now().map_err(CalibrationFailure::Refused)?;
1204 let start_count = counter.read().map_err(lost)?;
1205 let start = tsc();
1206 let spin_start = std::time::Instant::now();
1207 while spin_start.elapsed() < spin {
1208 std::hint::spin_loop();
1209 }
1210 let end_count = counter.read().map_err(lost)?;
1211 let end = tsc();
1212 let switches_after = Switches::now().map_err(CalibrationFailure::Refused)?;
1213 let after = irq_counter::snapshot_proc_interrupts().map_err(refused)?;
1214 let measured_to = tsc();
1215 let outside = 1.0 - (end - start) as f64 / (measured_to - measured_from) as f64;
1216 if switches_after != switches {
1219 switched += 1;
1220 lost_spins.add(&switches_after.since(&switches));
1221 continue;
1222 }
1223 if outside > 0.3 {
1224 slow_reads += 1;
1225 continue;
1226 }
1227 let before = irq_counter::interrupts_of(&before, cpu).map_err(refused)?;
1228 let after = irq_counter::interrupts_of(&after, cpu).map_err(refused)?;
1229 let kernel = irq_counter::CpuInterrupts {
1230 local_timer: after.local_timer - before.local_timer,
1231 total: after.total - before.total,
1232 };
1233 clean += 1;
1234 sum.add(end_count - start_count, kernel, outside);
1235 if kernel.local_timer < SHORT_SPIN_TICKS {
1236 spin = (spin * 2).min(CALIBRATION_SPIN_MAX);
1237 }
1238 if sum.kernel.local_timer < irq_counter::MIN_CALIBRATION_TICKS {
1239 continue;
1240 }
1241 let outside = sum.outside();
1242 irq_counter::calibration_verdict(sum.counter, sum.kernel, outside).map_err(|why| {
1243 CalibrationFailure::Refused(irq_counter::CounterRefusal::Calibration(why).to_string())
1244 })?;
1245 return Ok(format!(
1246 "counter {}, local timer {}, CPU total {} over {clean} clean spins \
1247 ({:.1}% of the interval outside the counter)",
1248 sum.counter,
1249 sum.kernel.local_timer,
1250 sum.kernel.total,
1251 outside * 100.0
1252 ));
1253 }
1254 Err(CalibrationFailure::Refused(format!(
1255 "the interrupt-counter calibration on CPU {cpu} did not finish in \
1256 {CALIBRATION_ATTEMPTS} spins: {clean} clean spins saw {} local-timer interrupts \
1257 (need {}), {switched} were switched out or faulted ({} involuntary and {} \
1258 voluntary context switches and {} page faults in all), and {slow_reads} spent \
1259 over 30% of their interval reading /proc/interrupts",
1260 sum.kernel.local_timer,
1261 irq_counter::MIN_CALIBRATION_TICKS,
1262 lost_spins.involuntary,
1263 lost_spins.voluntary,
1264 lost_spins.faults
1265 )))
1266}
1267
1268#[derive(Clone, Copy, Debug, Default)]
1270struct CalibrationSum {
1271 counter: u64,
1272 kernel: irq_counter::CpuInterrupts,
1273 outside_interrupts: f64,
1276}
1277
1278impl CalibrationSum {
1279 fn add(&mut self, counter: u64, kernel: irq_counter::CpuInterrupts, outside: f64) {
1280 self.counter += counter;
1281 self.kernel.local_timer += kernel.local_timer;
1282 self.kernel.total += kernel.total;
1283 self.outside_interrupts += kernel.total as f64 * outside;
1284 }
1285
1286 fn outside(&self) -> f64 {
1289 if self.kernel.total == 0 {
1290 0.0
1291 } else {
1292 self.outside_interrupts / self.kernel.total as f64
1293 }
1294 }
1295}
1296
1297const CALIBRATION_SPIN: std::time::Duration = std::time::Duration::from_millis(70);
1300const CALIBRATION_SPIN_MAX: std::time::Duration = std::time::Duration::from_millis(560);
1303const SHORT_SPIN_TICKS: u64 = 30;
1306const CALIBRATION_ATTEMPTS: usize = 100;
1309const CALIBRATION_PAUSE: std::time::Duration = std::time::Duration::from_millis(2);
1311
1312#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1314struct Switches {
1315 voluntary: i64,
1316 involuntary: i64,
1317 faults: i64,
1318}
1319
1320impl Switches {
1321 fn now() -> Result<Self, ProbeRefusal> {
1322 let mut usage = std::mem::MaybeUninit::<libc::rusage>::zeroed();
1323 if unsafe { libc::getrusage(libc::RUSAGE_THREAD, usage.as_mut_ptr()) } != 0 {
1325 return Err(format!(
1326 "getrusage failed: {}",
1327 std::io::Error::last_os_error()
1328 ));
1329 }
1330 let u = unsafe { usage.assume_init() };
1332 Ok(Self {
1333 voluntary: u.ru_nvcsw,
1334 involuntary: u.ru_nivcsw,
1335 faults: u.ru_minflt + u.ru_majflt,
1336 })
1337 }
1338
1339 fn since(&self, earlier: &Self) -> Self {
1340 Self {
1341 voluntary: self.voluntary - earlier.voluntary,
1342 involuntary: self.involuntary - earlier.involuntary,
1343 faults: self.faults - earlier.faults,
1344 }
1345 }
1346
1347 fn add(&mut self, other: &Self) {
1348 self.voluntary += other.voluntary;
1349 self.involuntary += other.involuntary;
1350 self.faults += other.faults;
1351 }
1352}
1353
1354#[cfg(test)]
1355mod tests {
1356 use super::*;
1357
1358 #[test]
1362 fn pkru_instructions_need_the_component_and_ospke() {
1363 assert_eq!(pkru_usable(0x2e7, true), Ok(true));
1364 assert_eq!(pkru_usable(0xe7, true), Ok(false));
1365 assert_eq!(pkru_usable(0xe7, false), Ok(false));
1366 assert_eq!(pkru_usable(0x7, false), Ok(false));
1367 assert!(pkru_usable(0x2e7, false).unwrap_err().contains("OSPKE"));
1368 assert!(pkru_usable(0x207, false).is_err());
1369 }
1370
1371 #[test]
1374 fn calibration_spins_are_summed_with_interrupt_weighted_outside_time() {
1375 let mut sum = CalibrationSum::default();
1376 assert_eq!(sum.outside(), 0.0);
1377 let kernel = |local_timer, total| irq_counter::CpuInterrupts { local_timer, total };
1378 sum.add(60, kernel(64, 70), 0.1);
1379 sum.add(130, kernel(132, 140), 0.0);
1380 assert_eq!(sum.counter, 190);
1381 assert_eq!(sum.kernel.local_timer, 196);
1382 assert_eq!(sum.kernel.total, 210);
1383 assert!(
1386 (sum.outside() - 7.0 / 210.0).abs() < 1e-12,
1387 "{}",
1388 sum.outside()
1389 );
1390 }
1391
1392 unsafe fn rdpkru() -> u32 {
1394 let pkru: u32;
1395 unsafe {
1398 std::arch::asm!("rdpkru", in("ecx") 0, out("eax") pkru, out("edx") _,
1399 options(nomem, nostack, preserves_flags));
1400 }
1401 pkru
1402 }
1403
1404 unsafe fn wrpkru(pkru: u32) {
1407 unsafe {
1409 std::arch::asm!("wrpkru", in("eax") pkru, in("ecx") 0, in("edx") 0,
1410 options(nostack, preserves_flags));
1411 }
1412 }
1413
1414 #[test]
1422 fn an_arm_runs_with_and_without_pkru_instructions() {
1423 if std::arch::x86_64::__cpuid(1).ecx & (1 << 27) == 0 {
1424 return; }
1426 let xcr0 = unsafe { std::arch::x86_64::_xgetbv(0) };
1428 let ospke = native_ospke();
1429 for pkru in [false, true] {
1430 if pkru && !ospke {
1431 continue;
1432 }
1433 let mut save = Area::boxed();
1434 let image = Area::boxed();
1435 let mut ones = Area::boxed();
1436 ones.0[..64].fill(0xa5);
1437 let mut ctx = ArmCtx {
1438 kind: Kind::Vzeroupper as u32,
1439 component: 0,
1440 mxcsr: 0x1f80,
1441 mode: Mode::Reference as u32,
1442 xcr0,
1443 save_area: save.0.as_mut_ptr(),
1444 image: image.0.as_ptr(),
1445 fd_to_peer: -1,
1446 fd_from_peer: -1,
1447 tgid: 0,
1448 tid: 0,
1449 ones: ones.0.as_ptr(),
1450 pkru_out: 0xdead,
1451 avx512: (xcr0 & AVX512_COMPONENTS != 0) as u32,
1452 cpuid_cycles: 0,
1453 seq: 0,
1454 reply: 0,
1455 pkru: u8::from(pkru),
1456 _pad: 0,
1457 status: 0,
1458 saved_pkru: 0,
1459 saved_mxcsr: 0,
1460 };
1461 const CALLER_PKRU: u32 = 0x5555_5550;
1464 let original = ospke.then(|| {
1465 let original = unsafe { rdpkru() };
1467 unsafe { wrpkru(CALLER_PKRU) };
1468 original
1469 });
1470 let status = unsafe { asm::hermit_xinuse_arm(&mut ctx) };
1473 if let Some(original) = original {
1474 let after = unsafe { rdpkru() };
1476 unsafe { wrpkru(original) };
1477 assert_eq!(after, CALLER_PKRU, "pkru {pkru}: the caller's PKRU changed");
1478 }
1479 assert_eq!(
1480 ArmStatus::from_raw(status),
1481 Some(ArmStatus::Ok),
1482 "pkru {pkru}"
1483 );
1484 assert_eq!(
1485 ctx.pkru_out,
1486 if pkru { 0x5555_5554 } else { 0 },
1487 "pkru {pkru}"
1488 );
1489 }
1490 }
1491
1492 #[test]
1498 fn the_peer_loop_runs_with_and_without_pkru_instructions() {
1499 if std::arch::x86_64::__cpuid(1).ecx & (1 << 27) == 0 {
1500 return; }
1502 let xcr0 = unsafe { std::arch::x86_64::_xgetbv(0) };
1504 let ospke = native_ospke();
1505 for pkru in [false, true] {
1506 if pkru && !ospke {
1507 continue;
1508 }
1509 for state in [PeerState::DirtyPkruDiffers, PeerState::DirtyWrpkruZero] {
1510 let (mut to_peer, mut from_peer) = ([0i32; 2], [0i32; 2]);
1511 unsafe {
1513 assert_eq!(libc::pipe2(to_peer.as_mut_ptr(), libc::O_CLOEXEC), 0);
1514 assert_eq!(libc::pipe2(from_peer.as_mut_ptr(), libc::O_CLOEXEC), 0);
1515 }
1516 let bytes = [1u8, asm::PEER_EXIT];
1518 assert_eq!(
1520 unsafe { libc::write(to_peer[1], bytes.as_ptr().cast(), 2) },
1521 2
1522 );
1523 let (fd_in, fd_out) = (to_peer[0], from_peer[1]);
1524 let (status, rounds, after) = std::thread::spawn(move || {
1527 const THREAD_PKRU: u32 = 0x5555_5540;
1530 let image = Area::boxed();
1531 let mut ones = Area::boxed();
1532 ones.0[..64].fill(0xa5);
1533 let mut ctx = PeerCtx {
1534 state: state as u32,
1535 fd_in,
1536 fd_out,
1537 avx512: (xcr0 & AVX512_COMPONENTS != 0) as u32,
1538 image: image.0.as_ptr(),
1539 xcr0,
1540 ones: ones.0.as_ptr(),
1541 tgid: 0,
1542 tid: 0,
1543 expected_cpu: 0,
1544 wrong_cpu: 0,
1545 status: 0,
1546 rounds: 0,
1547 byte: 0,
1548 reply: 0,
1549 pkru: u8::from(pkru),
1550 _pad: 0,
1551 cpu_out: 0,
1552 saved_pkru: 0,
1553 saved_mxcsr: 0,
1554 };
1555 let original = ospke.then(|| unsafe {
1557 let original = rdpkru();
1558 wrpkru(THREAD_PKRU);
1559 original
1560 });
1561 let status = unsafe { asm::hermit_xinuse_peer_loop(&mut ctx) };
1564 let after = original.map(|original| unsafe {
1566 let after = rdpkru();
1567 wrpkru(original);
1568 (after, THREAD_PKRU)
1569 });
1570 (status, ctx.rounds, after)
1571 })
1572 .join()
1573 .unwrap();
1574 let mut reply = [0u8; 1];
1575 let read = unsafe { libc::read(from_peer[0], reply.as_mut_ptr().cast(), 1) };
1577 unsafe {
1579 for fd in to_peer.into_iter().chain(from_peer) {
1580 libc::close(fd);
1581 }
1582 }
1583 assert_eq!(status, 0, "pkru {pkru} {state:?}");
1584 assert_eq!((rounds, read, reply[0]), (1, 1, 2), "pkru {pkru} {state:?}");
1585 if let Some((after, before)) = after {
1586 assert_eq!(
1587 after, before,
1588 "pkru {pkru} {state:?}: the thread's PKRU changed"
1589 );
1590 }
1591 }
1592 }
1593 }
1594
1595 #[test]
1598 fn a_peer_that_cannot_start_is_refused() {
1599 let tid = AtomicU64::new(0);
1600 let handle = std::thread::spawn(|| -> Result<u32, ProbeRefusal> {
1601 Err("cannot pin the probe to CPU 7: Invalid argument".into())
1602 });
1603 let refusal = await_peer_start(&tid, handle).unwrap_err();
1604 assert!(refusal.contains("did not start"), "{refusal}");
1605 assert!(refusal.contains("cannot pin"), "{refusal}");
1606 }
1607
1608 #[test]
1612 fn a_failed_peer_ends_a_pending_reply_read() {
1613 let mut fds = [0i32; 2];
1614 assert_eq!(unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) }, 0);
1616 let reply = Arc::new(AtomicI32::new(fds[1]));
1617 let peer = reply.clone();
1618 let closer = std::thread::spawn(move || {
1619 std::thread::sleep(std::time::Duration::from_millis(50));
1620 close_peer_reply(&peer);
1621 });
1622 let mut byte = [0u8; 1];
1623 let read = unsafe { libc::read(fds[0], byte.as_mut_ptr().cast(), 1) };
1625 closer.join().unwrap();
1626 assert_eq!(read, 0);
1627 close_peer_reply(&reply);
1628 assert_eq!(reply.load(Ordering::SeqCst), -1);
1629 unsafe { libc::close(fds[0]) };
1631 }
1632
1633 fn proc_stat(cpus: &[(usize, u64, u64)]) -> String {
1635 let mut text = String::from("cpu 0 0 0 0 0 0 0 0 0 0\n");
1636 for (cpu, busy, idle) in cpus {
1637 text.push_str(&format!("cpu{cpu} {busy} 0 0 {idle} 0 0 0 0 0 0\n"));
1638 }
1639 text
1640 }
1641
1642 #[test]
1643 fn the_probe_takes_the_lowest_idle_cpu_and_otherwise_the_idlest() {
1644 let before = proc_stat(&[(1, 0, 0), (2, 0, 0), (40, 0, 0), (41, 0, 0)]);
1645 let after = proc_stat(&[(1, 10, 0), (2, 9, 1), (40, 0, 10), (41, 0, 10)]);
1647 assert_eq!(choose_probe_cpu(&[1, 2, 40, 41], &before, &after), Some(40));
1648 let after = proc_stat(&[(1, 10, 0), (2, 5, 5), (40, 2, 8), (41, 2, 8)]);
1650 assert_eq!(choose_probe_cpu(&[1, 2, 40, 41], &before, &after), Some(40));
1651 assert_eq!(choose_probe_cpu(&[1, 2], &before, &after), Some(2));
1653 assert_eq!(choose_probe_cpu(&[], &before, &after), None);
1654 }
1655
1656 #[test]
1657 fn only_covered_component_sets_are_admitted() {
1658 assert_eq!(check_xcr0(0x2e7), Ok(()));
1659 assert_eq!(check_xcr0(0x207), Ok(()), "AVX2-only host with PKRU");
1660 assert_eq!(check_xcr0(0x7), Ok(()));
1661 assert!(check_xcr0(0x3).is_err(), "no AVX");
1662 assert!(check_xcr0(0x267).is_err(), "partial AVX-512");
1663 assert!(check_xcr0(0x802e7).is_err());
1665 assert!(check_xcr0(0x602e7).is_err());
1666 assert!(
1667 check_xcr0(0x2e7 | (1 << 62)).is_err(),
1668 "all 64 bits are checked"
1669 );
1670 }
1671
1672 #[test]
1673 fn the_case_list_matches_its_independent_count() {
1674 for xcr0 in [0x2e7u64, 0x207, 0xe7, 0x7] {
1675 let cases = case_list(xcr0).unwrap();
1676 assert_eq!(cases.len(), expected_case_count(xcr0), "xcr0 {xcr0:#x}");
1677 for case in &cases {
1678 assert_eq!(case.targets & !xcr0, 0, "{case:?}");
1679 assert_eq!(case.live & !xcr0, 0, "{case:?}");
1680 assert_eq!(case.targets & case.live, 0, "{case:?}");
1681 assert_ne!(case.targets, 0, "{case:?}");
1682 }
1683 }
1684 assert_eq!(case_list(0x2e7).unwrap().len(), 33);
1685 assert!(case_list(0x207).unwrap().iter().all(|c| !matches!(
1687 c.kind,
1688 Kind::Hi16Zero | Kind::PxorVzeroupperHi16 | Kind::OpmaskZero
1689 )));
1690 }
1691
1692 #[test]
1693 fn schedstat_timeslices_are_read_by_version() {
1694 let text = "version 17\ntimestamp 5\ncpu0 0 0 0 0 0 0 7 8 440964198\ncpu3 0 0 0 0 0 0 1 2 1072656639\n";
1695 assert_eq!(parse_schedstat_timeslices(text, 3), Ok(1_072_656_639));
1696 assert!(parse_schedstat_timeslices(text, 4).is_err());
1697 assert!(parse_schedstat_timeslices("version 18\ncpu0 0 0 0 0 0 0 1 2 3\n", 0).is_err());
1698 assert!(parse_schedstat_timeslices("cpu0 0 0 0 0 0 0 1 2 3\n", 0).is_err());
1699 }
1700
1701 #[test]
1702 fn initial_state_is_recognized_from_the_header_or_the_values() {
1703 let layout = Layout {
1704 xcr0: 0x207,
1705 size: 832,
1706 components: vec![(2, 576, 256)],
1707 };
1708 let mut image = vec![0u8; 832];
1709 assert!(layout.is_initial(&image, 0, 0), "header bit clear");
1710 image[512] = 0b111;
1711 image[0..2].copy_from_slice(&0x37fu16.to_le_bytes());
1712 assert!(layout.is_initial(&image, 0, 0), "initial x87 values");
1713 image[40] = 1;
1714 assert!(!layout.is_initial(&image, 0, 0), "a nonzero ST register");
1715 assert!(layout.is_initial(&image, 0, 2));
1716 image[600] = 1;
1717 assert!(!layout.is_initial(&image, 0, 2));
1718 image[513] = 0b10;
1719 assert!(layout.is_initial(&image, 0, 9));
1720 assert!(!layout.is_initial(&image, 4, 9));
1721 }
1722}