RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

authorTyler Fowler <tyler@localhost>2026-10-30 11:00:20 -0700
commit7ad1388c3b1af493be4824bf58f1cf29159bd94e
parenta64c7daa9bbdf2bc171da89655822defeb6176b8
treebrowse files at this commit
WIP 15.1: per-thread FPU/SSE/AVX state (XSAVE/FXSAVE), signal fpstate, #MF/#XM, fputest
MCargo.toml+5 -0
Mbuild.rs+1 -0
Asrc/fpu.rs+208 -0
Msrc/interrupts.rs+19 -0
Msrc/lib.rs+2 -0
Msrc/process.rs+16 -6
Msrc/signal.rs+104 -10
Msrc/smp.rs+1 -0
Msrc/thread.rs+8 -0
Msrc/trap.rs+4 -0
Atests/musl_prereq.rs+74 -0
Auser/fputest.rs+457 -0

12 files changed, 899 insertions, 16 deletions

@@ -86,6 +86,10 @@ harness = false run-args = [     # Four CPU cores (smp.rs starts the other three).     "-smp", "4",+    # The most capable CPU QEMU can emulate: it has XSAVE and AVX (the+    # default "qemu64" model has neither, only FXSAVE + SSE2). fpu.rs+    # handles both.+    "-cpu", "max",     # Second disk: the FAT32 image build.rs makes (target/disk.img), as a     # virtio-blk PCI device. snapshot=on: writes go to a temporary overlay,     # so the image is the same at every boot (and tests can't corrupt it).@@ -123,6 +127,7 @@ run-args = [ # Used by `cargo test`: headless, results on serial, exit code 33 = pass. test-args = [     "-smp", "4",   # four CPU cores (tests that call init_smp use them all)+    "-cpu", "max", # XSAVE + AVX (fpu.rs; the default qemu64 has only FXSAVE)     # Second disk: the FAT32 image build.rs makes (target/disk.img), as a     # virtio-blk PCI device. snapshot=on: writes go to a temporary overlay,     # so the image is the same at every boot (and tests can't corrupt it).
@@ -77,6 +77,7 @@ const PROGRAMS: &[&str] = &[     "random", "getty", "tile", // stage 13: virtual drivers     "forktest", "cowtest", "mmaptest", "sigtest", // stage 14: processes     "threads", "echoserver", "ptytest", "nbtest", // stage 15: libc plumbing+    "fputest", // stage 15.1: musl prerequisites ];  const DISK_SECTORS: u64 = 64 * 1024 * 1024 / 512;
@@ -0,0 +1,208 @@+//! The FPU, SSE and AVX registers of user programs (stage 15.1).+//!+//! Besides the 16 general-purpose registers, an x86_64 CPU has a second set+//! for floating point and vector maths: the old x87 FPU stack, the 16 SSE+//! registers xmm0-xmm15 (128 bits each) plus their control register MXCSR,+//! and, on CPUs with AVX, the upper halves that make them 256-bit ymm0-ymm15.+//! Every C compiler uses xmm registers for `double` arithmetic and for fast+//! `memcpy`, so to the kernel they are as much "the program's registers" as+//! rax is: if two threads share a CPU, each must find its own values there+//! when it runs again.+//!+//! The kernel itself is built *soft-float* (see x86_64-rust_kernel.json): it+//! never touches these registers. That makes the job simple: whatever is in+//! them while the kernel runs belongs to the user thread the CPU last ran.+//! So we only+//!+//!   * save them into the thread's `FpuState` when the scheduler switches+//!     away from a user thread, and load the next user thread's (thread.rs,+//!     "eager" switching -- simple, and cheap enough at ~1 KiB);+//!   * copy them into a fork's child / a new thread (they start with the+//!     parent's values, like Linux);+//!   * reset them on execve (a new program starts clean);+//!   * put them in signal frames, so a handler may use floats and the+//!     interrupted code gets its own values back (signal.rs).+//!+//! The CPU has instructions that save/load all of it at once:+//!+//!   * FXSAVE / FXRSTOR (every x86_64 CPU): x87 + SSE, a 512-byte area.+//!   * XSAVE / XRSTOR (newer CPUs): a 512-byte legacy part in the same+//!     format, a 64-byte header (which parts are saved), then one part per+//!     extra feature -- AVX's upper ymm halves are 256 bytes more. Which+//!     features are switched on is the OS's choice, in register XCR0.+//!+//! Before user code may use any of this, the OS must say it will save the+//! state (otherwise the CPU raises "invalid opcode"): control register bits+//! CR4.OSFXSR (SSE allowed), CR4.OSXMMEXCPT (SSE maths errors raise #XM, not+//! #UD) and CR4.OSXSAVE (XSAVE + XCR0 allowed), on every CPU.++use core::arch::asm;+use core::arch::x86_64::{__cpuid, __cpuid_count};+use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};++/// Room for the biggest state we enable: x87 + SSE + AVX = 832 bytes with+/// XSAVE (we don't switch on AVX-512, which would need ~2.7 KiB).+pub const AREA_SIZE: usize = 1024;++/// One thread's saved FPU/SSE/AVX registers, in the FXSAVE/XSAVE memory+/// format. XSAVE needs 64-byte alignment.+#[repr(C, align(64))]+#[derive(Clone)]+pub struct FpuState(pub [u8; AREA_SIZE]);++/// XSAVE is used (else FXSAVE).+static USE_XSAVE: AtomicBool = AtomicBool::new(false);+/// The features in XCR0 (bit 0 x87, bit 1 SSE, bit 2 AVX).+static XCR0: AtomicU64 = AtomicU64::new(0);+/// Bytes of state: 512 (FXSAVE) or what CPUID says for our XCR0.+static SIZE: AtomicU32 = AtomicU32::new(512);+/// MXCSR bits the CPU supports; loading any other bit faults (#GP).+static MXCSR_MASK: AtomicU32 = AtomicU32::new(0xffbf);++/// MXCSR at reset: all six SSE exceptions masked, round to nearest.+pub const MXCSR_DEFAULT: u32 = 0x1f80;+/// x87 control word at reset (FNINIT): all exceptions masked, 64-bit precision.+const FCW_DEFAULT: u16 = 0x037f;++pub const XFEATURE_X87: u64 = 1;+pub const XFEATURE_SSE: u64 = 2;+pub const XFEATURE_AVX: u64 = 4;++/// Switch the FPU/SSE (and AVX) on for this CPU. Every CPU calls it once at+/// boot: CR0/CR4/XCR0 are per CPU.+pub fn init_cpu() {+    // CPUID leaf 1: ECX bit 26 = XSAVE, bit 28 = AVX.+    let leaf1 = __cpuid(1);+    let has_xsave = leaf1.ecx & (1 << 26) != 0;+    let has_avx = leaf1.ecx & (1 << 28) != 0;+    unsafe {+        // CR0: EM (bit 2, "no FPU: trap every FPU instruction") and TS (bit+        // 3, "trap the next one", for lazy switching) off; MP (bit 1) and NE+        // (bit 5, report x87 errors as exception #MF, not via the old PC IRQ 13) on.+        let mut cr0: u64;+        asm!("mov {}, cr0", out(reg) cr0);+        cr0 = (cr0 & !(1 << 2 | 1 << 3)) | 1 << 1 | 1 << 5;+        asm!("mov cr0, {}", in(reg) cr0);+        // CR4: OSFXSR (bit 9) + OSXMMEXCPT (bit 10), and OSXSAVE (bit 18).+        let mut cr4: u64;+        asm!("mov {}, cr4", out(reg) cr4);+        cr4 |= 1 << 9 | 1 << 10;+        if has_xsave {+            cr4 |= 1 << 18;+        }+        asm!("mov cr4, {}", in(reg) cr4);+    }+    let xcr0 = XFEATURE_X87 | XFEATURE_SSE | if has_avx && has_xsave { XFEATURE_AVX } else { 0 };+    if has_xsave {+        // XSETBV ecx=0: write XCR0 (edx:eax).+        unsafe { asm!("xsetbv", in("ecx") 0, in("eax") xcr0 as u32, in("edx") (xcr0 >> 32) as u32) };+        // CPUID leaf 0xD, sub-leaf 0: EBX = bytes XSAVE needs for the+        // features currently on in XCR0.+        let size = __cpuid_count(0xd, 0).ebx;+        assert!(size as usize <= AREA_SIZE, "XSAVE area too big: {}", size);+        SIZE.store(size, Ordering::Relaxed);+        XCR0.store(xcr0, Ordering::Relaxed);+        USE_XSAVE.store(true, Ordering::Relaxed);+    } else {+        XCR0.store(XFEATURE_X87 | XFEATURE_SSE, Ordering::Relaxed);+    }+    // Clean registers, and find out which MXCSR bits are allowed: FXSAVE+    // stores that mask at byte 28 (0 means the old default, 0xffbf).+    unsafe { asm!("fninit") };+    let mut probe = FpuState([0; AREA_SIZE]);+    unsafe { asm!("fxsave64 [{}]", in(reg) probe.0.as_mut_ptr()) };+    let mask = u32::from_le_bytes(probe.0[28..32].try_into().unwrap());+    MXCSR_MASK.store(if mask == 0 { 0xffbf } else { mask }, Ordering::Relaxed);+    restore(&initial());+}++pub fn uses_xsave() -> bool {+    USE_XSAVE.load(Ordering::Relaxed)+}+pub fn xfeatures() -> u64 {+    XCR0.load(Ordering::Relaxed)+}+/// Bytes of saved state (512 with FXSAVE).+pub fn size() -> usize {+    SIZE.load(Ordering::Relaxed) as usize+}++/// The state a new program starts with: everything zero, the x87 control+/// word and MXCSR at their reset values. The XSAVE header (bytes 512..576)+/// is all zero, which tells XRSTOR "every extra feature is in its initial+/// state" (zero).+pub fn initial() -> FpuState {+    let mut s = FpuState([0; AREA_SIZE]);+    s.0[0..2].copy_from_slice(&FCW_DEFAULT.to_le_bytes());+    s.0[24..28].copy_from_slice(&MXCSR_DEFAULT.to_le_bytes());+    if uses_xsave() {+        // ... except x87 + SSE, which we say are valid (the values above).+        s.0[512..520].copy_from_slice(&(XFEATURE_X87 | XFEATURE_SSE).to_le_bytes());+    }+    s+}++/// Save this CPU's registers into `s`.+pub fn save(s: &mut FpuState) {+    let p = s.0.as_mut_ptr();+    unsafe {+        if uses_xsave() {+            let m = xfeatures();+            asm!("xsave64 [{}]", in(reg) p, in("eax") m as u32, in("edx") (m >> 32) as u32);+        } else {+            asm!("fxsave64 [{}]", in(reg) p);+        }+    }+}++/// The registers right now (for fork, clone and signal frames).+pub fn save_live() -> FpuState {+    let mut s = FpuState([0; AREA_SIZE]);+    save(&mut s);+    s+}++/// Load `s` into this CPU's registers. `s` must be valid (see `sanitize`):+/// a bad MXCSR or XSAVE header would fault in the kernel.+pub fn restore(s: &FpuState) {+    let p = s.0.as_ptr();+    unsafe {+        if uses_xsave() {+            let m = xfeatures();+            asm!("xrstor64 [{}]", in(reg) p, in("eax") m as u32, in("edx") (m >> 32) as u32);+        } else {+            asm!("fxrstor64 [{}]", in(reg) p);+        }+    }+}++/// execve and signal handlers start from clean registers.+pub fn reset_live() {+    restore(&initial());+}++/// Make state that came from user memory (a signal frame the program may+/// have edited) safe to load: false if MXCSR has bits the CPU doesn't+/// support (XRSTOR would fault). Fixes the XSAVE header: only features we+/// enabled, standard (not compacted) format, reserved bytes zero.+pub fn sanitize(s: &mut FpuState) -> bool {+    let mxcsr = u32::from_le_bytes(s.0[24..28].try_into().unwrap());+    if mxcsr & !MXCSR_MASK.load(Ordering::Relaxed) != 0 {+        return false;+    }+    if uses_xsave() {+        let bv = u64::from_le_bytes(s.0[512..520].try_into().unwrap()) & xfeatures();+        s.0[512..520].copy_from_slice(&bv.to_le_bytes());+        s.0[520..576].fill(0); // XCOMP_BV + reserved+    }+    true+}++/// The SSE status flags of the last SIMD exception (#XM), for SIGFPE's+/// si_code: MXCSR bits 0-5 = invalid, denormal, divide by zero, overflow,+/// underflow, precision.+pub fn mxcsr() -> u32 {+    let mut v: u32 = 0;+    unsafe { asm!("stmxcsr [{}]", in(reg) &mut v as *mut u32) };+    v+}
@@ -73,6 +73,8 @@ lazy_static! {             idt.invalid_opcode.set_handler_addr(addr(trap::trap_invalid_opcode));             idt.general_protection_fault.set_handler_addr(addr(trap::trap_general_protection));             idt.page_fault.set_handler_addr(addr(trap::trap_page_fault));+            idt.x87_floating_point.set_handler_addr(addr(trap::trap_x87_error));+            idt.simd_floating_point.set_handler_addr(addr(trap::trap_simd_error));             idt[InterruptIndex::ApicTimer.as_u8()].set_handler_addr(addr(trap::trap_apic_timer));              // Run the double-fault handler on its own known-good stack (IST).@@ -127,11 +129,28 @@ pub extern "C" fn trap_handler(frame: &mut TrapFrame) {         0 => exception(frame, signal::SIGFPE, 1 /* FPE_INTDIV */, frame.rip, "divide error"),         6 => exception(frame, signal::SIGILL, 1 /* ILL_ILLOPC */, frame.rip, "invalid opcode"),         13 => exception(frame, signal::SIGSEGV, signal::SI_KERNEL, 0, "general protection fault"),+        16 => exception(frame, signal::SIGFPE, 0, frame.rip, "x87 floating-point error"),+        19 => exception(frame, signal::SIGFPE, simd_fpe_code(), frame.rip, "SIMD floating-point error"),         v => panic!("unexpected trap {}", v),     }     signal::deliver(frame); } +/// SIGFPE's si_code for an SSE exception (#XM, only raised if the program+/// unmasked it in MXCSR): which one, from MXCSR's status flags.+fn simd_fpe_code() -> i32 {+    let m = crate::fpu::mxcsr();+    let unmasked = m & !(m >> 7) & 0x3f; // flags whose mask bit (7-12) is clear+    match unmasked {+        f if f & 1 != 0 => 7,  // FPE_FLTINV: invalid operation+        f if f & 4 != 0 => 3,  // FPE_FLTDIV: divide by zero+        f if f & 8 != 0 => 4,  // FPE_FLTOVF: overflow+        f if f & 16 != 0 => 5, // FPE_FLTUND: underflow+        f if f & 32 != 0 => 6, // FPE_FLTRES: inexact result+        _ => 0,+    }+}+ /// An exception in user mode is the program's fault, not the kernel's: it /// gets a signal (which kills it, unless it has a handler). In the kernel /// it's a bug: stop.
@@ -29,6 +29,7 @@ pub mod dev; // /dev: null, random, hvc0, fb0, tty, ptmx, pts/N pub mod elf; // ELF64 loader for user programs pub mod errno; // Linux-compatible error numbers pub mod fbcon; // text console drawn on the framebuffer+pub mod fpu; // FPU/SSE/AVX registers: per-thread save/restore (XSAVE or FXSAVE) pub mod font; // 8x16 bitmap font (Spleen) for fbcon pub mod futex; // futexes: user-space locks' sleep/wake (threads) pub mod fs; // VFS: Inode/FileSystem/File traits, mount table, ramfs@@ -58,6 +59,7 @@ pub mod virtio; // modern virtio transport + virtio-rng, -console, -gpu /// PICs, and only then enable interrupts (`sti`). pub fn init() {     gdt::init();+    fpu::init_cpu(); // before any CPU starts: smp.rs copies our CR0/CR4     syscall::init();     interrupts::init_idt();     unsafe { interrupts::PICS.lock().initialize() };
@@ -53,6 +53,7 @@  use crate::elf::{self, ElfError}; use crate::errno::Errno;+use crate::fpu::FpuState; use crate::fs::{self, file::File}; use crate::mm::Mm; use crate::signal::{self, SigInfo, SignalState, ThreadSig};@@ -184,6 +185,10 @@ pub struct Task {     /// Its own signal mask and pending signals. Locked with interrupts off,     /// after the process's `signals` if both are needed.     pub sig: Mutex<ThreadSig>,+    /// Its FPU/SSE/AVX registers while it isn't running (fpu.rs). Only the+    /// scheduler touches it, with interrupts off: saved when the thread is+    /// switched out, loaded when it's switched in.+    pub fpu: Mutex<crate::fpu::FpuState>, }  impl Task {@@ -347,19 +352,20 @@ pub fn spawn_image(path: &str, elf_image: &[u8], spawn: SpawnArgs) -> Result<Arc         vfork_done: AtomicBool::new(false),     };     serial_println!("[process] started pid {} ({}), entry {:#x}", pid, name, start.rip);-    Ok(start_process(process, start, 0, 0))+    Ok(start_process(process, start, 0, 0, crate::fpu::initial())) }  /// Put the process in the table and start its first thread (tid = pid).-fn start_process(process: Process, start: TrapFrame, fs_base: u64, blocked: u64) -> Arc<Process> {+fn start_process(process: Process, start: TrapFrame, fs_base: u64, blocked: u64, fpu: FpuState) -> Arc<Process> {     let process = Arc::new(process);     without_interrupts(|| PROCESSES.lock().insert(process.pid, process.clone()));-    start_task(&process, process.pid, start, fs_base, blocked);+    start_task(&process, process.pid, start, fs_base, blocked, fpu);     process }  /// Start a thread `tid` of `process` that enters user mode with `start`.-fn start_task(process: &Arc<Process>, tid: u64, start: TrapFrame, fs_base: u64, blocked: u64) -> Arc<Task> {+/// `fpu`: its FPU/SSE registers (a copy of the creator's, or clean).+fn start_task(process: &Arc<Process>, tid: u64, start: TrapFrame, fs_base: u64, blocked: u64, fpu: FpuState) -> Arc<Task> {     let task = Arc::new(Task {         tid: AtomicU64::new(tid),         process: process.clone(),@@ -369,6 +375,7 @@ fn start_task(process: &Arc<Process>, tid: u64, start: TrapFrame, fs_base: u64,          start: Mutex::new(Some(start)),         killed: AtomicBool::new(false),         sig: Mutex::new(ThreadSig::new(blocked)),+        fpu: Mutex::new(fpu),     });     without_interrupts(|| process.tasks.lock().push(task.clone()));     let l4 = process.mm.lock().l4;@@ -894,7 +901,9 @@ pub fn fork(parent: &Arc<Process>, frame: &TrapFrame, stack: u64, vfork: bool) -         vfork_done: AtomicBool::new(false),     };     serial_println!("[process] pid {} forked pid {}", parent.pid, pid);-    let child = start_process(child, start, fs_base, blocked);+    // The child's FPU/SSE registers start as a copy of ours (we're running,+    // so ours are in the CPU right now).+    let child = start_process(child, start, fs_base, blocked, crate::fpu::save_live());     if vfork {         // The parent sleeps until the child has exec'd or exited (real         // vfork shares the memory meanwhile; we just use copy-on-write).@@ -929,7 +938,7 @@ pub fn clone_thread(p: &Arc<Process>, frame: &TrapFrame, stack: u64, tls: Option     let blocked = without_interrupts(|| me.sig.lock().blocked);     // Forget scheduler threads of threads that are completely gone.     without_interrupts(|| p.kthreads.lock().retain(|&k| thread::exists(k)));-    let task = start_task(p, tid, start, fs_base, blocked);+    let task = start_task(p, tid, start, fs_base, blocked, crate::fpu::save_live());     task.clear_child_tid.store(clear_tid, Ordering::Relaxed);     serial_println!("[process] pid {} new thread {}", p.pid, tid);     Ok(tid)@@ -980,6 +989,7 @@ pub fn execve(p: &Arc<Process>, frame: &mut TrapFrame, path: &str, argv: &[Vec<u     *p.exe.lock() = String::from(path);     if let Some(me) = thread::current_task() {         me.fs_base.store(0, Ordering::Relaxed); // no TLS until the new libc sets one+        crate::fpu::reset_live(); // clean FPU/SSE/AVX registers for the new program         me.clear_child_tid.store(0, Ordering::Relaxed);     }     *p.env.lock() = envp.iter().map(|e| String::from_utf8_lossy(e).into_owned()).collect();
@@ -627,7 +627,7 @@ struct SigContext {     trapno: u64,     oldmask: u64,     cr2: u64,-    fpstate: u64, // pointer to saved FPU/SSE state; 0 = none (see below)+    fpstate: u64, // pointer to the saved FPU/SSE/AVX state (below the frame); 0 = none     reserved: [u64; 8], } @@ -655,20 +655,47 @@ struct RtSigFrame { /// enable, I/O privilege level...) only the kernel decides. const USER_RFLAGS: u64 = 0x0004_0DD5 | (1 << 16); +// The FPU/SSE/AVX state in a signal frame (Linux's layout, stage 15.1).+// It sits above the rt_sigframe, 64-byte aligned, and+// `uc.mcontext.fpstate` points at it. Its first 512 bytes are the FXSAVE+// format; bytes 464..512 (which FXSAVE leaves unused) hold+// `struct _fpx_sw_bytes`, saying whether an XSAVE area follows:+//+//   u32 magic1 = FP_XSTATE_MAGIC1   u32 extended_size (state + 4)+//   u64 xfeatures (= XCR0)          u32 xstate_size   + 7 u32 padding+//+// and FP_XSTATE_MAGIC2 is stored right after the XSAVE area. With only+// FXSAVE, magic1 is 0. A handler may read and even change the saved+// values: sigreturn loads whatever is there (after checking it).+const FP_XSTATE_MAGIC1: u32 = 0x4650_5853;+const FP_XSTATE_MAGIC2: u32 = 0x4650_5845;+const SW_BYTES: usize = 464;+/// uc_flags: an XSAVE area is there (UC_FP_XSTATE), and the two flags+/// Linux always sets on x86_64 (UC_SIGCONTEXT_SS | UC_STRICT_RESTORE_SS).+const UC_FP_XSTATE: u64 = 1;+const UC_SS_FLAGS: u64 = 2 | 4;++/// Bytes the FPU state takes in a signal frame.+fn fpstate_size() -> u64 {+    if crate::fpu::uses_xsave() { crate::fpu::size() as u64 + 4 } else { 512 }+}+ /// Push a signal frame on the user stack and point `frame` at the handler.+/// The stack, from higher to lower addresses: ///-/// Note: we don't save the FPU/SSE registers (`fpstate` = 0). Our kernel-/// doesn't use them and our programs' handlers don't touch floats, so they-/// survive anyway; a full libc needs XSAVE state here (roadmap).+///   [interrupted code's stack] [128-byte red zone] [FPU state, 64-aligned]+///   [rt_sigframe: pretcode, ucontext, siginfo]  <- rsp of the handler fn setup_frame(t: &Task, frame: &mut TrapFrame, sig: u32, info: &SigInfo, action: &SigAction) -> Result<(), ()> {     if action.flags & SA_RESTORER == 0 || action.restorer == 0 {         return Err(()); // nowhere to return to (x86_64 requires a restorer)     }     let size = core::mem::size_of::<RtSigFrame>() as u64;     // Below the 128-byte "red zone" (leaf functions may use it without-    // moving rsp), aligned so the handler starts with rsp % 16 == 8, as-    // if it had been called.-    let sp = ((frame.rsp.wrapping_sub(128).wrapping_sub(size)) & !0xf).wrapping_sub(8);+    // moving rsp) goes the FPU state, then the frame, aligned so the+    // handler starts with rsp % 16 == 8, as if it had been called.+    let fp_size = fpstate_size();+    let fp = (frame.rsp.wrapping_sub(128).wrapping_sub(fp_size)) & !63;+    let sp = ((fp.wrapping_sub(size)) & !0xf).wrapping_sub(8);     // The mask sigreturn restores: the real one, if a ppoll & co. had     // swapped in a temporary mask.     let blocked = without_interrupts(|| {@@ -700,17 +727,44 @@ fn setup_frame(t: &Task, frame: &mut TrapFrame, sig: u32, info: &SigInfo, action         trapno: frame.vector,         oldmask: blocked,         cr2: if sig == SIGSEGV || sig == SIGBUS { info.fields[0] } else { 0 },+        fpstate: fp,         ..Default::default()     };+    let xsave = crate::fpu::uses_xsave();     let rt = RtSigFrame {         pretcode: action.restorer,-        uc: UContext { mcontext, sigmask: blocked, ..Default::default() },+        uc: UContext {+            flags: UC_SS_FLAGS | if xsave { UC_FP_XSTATE } else { 0 },+            mcontext,+            sigmask: blocked,+            ..Default::default()+        },         info: *info,     };-    if !crate::mm::user_access_ok(sp, size, true) {+    if !crate::mm::user_access_ok(sp, fp + fp_size - sp, true) {         return Err(());     }-    unsafe { core::ptr::write_unaligned(sp as *mut RtSigFrame, rt) };+    // The FPU registers right now are still the interrupted code's (the+    // kernel never touches them): save them into the frame.+    let mut state = crate::fpu::save_live();+    let n = crate::fpu::size();+    let sw = &mut state.0[SW_BYTES..512];+    sw.fill(0);+    if xsave {+        sw[0..4].copy_from_slice(&FP_XSTATE_MAGIC1.to_le_bytes());+        sw[4..8].copy_from_slice(&(n as u32 + 4).to_le_bytes());+        sw[8..16].copy_from_slice(&crate::fpu::xfeatures().to_le_bytes());+        sw[16..20].copy_from_slice(&(n as u32).to_le_bytes());+        state.0[n..n + 4].copy_from_slice(&FP_XSTATE_MAGIC2.to_le_bytes());+    }+    unsafe {+        core::ptr::copy_nonoverlapping(state.0.as_ptr(), fp as *mut u8, fp_size as usize);+        core::ptr::write_unaligned(sp as *mut RtSigFrame, rt);+    }+    // Like Linux, the handler starts with clean FPU/SSE registers (MXCSR at+    // its default, so its float maths can't trap because the interrupted+    // code had unmasked an exception).+    crate::fpu::reset_live();     frame.rsp = sp;     frame.rip = action.handler;     frame.rdi = sig as u64;@@ -766,6 +820,46 @@ pub fn sys_rt_sigreturn(frame: &mut TrapFrame) {     if let Some(t) = thread::current_task() {         without_interrupts(|| t.sig.lock().blocked = rt.uc.sigmask & !UNBLOCKABLE);     }+    if restore_fpstate(m.fpstate).is_err() {+        process::exit_group(ExitStatus::Killed(SIGSEGV, "bad sigreturn FPU state"));+    }+}++/// sigreturn's FPU part: load the state `fpstate` points to (0 = clean+/// registers, like Linux). An XSAVE area is used only if both magic numbers+/// are there; otherwise just the 512-byte FXSAVE part (the AVX halves are+/// then cleared). Err if it's unreadable or has an impossible MXCSR.+fn restore_fpstate(fpstate: u64) -> Result<(), ()> {+    use crate::fpu;+    if fpstate == 0 {+        fpu::reset_live();+        return Ok(());+    }+    let mut state = fpu::FpuState([0; fpu::AREA_SIZE]);+    let legacy = crate::syscall::user_slice(fpstate, 512).map_err(|_| ())?;+    state.0[..512].copy_from_slice(legacy);+    let word = |b: &[u8], at: usize| u32::from_le_bytes(b[at..at + 4].try_into().unwrap());+    let n = fpu::size();+    let mut extended = false;+    if fpu::uses_xsave() && word(&state.0, SW_BYTES) == FP_XSTATE_MAGIC1 && word(&state.0, SW_BYTES + 16) as usize == n {+        if let Ok(all) = crate::syscall::user_slice(fpstate, n as u64 + 4) {+            if word(all, n) == FP_XSTATE_MAGIC2 {+                state.0[..n].copy_from_slice(&all[..n]);+                extended = true;+            }+        }+    }+    if fpu::uses_xsave() && !extended {+        // An FXSAVE-only frame: x87 + SSE from it, every other feature+        // back to its initial state (XSAVE header: just bits 0 and 1).+        state.0[512..fpu::AREA_SIZE].fill(0);+        state.0[512..520].copy_from_slice(&(fpu::XFEATURE_X87 | fpu::XFEATURE_SSE).to_le_bytes());+    }+    if !fpu::sanitize(&mut state) {+        return Err(());+    }+    fpu::restore(&state);+    Ok(()) }  // ---------------------------------------------------------------------------
@@ -336,6 +336,7 @@ extern "C" fn ap_entry(cpu: u64) -> ! {     let cpu = cpu as usize;     AP_ALIVE.store(true, Ordering::SeqCst);     gdt::init_ap(cpu);+    crate::fpu::init_cpu(); // XCR0 and the FPU are per CPU     interrupts::init_idt();     crate::syscall::init();     apic::init_ap();
@@ -366,6 +366,12 @@ pub fn schedule() {             }         } +        // The FPU/SSE/AVX registers (fpu.rs): the kernel never uses them, so+        // they still hold the outgoing user thread's values. Save those...+        if let Some(task) = &s.thread(cur).task {+            crate::fpu::save(&mut task.fpu.lock());+        }+         let bit = 1u32 << cpu;         let t = s.thread(next);         t.state = State::Running;@@ -379,6 +385,8 @@ pub fn schedule() {             x86_64::registers::model_specific::FsBase::write(x86_64::VirtAddr::new_truncate(                 task.fs_base.load(core::sync::atomic::Ordering::Relaxed),             ));+            // ...and load the incoming one's.+            crate::fpu::restore(&task.fpu.lock());         }         if let Some(stack) = &t.stack {             if t.tlb_clean_on & bit == 0 {
@@ -178,6 +178,8 @@ trap_stub!("trap_divide_error", "0", "push 0"); // SIGFPE trap_stub!("trap_invalid_opcode", "6", "push 0"); // SIGILL trap_stub!("trap_general_protection", "13", ""); // SIGSEGV (CPU pushes an error code) trap_stub!("trap_page_fault", "14", ""); // SIGSEGV or demand paging+trap_stub!("trap_x87_error", "16", "push 0"); // SIGFPE (an unmasked x87 exception)+trap_stub!("trap_simd_error", "19", "push 0"); // SIGFPE (an unmasked SSE exception) trap_stub!("trap_apic_timer", "0x40", "push 0");  unsafe extern "C" {@@ -185,5 +187,7 @@ unsafe extern "C" {     pub fn trap_invalid_opcode();     pub fn trap_general_protection();     pub fn trap_page_fault();+    pub fn trap_x87_error();+    pub fn trap_simd_error();     pub fn trap_apic_timer(); }
@@ -0,0 +1,74 @@+//! Stage 15.1, what a static musl binary needs from the kernel before it+//! can even start: its own FPU/SSE/AVX registers per thread (saved on+//! every context switch, in signal frames, cleared by exec), Linux's+//! C-string path syscalls and `struct stat`, and Linux's syscall numbers.+//! As before the detailed checks live in user programs (fputest,+//! pathtest, rawabi: exit code = failed checks); this file runs them and+//! checks nothing is left behind.+#![no_std]+#![no_main]+#![feature(custom_test_frameworks)]+#![test_runner(rustbelt::test_runner)]+#![reexport_test_harness_main = "test_main"]++extern crate alloc;++use alloc::string::String;+use bootloader::{BootInfo, entry_point};+use core::panic::PanicInfo;+use rustbelt::process::{self, ExitStatus, SpawnArgs};+use rustbelt::{fs, memory, thread};++entry_point!(main);++fn main(boot_info: &'static BootInfo) -> ! {+    rustbelt::init();+    rustbelt::init_memory(boot_info);+    rustbelt::init_smp(); // all 4 CPUs run threads+    fs::init();+    rustbelt::init_storage();+    test_main();+    rustbelt::hlt_loop();+}++fn run(name: &str, args: &[&str]) -> ExitStatus {+    let mut spawn = SpawnArgs::kernel(name);+    spawn.args.extend(args.iter().map(|a| String::from(*a)));+    let p = process::spawn_path(&alloc::format!("/mnt/disk0/bin/{}", name), spawn).expect("spawn failed");+    process::wait(&p)+}++fn counts() -> (usize, usize, usize) {+    thread::sleep_ticks(3); // let exited threads' stacks be freed+    (thread::thread_count(), process::count(), memory::frames_in_use())+}++/// Run a program twice (the first warms caches), check it passes and+/// threads, processes and memory frames come back to where they were.+fn run_without_leaks(name: &str, args: &[&str]) {+    assert_eq!(run(name, args), ExitStatus::Exited(0), "{} failed (warm-up)", name);+    let before = counts();+    assert_eq!(run(name, args), ExitStatus::Exited(0), "{} failed", name);+    assert_eq!(counts(), before, "{} leaked something", name);+}++/// The kernel set up the FPU on every CPU: XSAVE under QEMU's `-cpu max`.+#[test_case]+fn fpu_enabled() {+    assert!(rustbelt::fpu::size() >= 512);+    if rustbelt::fpu::uses_xsave() {+        assert!(rustbelt::fpu::xfeatures() & 3 == 3, "x87 and SSE state in XCR0");+    }+}++/// 12 threads in 3 processes with distinct vector registers on 4 CPUs;+/// signal handlers that clobber them; fork copies, exec clears; SIGFPE.+#[test_case]+fn fpu_state_is_per_thread() {+    run_without_leaks("fputest", &[]);+}++#[panic_handler]+fn panic(info: &PanicInfo) -> ! {+    rustbelt::test_panic_handler(info)+}
@@ -0,0 +1,457 @@+//! fputest -- every thread keeps its own FPU/SSE/AVX registers (stage 15.1).+//!+//! The kernel never uses the FPU registers itself, but user code does (any+//! C program's `double` maths, and libc's memcpy/strlen use SSE). So on a+//! context switch the kernel saves one thread's registers and loads the+//! next one's (XSAVE, or FXSAVE on older CPUs), a signal handler gets+//! clean registers and sigreturn puts the old ones back, fork copies them+//! and exec clears them. This program checks all of that:+//!+//! - threads + processes (spread over all CPUs) each load *distinct*+//!   values into xmm0-15 (all 256 bits of ymm0-15 if the CPU has AVX) and+//!   MXCSR, spin and yield for a while, then check nothing changed;+//! - a signal handler that scribbles over every register, synchronously+//!   (kill to ourselves) and asynchronously (another thread's tgkill while+//!   we spin): the interrupted code must see its own values afterwards;+//! - the handler starts with clean registers, finds the saved ones via+//!   uc_mcontext.fpstate (Linux's layout), and a value it changes there is+//!   what sigreturn loads;+//! - an unmasked SSE divide-by-zero raises SIGFPE (FPE_FLTDIV);+//! - fork copies the registers; exec starts with zeros and MXCSR 0x1f80.+//!+//! Our programs are built soft-float, so Rust code here never touches the+//! vector registers: only the `asm!` blocks below do. (That's also why the+//! asm can't name xmm registers as operands: it uses memory instead.)+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++extern crate alloc;++use alloc::sync::Arc;+use alloc::vec::Vec;+use core::arch::asm;+use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};+use ulib::*;++/// The 16 vector registers, 32 bytes each (only the low 16 used without AVX).+#[repr(C, align(32))]+#[derive(Clone, Copy, PartialEq)]+struct Regs([u8; 512]);++static AVX: AtomicBool = AtomicBool::new(false);++/// Can we use AVX? The CPU must have it (CPUID.1:ECX bit 28) *and* the+/// kernel must have turned on XSAVE (OSXSAVE, bit 27) with the AVX state+/// in XCR0 (bits 1 and 2): otherwise AVX instructions fault (#UD).+fn detect_avx() -> bool {+    let ecx = core::arch::x86_64::__cpuid(1).ecx;+    if ecx & (1 << 27) == 0 || ecx & (1 << 28) == 0 {+        return false;+    }+    let (lo, _hi): (u32, u32);+    unsafe { asm!("xgetbv", in("ecx") 0, out("eax") lo, out("edx") _hi, options(nomem, nostack)) };+    lo & 6 == 6+}+fn avx() -> bool {+    AVX.load(Ordering::Relaxed)+}++/// Load all vector registers from `r`.+fn load(r: &Regs) {+    unsafe {+        if avx() {+            asm!(+            "vmovdqu ymm0, [{p} + 0]",+            "vmovdqu ymm1, [{p} + 32]",+            "vmovdqu ymm2, [{p} + 64]",+            "vmovdqu ymm3, [{p} + 96]",+            "vmovdqu ymm4, [{p} + 128]",+            "vmovdqu ymm5, [{p} + 160]",+            "vmovdqu ymm6, [{p} + 192]",+            "vmovdqu ymm7, [{p} + 224]",+            "vmovdqu ymm8, [{p} + 256]",+            "vmovdqu ymm9, [{p} + 288]",+            "vmovdqu ymm10, [{p} + 320]",+            "vmovdqu ymm11, [{p} + 352]",+            "vmovdqu ymm12, [{p} + 384]",+            "vmovdqu ymm13, [{p} + 416]",+            "vmovdqu ymm14, [{p} + 448]",+            "vmovdqu ymm15, [{p} + 480]",+                p = in(reg) r.0.as_ptr(), options(nostack, readonly, preserves_flags));+        } else {+            asm!(+            "movdqu xmm0, [{p} + 0]",+            "movdqu xmm1, [{p} + 32]",+            "movdqu xmm2, [{p} + 64]",+            "movdqu xmm3, [{p} + 96]",+            "movdqu xmm4, [{p} + 128]",+            "movdqu xmm5, [{p} + 160]",+            "movdqu xmm6, [{p} + 192]",+            "movdqu xmm7, [{p} + 224]",+            "movdqu xmm8, [{p} + 256]",+            "movdqu xmm9, [{p} + 288]",+            "movdqu xmm10, [{p} + 320]",+            "movdqu xmm11, [{p} + 352]",+            "movdqu xmm12, [{p} + 384]",+            "movdqu xmm13, [{p} + 416]",+            "movdqu xmm14, [{p} + 448]",+            "movdqu xmm15, [{p} + 480]",+                p = in(reg) r.0.as_ptr(), options(nostack, readonly, preserves_flags));+        }+    }+}++/// Store all vector registers (the bytes AVX doesn't have stay 0).+fn store() -> Regs {+    let mut r = Regs([0; 512]);+    unsafe {+        if avx() {+            asm!(+            "vmovdqu [{p} + 0], ymm0",+            "vmovdqu [{p} + 32], ymm1",+            "vmovdqu [{p} + 64], ymm2",+            "vmovdqu [{p} + 96], ymm3",+            "vmovdqu [{p} + 128], ymm4",+            "vmovdqu [{p} + 160], ymm5",+            "vmovdqu [{p} + 192], ymm6",+            "vmovdqu [{p} + 224], ymm7",+            "vmovdqu [{p} + 256], ymm8",+            "vmovdqu [{p} + 288], ymm9",+            "vmovdqu [{p} + 320], ymm10",+            "vmovdqu [{p} + 352], ymm11",+            "vmovdqu [{p} + 384], ymm12",+            "vmovdqu [{p} + 416], ymm13",+            "vmovdqu [{p} + 448], ymm14",+            "vmovdqu [{p} + 480], ymm15",+                p = in(reg) r.0.as_mut_ptr(), options(nostack, preserves_flags));+        } else {+            asm!(+            "movdqu [{p} + 0], xmm0",+            "movdqu [{p} + 32], xmm1",+            "movdqu [{p} + 64], xmm2",+            "movdqu [{p} + 96], xmm3",+            "movdqu [{p} + 128], xmm4",+            "movdqu [{p} + 160], xmm5",+            "movdqu [{p} + 192], xmm6",+            "movdqu [{p} + 224], xmm7",+            "movdqu [{p} + 256], xmm8",+            "movdqu [{p} + 288], xmm9",+            "movdqu [{p} + 320], xmm10",+            "movdqu [{p} + 352], xmm11",+            "movdqu [{p} + 384], xmm12",+            "movdqu [{p} + 416], xmm13",+            "movdqu [{p} + 448], xmm14",+            "movdqu [{p} + 480], xmm15",+                p = in(reg) r.0.as_mut_ptr(), options(nostack, preserves_flags));+        }+    }+    r+}++fn get_mxcsr() -> u32 {+    let mut m = 0u32;+    unsafe { asm!("stmxcsr [{}]", in(reg) &mut m, options(nostack, preserves_flags)) };+    m+}+fn set_mxcsr(m: u32) {+    unsafe { asm!("ldmxcsr [{}]", in(reg) &m, options(nostack, readonly, preserves_flags)) };+}++/// A register image no other `seed` produces (only the parts this CPU has).+fn pattern(seed: u64) -> Regs {+    let mut r = Regs([0; 512]);+    let mut x = seed.wrapping_mul(0x9e37_79b9_7f4a_7c15) | 1;+    for (i, chunk) in r.0.chunks_mut(8).enumerate() {+        x ^= x << 13;+        x ^= x >> 7;+        x ^= x << 17;+        if avx() || i % 4 < 2 {+            chunk.copy_from_slice(&x.to_le_bytes());+        }+    }+    r+}+/// A distinct MXCSR per seed: all exceptions masked, rounding mode (bits+/// 13-14) and flush-to-zero / denormals-are-zero (15, 6) from the seed.+fn mxcsr_for(seed: u64) -> u32 {+    0x1f80 | ((seed as u32 & 3) << 13) | ((seed as u32 >> 2 & 1) << 15) | ((seed as u32 >> 3 & 1) << 6)+}++/// Load a pattern, spin and yield across many timer ticks (so the thread+/// is switched out and probably migrated), checking it now and then.+/// Returns how many checks found a wrong register.+fn hammer(seed: u64, rounds: u32) -> u32 {+    let want = pattern(seed);+    let mx = mxcsr_for(seed);+    load(&want);+    set_mxcsr(mx);+    let mut bad = 0;+    for round in 0..rounds {+        let mut x = round as u64;+        for _ in 0..20_000 {+            x = core::hint::black_box(x.wrapping_mul(6364136223846793005).wrapping_add(1));+        }+        if round % 3 == 0 {+            sched_yield();+        }+        if store() != want || get_mxcsr() != mx {+            bad += 1;+            load(&want); // keep going with good values+            set_mxcsr(mx);+        }+    }+    set_mxcsr(0x1f80);+    bad+}++// --- signal handlers -----------------------------------------------------------++/// Offsets in the ucontext the kernel passes (same as Linux's): the+/// `fpstate` pointer in uc_mcontext, and inside the FXSAVE area: MXCSR,+/// xmm0, the software-reserved bytes (sw_bytes) and, in the XSAVE area,+/// the upper halves of ymm0-15.+const UC_FLAGS: usize = 0;+const UC_FPSTATE: usize = 40 + 23 * 8;+const FX_MXCSR: usize = 24;+const FX_XMM: usize = 160;+const FX_SW: usize = 464;+const XS_YMM_HI: usize = 576;+const MAGIC1: u32 = 0x4650_5853;+const MAGIC2: u32 = 0x4650_5845;++static SYNC_SEED: AtomicU64 = AtomicU64::new(0);+/// Bit flags the handler sets for main to check.+static H_CLEAN: AtomicU32 = AtomicU32::new(0); // registers zero + MXCSR default on entry+static H_SAVED: AtomicU32 = AtomicU32::new(0); // the frame held the interrupted values+static H_LAYOUT: AtomicU32 = AtomicU32::new(0); // magic numbers / flags right+static ASYNC_HITS: AtomicU32 = AtomicU32::new(0);+const NEW_XMM15: u64 = 0x1515_1515_cafe_f00d;++fn rd32(p: *const u8) -> u32 {+    unsafe { core::ptr::read_unaligned(p as *const u32) }+}+fn rd64(p: *const u8) -> u64 {+    unsafe { core::ptr::read_unaligned(p as *const u64) }+}++/// Scribble over every vector register and MXCSR (a handler that does+/// float maths, or calls memcpy, would).+fn clobber() {+    load(&pattern(0xdead));+    set_mxcsr(0x1f80 | (3 << 13));+}++extern "C" fn on_usr1(_sig: i32, _info: *const SigInfo, uc: *mut u8) {+    let clean = store() == Regs([0; 512]) && get_mxcsr() == 0x1f80;+    H_CLEAN.store(clean as u32, Ordering::SeqCst);+    let want = pattern(SYNC_SEED.load(Ordering::SeqCst));+    let fp = rd64(unsafe { uc.add(UC_FPSTATE) }) as *mut u8;+    if fp.is_null() || fp as u64 % 64 != 0 {+        H_SAVED.store(0, Ordering::SeqCst);+    } else {+        // xmm i is at FX_XMM + 16*i; our image has register i at 32*i.+        let mut ok = rd32(unsafe { fp.add(FX_MXCSR) }) == mxcsr_for(SYNC_SEED.load(Ordering::SeqCst));+        for i in 0..16 {+            for b in 0..16 {+                ok &= unsafe { *fp.add(FX_XMM + 16 * i + b) } == want.0[32 * i + b];+            }+        }+        let flags = rd64(unsafe { uc.add(UC_FLAGS) });+        let magic1 = rd32(unsafe { fp.add(FX_SW) });+        let layout = if magic1 == MAGIC1 {+            // XSAVE frame: sw_bytes say how big it is; magic2 at the end.+            let size = rd32(unsafe { fp.add(FX_SW + 16) }) as usize;+            let xfeatures = rd64(unsafe { fp.add(FX_SW + 8) });+            if avx() {+                for i in 0..16 {+                    for b in 0..16 {+                        ok &= unsafe { *fp.add(XS_YMM_HI + 16 * i + b) } == want.0[32 * i + 16 + b];+                    }+                }+            }+            flags & 1 != 0 && rd32(unsafe { fp.add(size) }) == MAGIC2 && xfeatures & 3 == 3 && (!avx() || xfeatures & 4 != 0)+        } else {+            // FXSAVE-only frame (no XSAVE): fine as long as we're not using AVX.+            magic1 == 0 && !avx()+        };+        H_SAVED.store(ok as u32, Ordering::SeqCst);+        H_LAYOUT.store(layout as u32, Ordering::SeqCst);+        // Change the saved xmm15: sigreturn must load what's in the frame.+        unsafe { core::ptr::write_unaligned(fp.add(FX_XMM + 16 * 15) as *mut u64, NEW_XMM15) };+    }+    clobber();+}++extern "C" fn on_usr2(_sig: i32) {+    clobber();+    ASYNC_HITS.fetch_add(1, Ordering::SeqCst);+}++static FPE_CODE: AtomicU32 = AtomicU32::new(0);+/// SIGFPE from an unmasked SSE exception. Returning would just run the+/// faulting instruction again, so mask all exceptions (and clear the+/// flags) in the *saved* MXCSR first: the retry then gives inf quietly.+extern "C" fn on_fpe(_sig: i32, info: *const SigInfo, uc: *mut u8) {+    FPE_CODE.store(unsafe { (*info).code } as u32, Ordering::SeqCst);+    let fp = rd64(unsafe { uc.add(UC_FPSTATE) }) as *mut u8;+    if !fp.is_null() {+        unsafe { core::ptr::write_unaligned(fp.add(FX_MXCSR) as *mut u32, 0x1f80) };+    }+}++// --- the tests ---------------------------------------------------------------------++/// N threads in this process, each with its own seed; returns bad checks.+fn thread_round(base: u64, n: u64, rounds: u32) -> u32 {+    let bad = Arc::new(AtomicU32::new(0));+    let mut ts = Vec::new();+    for i in 0..n {+        let bad = bad.clone();+        ts.push(spawn_thread(0, move || {+            bad.fetch_add(hammer(base + i, rounds), Ordering::SeqCst);+        }));+    }+    let main_bad = hammer(base + 99, rounds);+    for t in ts {+        match t {+            Ok(t) => t.join(),+            Err(_) => return 1000,+        }+    }+    bad.load(Ordering::SeqCst) + main_bad+}++fn exec_check() -> i64 {+    // Run straight after exec: nothing has touched the vector registers.+    let clean = store() == Regs([0; 512]) && get_mxcsr() == 0x1f80;+    if clean { 0 } else { 1 }+}++fn main() -> i64 {+    AVX.store(detect_avx(), Ordering::Relaxed);+    if arg(1) == Some("exec-check") {+        return exec_check();+    }+    let mut c = Checks::new("fputest");+    println!(+        "fputest: {} registers, {} CPU(s)",+        if avx() { "ymm0-15 (AVX)" } else { "xmm0-15 (SSE)" },+        sysinfo().cpus+    );++    // 1. The registers start clean (main ran just after exec).+    c.check(exec_check() == 0, "a new program starts with zeroed registers and MXCSR 0x1f80");++    // 2. Threads and processes, all with distinct values, on all CPUs.+    let mut children = Vec::new();+    for p in 1..=2u64 {+        let pid = fork();+        if pid == 0 {+            exit(thread_round(p * 1000, 3, 60).min(100) as i64);+        }+        children.push(pid);+    }+    let bad = thread_round(0, 3, 60);+    let mut child_bad = 0;+    for pid in children {+        child_bad += wait(pid);+    }+    c.check(bad == 0, "4 threads kept their own vector registers + MXCSR");+    c.check(child_bad == 0, "so did 4 threads in each of 2 forked processes (12 at once)");++    // 3. fork copies the registers; exec clears them.+    let want = pattern(7);+    load(&want);+    set_mxcsr(mxcsr_for(7));+    let pid = fork();+    if pid == 0 {+        let same = store() == want && get_mxcsr() == mxcsr_for(7);+        if !same {+            exit(2);+        }+        execve("/mnt/disk0/bin/fputest", &["fputest", "exec-check"], &[]);+        exit(3);+    }+    let parent_same = store() == want;+    set_mxcsr(0x1f80);+    let code = wait(pid);+    c.check(parent_same && code != 2, "fork: the child starts with the parent's registers");+    c.check(code == 0, "exec: the new program starts with clean registers");++    // 4. A synchronous signal whose handler clobbers everything.+    sigaction_info(SIGUSR1, on_usr1, 0);+    SYNC_SEED.store(42, Ordering::SeqCst);+    let mut want = pattern(42);+    load(&want);+    set_mxcsr(mxcsr_for(42));+    kill(getpid(), SIGUSR1); // the handler runs on the way back from this+    let got = store();+    let mx = get_mxcsr();+    set_mxcsr(0x1f80);+    c.check(H_CLEAN.load(Ordering::SeqCst) == 1, "the handler starts with clean registers");+    c.check(H_SAVED.load(Ordering::SeqCst) == 1, "uc_mcontext.fpstate holds the interrupted registers");+    c.check(H_LAYOUT.load(Ordering::SeqCst) == 1, "the fpstate has Linux's layout (sw_bytes, magics, uc_flags)");+    want.0[32 * 15..32 * 15 + 8].copy_from_slice(&NEW_XMM15.to_le_bytes());+    c.check(got == want && mx == mxcsr_for(42), "after sigreturn: our registers back, with the handler's xmm15 edit");++    // 5. Asynchronous signals: another thread interrupts us while we spin.+    signal(SIGUSR2, on_usr2);+    let me = gettid();+    let sender = spawn_thread(0, move || {+        for _ in 0..40 {+            tgkill(getpid(), me, SIGUSR2);+            sleep_ms(2);+        }+    });+    let want = pattern(4242);+    load(&want);+    set_mxcsr(mxcsr_for(4242));+    let t0 = now_us();+    let mut bad = 0;+    while ASYNC_HITS.load(Ordering::SeqCst) < 40 && now_us() - t0 < 5_000_000 {+        if store() != want || get_mxcsr() != mxcsr_for(4242) {+            bad += 1;+            load(&want);+            set_mxcsr(mxcsr_for(4242));+        }+    }+    set_mxcsr(0x1f80);+    if let Ok(t) = sender {+        t.join();+    }+    let hits = ASYNC_HITS.load(Ordering::SeqCst);+    println!("fputest: {} async signals, each handler clobbered every register", hits);+    c.check(hits == 40 && bad == 0, "40 async signals with clobbering handlers changed nothing we saw");++    // 6. An unmasked SSE exception (divide by zero) -> SIGFPE, FPE_FLTDIV.+    sigaction_info(SIGFPE, on_fpe, 0);+    let one = 1.0f64.to_bits();+    let zero = 0u64;+    let mut out = 0u64;+    set_mxcsr(0x1f80 & !(1 << 9)); // unmask ZE (bit 9)+    unsafe {+        asm!(+            "movq xmm0, [{a}]",+            "divsd xmm0, [{b}]", // 1.0 / 0.0+            "movq [{o}], xmm0",+            a = in(reg) &one, b = in(reg) &zero, o = in(reg) &mut out,+            options(nostack)+        );+    }+    let mx = get_mxcsr();+    set_mxcsr(0x1f80);+    let code = FPE_CODE.load(Ordering::SeqCst);+    if code == 0 && out == f64::INFINITY.to_bits() && mx & 4 != 0 {+        // QEMU's software CPU (TCG) sets the flag but never raises #XM.+        println!("fputest: skipped SIGFPE check: this CPU doesn't raise SIMD exceptions (QEMU TCG)");+    } else {+        c.check(code == 3, "unmasked 1.0/0.0 -> SIGFPE with si_code FPE_FLTDIV");+        c.check(out == f64::INFINITY.to_bits() && mx & (1 << 9) != 0, "the handler's MXCSR edit took effect (retry gave inf)");+    }++    c.done()+}