Skip to main content

hermit/xstate_probe/
mod.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! The startup qualification of `--cpu-profile x86-64-v3`
10//! (<https://github.com/rrnewton/hermit/issues/4243>).
11//!
12//! Under v3 a guest may execute XSAVE. The XSTATE_BV bits a plain XSAVE writes
13//! come from the processor's in-use tracking (XINUSE), which Hermit cannot trap
14//! on the ptrace backend. The host kernel saves and restores a task's registers
15//! at moments Hermit does not control. If such a save and restore changed a bit
16//! the guest can read, a guest's output would depend on host timing.
17//!
18//! This probe checks, for a fixed set of cases, that it does not. Each case
19//! dirties every register of every enabled component, brings some components to
20//! their initial state by one path, and then runs plain XSAVE three ways:
21//!
22//! - **reference:** directly, with a certificate that nothing reloaded the
23//!   registers in between (no context switch or fault, by `getrusage`; no
24//!   interrupt, by the per-thread PMU counter in [`irq_counter`]);
25//! - **reload:** after a handoff to a peer thread on the same CPU, so the kernel
26//!   saves this thread's state, runs the peer's, and restores ours, with exactly
27//!   two new timeslices on the CPU (the peer's, then ours);
28//! - **forced exit** (under a hypervisor): after CPUID, which exits to it.
29//!
30//! The arms must write identical save images (all of it, not only the header)
31//! and the same PKRU, and every repetition must agree. Anything inconclusive
32//! refuses. The probe samples a fixed set of paths; it cannot exclude a CPU or
33//! hypervisor that behaves differently only on others, and that limit is stated
34//! wherever v3 is offered.
35
36pub 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
53/// The fewest repetitions per cell (the design's retained minimum).
54pub const MIN_REPETITIONS: u32 = 32;
55/// The most repetitions accepted, checked before any narrowing.
56pub const MAX_REPETITIONS: u32 = 4096;
57/// Attempts per trial before its cell is inconclusive. A reload trial is
58/// retried whenever another task ran on the CPU between the peer and the probe,
59/// which on a busy machine (load 80 on 316 CPUs) happens often enough that 20
60/// attempts left a cell inconclusive in one of six runs.
61const MAX_ATTEMPTS: u32 = 200;
62/// The XCR0 components the probe covers: x87, SSE, AVX, opmask, ZMM_Hi256,
63/// Hi16_ZMM and PKRU.
64pub const COVERED_XCR0: u64 = 0b10_1110_0111;
65const AVX512_COMPONENTS: u64 = 0b1110_0000;
66const PKRU_COMPONENT: u32 = 9;
67const SIGUSR1: i32 = 10;
68/// The MXCSR values each case runs under: the default, and a legal
69/// non-default value (the SSE header bit is forced by the kernel's XSAVES when
70/// MXCSR is not the default).
71pub const MXCSR_VALUES: [u32; 2] = [0x1f80, 0x3f80];
72
73/// One probe case: its initializing step, the components it brings to their
74/// initial state, and the components it leaves live (both as XCR0 bit masks).
75#[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
94/// Why the host cannot use the v3 profile, as one sentence for the refusal.
95pub type ProbeRefusal = String;
96
97/// The admitted component sets: x87, SSE and AVX are required by v3; the three
98/// AVX-512 components come together or not at all; PKRU is optional. Any other
99/// bit refuses, over all 64 bits.
100pub 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
121/// Whether the probe may run RDPKRU and WRPKRU: exactly when XCR0 enables the
122/// PKRU component and the kernel enables protection keys (CPUID.7.0:ECX.OSPKE,
123/// from CR4.PKE); either instruction raises #UD without the latter. A host
124/// whose XCR0 has PKRU but whose kernel does not enable it is refused, since
125/// the PKRU cases could not run there.
126pub 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
137/// CPUID.7.0:ECX.OSPKE on this CPU.
138fn native_ospke() -> bool {
139    std::arch::x86_64::__cpuid_count(7, 0).ecx & (1 << 4) != 0
140}
141
142/// The closed case list for an admitted XCR0 (condition C6): every case whose
143/// instructions and components exist on it.
144pub 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    // VZEROUPPER zeroes bits 128 and up of registers 0-15: AVX and ZMM_Hi256.
182    push(Kind::Vzeroupper, 0b100_0100);
183    // A legacy PXOR leaves the upper halves alone.
184    push(Kind::LegacyPxor, 0b10);
185    // A VEX-128 write zeroes everything above bit 127 of registers 0-15.
186    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    // FXRSTOR loads x87 and XMM0-15, leaving the YMM upper halves alone.
195    push(Kind::FxrstorInit, 0b11);
196    push(Kind::VzeroallAll, 0b1100_0110);
197    Ok(cases)
198}
199
200/// The number of cases [`case_list`] must produce for `xcr0`, derived
201/// independently of it (gate 5).
202pub 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/// Where each component lives in a standard-format save area (CPUID 0xD).
209#[derive(Clone, Debug, PartialEq, Eq)]
210pub struct Layout {
211    pub xcr0: u64,
212    /// The save-area size for the enabled components (0xD.0 EBX).
213    pub size: usize,
214    /// (offset, size) of each component from 2 up, by component number.
215    pub components: Vec<(u32, usize, usize)>,
216}
217
218impl Layout {
219    pub fn host() -> Result<Self, ProbeRefusal> {
220        // SAFETY: XGETBV with ECX 0 is available whenever OSXSAVE is set, which
221        // `check_host_xsave` establishes before the probe runs.
222        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    /// Whether `component` is in its initial configuration in `image`: its
246    /// header bit is clear, or its saved values are the initial ones.
247    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
283/// Zeroes the part of an area any enabled-component image can use: the host's
284/// XSAVE size (at most [`USED_BYTES`]) plus RDPKRU's slot.
285fn clear(area: &mut Area) {
286    area.0[..USED_BYTES].fill(0);
287}
288
289/// The bytes of an area the probe uses; [`Layout::host`] refuses a save area
290/// that does not fit.
291const USED_BYTES: usize = 8192;
292
293/// A standard-format image of initial values with `header` as XSTATE_BV.
294fn 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()); // MXCSR_MASK
299    area.0[512..520].copy_from_slice(&header.to_le_bytes());
300}
301
302/// A compacted-format image whose header clears every XSTATE_BV bit.
303fn 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
310/// An FXSAVE image of initial x87 and SSE values.
311fn 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    // SAFETY: gettid has no preconditions.
323    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
331/// Context switches plus page faults of the calling thread.
332fn switches_and_faults() -> Result<i64, ProbeRefusal> {
333    let mut usage = std::mem::MaybeUninit::<libc::rusage>::zeroed();
334    // SAFETY: `usage` is writable and RUSAGE_THREAD names the calling thread.
335    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    // SAFETY: getrusage succeeded and initialized it.
342    let u = unsafe { usage.assume_init() };
343    Ok(u.ru_nvcsw + u.ru_nivcsw + u.ru_minflt + u.ru_majflt)
344}
345
346/// The timeslices that have started on `cpu`, from `/proc/schedstat`.
347///
348/// The file is about 200 KB on a 316-CPU machine and costs a millisecond to
349/// generate whole; reading stops at `cpu`'s line, so for the low-numbered CPU
350/// the probe pins itself to, the kernel generates only the first few lines.
351fn 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
369/// Parses a `/proc/schedstat` version 15-17 text for `cpu`'s timeslice count:
370/// the ninth field of its `cpuN` line, which `sched_info_switch` advances for
371/// each task that starts running there (the idle task excluded).
372pub 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/// What the probe found, bound into a run's manifest and printed on request.
398#[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    /// Trials repeated because a certificate failed.
411    pub retried_references: u64,
412    pub retried_reloads: u64,
413    pub retried_exits: u64,
414    /// Times the counter was reopened and recalibrated after another PMU user
415    /// kept it off the PMU.
416    pub counter_reopens: u32,
417}
418
419/// How the probe is run.
420#[derive(Clone, Copy, Debug)]
421pub struct ProbeOptions {
422    pub repetitions: u32,
423    /// `None`: on exactly when CPUID.1:ECX bit 31 (hypervisor) is set.
424    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
436/// Runs the qualification on a new thread pinned to an idle, low-numbered CPU.
437/// Returns the report, or the reason v3 cannot be offered on this host.
438pub 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    // Probes pin themselves to the same CPU, so two at once would switch each
446    // other out until both refused. One at a time per user: the others wait.
447    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
456/// An exclusive `flock` on a per-user file in the temporary directory, held
457/// for one probe.
458struct ProbeLock {
459    _file: std::fs::File,
460}
461
462impl ProbeLock {
463    fn acquire() -> Result<Self, ProbeRefusal> {
464        use std::os::fd::AsRawFd;
465        // SAFETY: getuid has no preconditions.
466        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        // SAFETY: the descriptor is open for the duration of the call.
475        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    // SAFETY: cpu_set_t is plain data; CPU_SET writes within it.
488    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
501/// The CPU the probe pins itself to: over 100 ms of /proc/stat, the
502/// lowest-numbered allowed CPU that was idle for at least [`IDLE_ENOUGH`] of the
503/// interval, or, when none was, the idlest allowed CPU. Idle, so that the
504/// calibration spins and the trials are not switched out by other work: with
505/// all of a loaded machine's low-numbered CPUs busy, every spin failed.
506/// Low-numbered among those, because each reload trial reads /proc/schedstat
507/// up to the chosen CPU's line, which costs about 80 us at CPU 15 and 1.3 ms at
508/// CPU 300.
509fn 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
521/// The fraction of the sampled interval a CPU must have been idle for
522/// [`probe_cpu`] to prefer it by number rather than by idleness.
523const IDLE_ENOUGH: f64 = 0.95;
524
525/// [`probe_cpu`]'s choice among `allowed`, from two /proc/stat texts.
526fn choose_probe_cpu(allowed: &[usize], before: &str, after: &str) -> Option<usize> {
527    // (idle and iowait, all fields) jiffies of one CPU line.
528    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
561/// The CPUs this thread may run on, in order.
562fn allowed_cpus() -> Result<Vec<usize>, ProbeRefusal> {
563    // SAFETY: cpu_set_t is plain data that sched_getaffinity fills.
564    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    // SAFETY: sched_getcpu has no preconditions.
580    let cpu = unsafe { libc::sched_getcpu() };
581    usize::try_from(cpu).map_err(|_| "sched_getcpu failed".to_owned())
582}
583
584/// Waits until the peer thread has published its tid. A peer that ended
585/// before that (it could not pin itself, say) is joined and refused, so that
586/// the probe never waits for replies from a thread that is gone.
587fn 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
606/// Closes the peer's reply descriptor, at most once.
607fn close_peer_reply(fd: &AtomicI32) {
608    let fd = fd.swap(-1, Ordering::SeqCst);
609    if fd >= 0 {
610        // SAFETY: the swap hands the descriptor to exactly one closer.
611        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    /// The write end of `from_peer`, which the peer closes when its loop fails,
631    /// so that a probe arm waiting for a reply reads end-of-file instead of
632    /// blocking; -1 once closed.
633    peer_reply_fd: Arc<AtomicI32>,
634    /// Whether RDPKRU and WRPKRU may run (see [`pkru_usable`]).
635    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        // A kernel round trip plus a full save and restore: the least a CPUID
666        // exit to a hypervisor and back can cost.
667        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        // SAFETY: each array has room for the two descriptors pipe2 writes.
689        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        // SAFETY: getpid and gettid have no preconditions.
698        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            // SAFETY: these descriptors were created above and are not used again.
777            unsafe { libc::close(fd) };
778        }
779        result.map(|()| probe.report)
780    }
781
782    /// Runs `f` with the peer thread serving handshakes, then stops it.
783    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                // SAFETY: gettid has no preconditions.
797                let tid = unsafe { libc::gettid() };
798                let peer = peer as *mut PeerCtx;
799                // SAFETY: the probe thread keeps the context alive until this
800                // thread is joined, and touches it only while this thread is
801                // blocked in its handshake read.
802                unsafe { (*peer).tid = tid };
803                tid_out.store(tid as u64, Ordering::SeqCst);
804                // SAFETY: as above.
805                let status = unsafe { asm::hermit_xinuse_peer_loop(peer) };
806                if ArmStatus::from_raw(status) != Some(ArmStatus::Ok) {
807                    // A probe arm may be waiting for this peer's reply: close
808                    // the only write end so that its read ends.
809                    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        // SAFETY: writes one byte from a live buffer to our pipe.
818        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    /// Runs one arm into save area `slot` until its certificate holds.
914    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            // SAFETY: the context and every buffer it points at outlive the call.
966            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            // A sigreturn trial must actually take its signal; a peer round
987            // that sends one must too.
988            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                // Exactly the peer's timeslice and then ours: no other task,
1004                // with its own PKRU, ran between them.
1005                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    /// Replaces a counter that failed a read (another PMU user kept it off the
1027    /// PMU, so its coverage is broken for good) with a newly opened and
1028    /// calibrated one. The trial in progress is discarded. Refuses after
1029    /// [`MAX_COUNTER_REOPENS`].
1030    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        // The peer is blocked in its handshake read, so it sees this before it
1066        // acts on the next byte.
1067        self.peer.state = peer as u32;
1068    }
1069
1070    /// Condition C7: the reference reached the case's intended state.
1071    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    // SAFETY: the handler only calls gettid and updates atomics.
1124    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
1144/// How many times a counter that lost PMU coverage may be replaced.
1145const MAX_COUNTER_REOPENS: u32 = 5;
1146
1147/// Opens the interrupt counter and calibrates it. A counter that fails a read
1148/// during calibration is replaced, up to [`MAX_COUNTER_REOPENS`] times in all.
1149fn 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    /// The counter lost PMU coverage; a new one may do better.
1171    CounterLost(irq_counter::CounterRefusal),
1172    Refused(ProbeRefusal),
1173}
1174
1175/// Condition C11's binding calibration: over spins with no context switch or
1176/// fault, summed, the counter sees at least this CPU's local-timer interrupts
1177/// and no more than all of them.
1178///
1179/// On a busy machine a long spin is often preempted, by per-CPU kernel work if
1180/// nothing else, and a preempted spin says nothing. So each spin is short,
1181/// [`CALIBRATION_SPIN`] at first, and starts after a short sleep, which lets
1182/// pending work on the CPU run first. The clean spins' counts are summed until
1183/// they hold [`irq_counter::MIN_CALIBRATION_TICKS`] local-timer interrupts, and
1184/// the verdict judges the sums, so its absolute slack is as tight a share as
1185/// one long spin's was. Up to [`CALIBRATION_ATTEMPTS`] spins are tried. A clean
1186/// spin that saw fewer than [`SHORT_SPIN_TICKS`] interrupts (a host with a
1187/// lower timer frequency) makes the next spins twice as long, up to
1188/// [`CALIBRATION_SPIN_MAX`]; a tickless CPU never reaches the floor and is
1189/// refused.
1190fn 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        // SAFETY: RDTSC is available on every x86_64 processor.
1200        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        // A spin that was switched out or faulted says nothing, and one whose
1217        // surrounding reads took over 30% of the interval binds loosely.
1218        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/// The clean calibration spins' counts, summed.
1269#[derive(Clone, Copy, Debug, Default)]
1270struct CalibrationSum {
1271    counter: u64,
1272    kernel: irq_counter::CpuInterrupts,
1273    /// The kernel's interrupts during the reads around each spin, estimated
1274    /// as that spin's total times its share of time outside the counter.
1275    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    /// The share of the summed interval the counter did not cover, weighted by
1287    /// each spin's interrupts, for [`irq_counter::calibration_verdict`].
1288    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
1297/// How long a calibration spin lasts at first: about 70 local-timer
1298/// interrupts at 1000 Hz.
1299const CALIBRATION_SPIN: std::time::Duration = std::time::Duration::from_millis(70);
1300/// The longest a calibration spin may grow to on a host with a lower timer
1301/// frequency: 56 interrupts at 100 Hz.
1302const CALIBRATION_SPIN_MAX: std::time::Duration = std::time::Duration::from_millis(560);
1303/// A clean spin with fewer local-timer interrupts than this doubles the next
1304/// spins' length.
1305const SHORT_SPIN_TICKS: u64 = 30;
1306/// How many calibration spins are tried. At 1000 Hz three clean spins reach the
1307/// floor.
1308const CALIBRATION_ATTEMPTS: usize = 100;
1309/// The sleep before each calibration spin.
1310const CALIBRATION_PAUSE: std::time::Duration = std::time::Duration::from_millis(2);
1311
1312/// This thread's context switches and page faults, from getrusage.
1313#[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        // SAFETY: `usage` is writable and RUSAGE_THREAD names the calling thread.
1324        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        // SAFETY: getrusage succeeded and initialized it.
1331        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    /// PKRU instructions run only with both the XCR0 component and OSPKE; the
1359    /// subsets without PKRU (C6) run with neither, and a host with the
1360    /// component but without OSPKE is refused.
1361    #[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    /// Clean spins sum their counts, and the outside share is weighted by each
1372    /// spin's interrupts, not averaged per spin.
1373    #[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        // 7 of 210 interrupts fell outside: 1/30, where a per-spin mean would
1384        // give 1/20.
1385        assert!(
1386            (sum.outside() - 7.0 / 210.0).abs() < 1e-12,
1387            "{}",
1388            sum.outside()
1389        );
1390    }
1391
1392    /// RDPKRU. Safety: CR4.PKE must be set (CPUID.7.0:ECX.OSPKE).
1393    unsafe fn rdpkru() -> u32 {
1394        let pkru: u32;
1395        // SAFETY: the caller checked OSPKE; RDPKRU reads PKRU into EAX and
1396        // zeroes EDX.
1397        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    /// WRPKRU. Safety: CR4.PKE must be set, and `pkru` must leave key 0
1405    /// accessible.
1406    unsafe fn wrpkru(pkru: u32) {
1407        // SAFETY: the caller checked OSPKE and the value.
1408        unsafe {
1409            std::arch::asm!("wrpkru", in("eax") pkru, in("ecx") 0, in("edx") 0,
1410                options(nostack, preserves_flags));
1411        }
1412    }
1413
1414    /// The arm's PKRU guards in the assembly, both ways: with the flag clear it
1415    /// runs neither RDPKRU nor WRPKRU (and reports PKRU 0), which is how a host
1416    /// without protection keys runs the subset cases; with it set (where this
1417    /// host has OSPKE) it sets the probe's PKRU and restores the caller's.
1418    /// Where this host has OSPKE the caller's PKRU is a distinctive value, so an
1419    /// unguarded WRPKRU with the flag clear, or a missing restore with it set,
1420    /// leaves the caller's PKRU changed.
1421    #[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; // no OSXSAVE: not an x86-64-v3 host
1425        }
1426        // SAFETY: OSXSAVE is set.
1427        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            // Keys 0 and 1 stay fully accessible, so this thread's memory, all
1462            // of it under key 0, stays usable whatever the arm does.
1463            const CALLER_PKRU: u32 = 0x5555_5550;
1464            let original = ospke.then(|| {
1465                // SAFETY: OSPKE is set, so RDPKRU and WRPKRU are enabled.
1466                let original = unsafe { rdpkru() };
1467                unsafe { wrpkru(CALLER_PKRU) };
1468                original
1469            });
1470            // SAFETY: the context and its buffers outlive the call; Reference
1471            // mode uses no descriptor.
1472            let status = unsafe { asm::hermit_xinuse_arm(&mut ctx) };
1473            if let Some(original) = original {
1474                // SAFETY: as above.
1475                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    /// The peer loop's PKRU guards, both ways, as for the arm: in the two
1493    /// states that write PKRU with WRPKRU, a peer with the flag clear leaves
1494    /// its thread's PKRU alone, and one with the flag set restores it at the
1495    /// end. Where this host has OSPKE the thread's PKRU starts at a value
1496    /// neither state writes, so an unguarded WRPKRU or a missing restore shows.
1497    #[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; // no OSXSAVE: not an x86-64-v3 host
1501        }
1502        // SAFETY: OSXSAVE is set.
1503        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                // SAFETY: each array has room for the two descriptors.
1512                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                // One handshake, then the byte that ends the loop.
1517                let bytes = [1u8, asm::PEER_EXIT];
1518                // SAFETY: writes two bytes from a live buffer into an empty pipe.
1519                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                // PKRU is per thread, so the peer runs on a thread of its own,
1525                // as in the probe.
1526                let (status, rounds, after) = std::thread::spawn(move || {
1527                    // Keys 0 to 2 stay fully accessible, and neither state
1528                    // writes this value.
1529                    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                    // SAFETY: OSPKE is set, so RDPKRU and WRPKRU are enabled.
1556                    let original = ospke.then(|| unsafe {
1557                        let original = rdpkru();
1558                        wrpkru(THREAD_PKRU);
1559                        original
1560                    });
1561                    // SAFETY: the context and its buffers outlive the call; the
1562                    // two states use neither the image nor tgid and tid.
1563                    let status = unsafe { asm::hermit_xinuse_peer_loop(&mut ctx) };
1564                    // SAFETY: as above.
1565                    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                // SAFETY: reads one byte into a live buffer; the peer wrote it.
1576                let read = unsafe { libc::read(from_peer[0], reply.as_mut_ptr().cast(), 1) };
1577                // SAFETY: the four descriptors were created above.
1578                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    /// A peer that fails before it serves a handshake is refused, not waited
1596    /// on.
1597    #[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    /// When a failed peer closes its reply descriptor, an arm blocked reading
1609    /// a reply sees end-of-file instead of waiting forever; and the
1610    /// descriptor is closed only once.
1611    #[test]
1612    fn a_failed_peer_ends_a_pending_reply_read() {
1613        let mut fds = [0i32; 2];
1614        // SAFETY: `fds` has room for the two descriptors.
1615        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        // SAFETY: reads at most one byte into a live buffer.
1624        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        // SAFETY: the read end was created above and is not used again.
1630        unsafe { libc::close(fds[0]) };
1631    }
1632
1633    /// A /proc/stat text whose cpuN lines have the given (busy, idle) jiffies.
1634    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        // CPUs 1 and 2 busy, 40 and 41 idle: the lowest idle one, 40.
1646        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        // Every CPU at least partly busy: the idlest, the lower on a tie.
1649        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        // Only allowed CPUs are considered.
1652        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        // APX (bit 19) and AMX (17, 18) are outside the probe.
1664        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        // AVX2-only hosts get no AVX-512 instruction.
1686        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}