From b79ad1147e922f4a6bec5f5c0747c090450803d2 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 9 Sep 2026 16:47:22 -0700 Subject: [PATCH 1/6] Preserve Windows guest floating-point state --- litebox_platform_windows_userland/src/lib.rs | 923 ++++++++++++++++++- 1 file changed, 879 insertions(+), 44 deletions(-) diff --git a/litebox_platform_windows_userland/src/lib.rs b/litebox_platform_windows_userland/src/lib.rs index 400290cda..1a4afbbf3 100644 --- a/litebox_platform_windows_userland/src/lib.rs +++ b/litebox_platform_windows_userland/src/lib.rs @@ -7,7 +7,7 @@ // Windows, but we _may_ allow for more in the future, if we find it useful to do so. #![cfg(all(target_os = "windows", target_arch = "x86_64"))] -use core::cell::Cell; +use core::cell::{Cell, UnsafeCell}; use core::panic; use core::sync::atomic::{AtomicU32, Ordering}; use core::time::Duration; @@ -95,8 +95,34 @@ impl WindowsUserland { } } +/// Runs the Rust handler with host FP controls +#[unsafe(naked)] unsafe extern "system" fn vectored_exception_handler( exception_info: *mut EXCEPTION_POINTERS, +) -> i32 { + core::arch::naked_asm!( + ".seh_proc vectored_exception_entry", + "sub rsp, 40", + ".seh_stackalloc 40", + ".seh_endprologue", + "fnstcw WORD PTR [rsp + 32]", + "stmxcsr DWORD PTR [rsp + 36]", + "fldcw WORD PTR [rip + {HOST_X87_CONTROL_WORD}]", + "ldmxcsr DWORD PTR [rip + {HOST_MXCSR}]", + "call {handler}", + "fldcw WORD PTR [rsp + 32]", + "ldmxcsr DWORD PTR [rsp + 36]", + "add rsp, 40", + "ret", + ".seh_endproc", + HOST_X87_CONTROL_WORD = sym HOST_X87_CONTROL_WORD, + HOST_MXCSR = sym HOST_MXCSR, + handler = sym vectored_exception_handler_inner, + ); +} + +unsafe extern "system" fn vectored_exception_handler_inner( + exception_info: *mut EXCEPTION_POINTERS, ) -> i32 { let Some(tls) = get_tls_ptr() else { // TLS slot not initialized yet; cannot be in guest @@ -128,7 +154,7 @@ unsafe extern "system" fn vectored_exception_handler( tls.is_in_guest.set(false); let regs = unsafe { &mut *tls.guest_context_top.get().wrapping_sub(1) }; - save_guest_context(regs, context); + save_guest_context(tls, regs, context); // If it looks like fs base was cleared, then go through the interrupt path // instead of the exception path to restore the fs base and try again. @@ -168,9 +194,14 @@ unsafe extern "system" fn vectored_exception_handler( } fn save_guest_context( + tls: &TlsState, guest_context: &mut litebox_common_linux::PtRegs, context: &windows_sys::Win32::System::Diagnostics::Debug::CONTEXT, ) { + use windows_sys::Win32::System::Diagnostics::Debug::{ + CONTEXT_FLOATING_POINT_AMD64, CONTEXT_XSTATE_AMD64, CopyContext, + }; + let litebox_common_linux::PtRegs { r15, r14, @@ -213,6 +244,18 @@ fn save_guest_context( *rip = context.Rip.trunc(); *eflags = context.EFlags as usize; *rsp = context.Rsp.trunc(); + + // SAFETY: the current thread is outside the guest, or the target thread is + // suspended, so no other code can access `continue_context` concurrently. + let ok = unsafe { + CopyContext( + (*tls.continue_context.get()).as_ptr(), + CONTEXT_FLOATING_POINT_AMD64 | CONTEXT_XSTATE_AMD64, + context, + ) + }; + assert_ne!(ok, 0, "CopyContext failed"); + tls.guest_xstate_format.set(GuestXstateFormat::Windows); } impl WindowsUserland { @@ -405,11 +448,294 @@ fn run_thread_inner( ctx, tls: &tls_state, }; - ThreadHandle::run_with_handle(&tls_state, || unsafe { - run_thread_arch(&mut thread_ctx, &tls_state); + ThreadHandle::run_with_handle(&tls_state, || { + debug_assert_host_fx_control_state(); + unsafe { run_thread_arch(&mut thread_ctx, &tls_state) }; }); } +/// Windows x64 ABI default: all x87 exceptions masked, 53-bit precision, round to nearest. +/// Unlike the guest's architectural initial value (0x037f), this selects double precision. +/// See . +static HOST_X87_CONTROL_WORD: u16 = 0x027f; +static HOST_MXCSR: u32 = core::arch::x86_64::_MM_MASK_MASK; + +#[inline] +fn debug_assert_host_fx_control_state() { + #[cfg(debug_assertions)] + { + const HOST_MXCSR_CONTROL_MASK: u32 = 0xffc0; + let mut x87_control_word = 0_u16; + let mut mxcsr = 0_u32; + unsafe { + core::arch::asm!( + "fnstcw WORD PTR [{x87_control_word}]", + "stmxcsr DWORD PTR [{mxcsr}]", + x87_control_word = in(reg) &raw mut x87_control_word, + mxcsr = in(reg) &raw mut mxcsr, + options(nostack, preserves_flags), + ); + } + debug_assert_eq!(x87_control_word, HOST_X87_CONTROL_WORD); + debug_assert_eq!(mxcsr & HOST_MXCSR_CONTROL_MASK, HOST_MXCSR); + } +} + +const XSAVE_LEGACY_SIZE: usize = + size_of::(); +const XSAVE_HEADER_OFFSET: usize = XSAVE_LEGACY_SIZE; + +#[repr(C, align(64))] +#[derive(Clone)] +struct XsaveChunk([u8; 64]); + +struct XsaveLayout { + size: usize, + mask: u64, + components: Vec, +} + +struct XsaveComponent { + id: u32, + offset: usize, + size: usize, +} + +impl XsaveLayout { + fn get() -> &'static Self { + static LAYOUT: OnceLock = OnceLock::new(); + LAYOUT.get_or_init(|| { + const CPUID_XSAVE: u32 = 1 << 26; + const CPUID_OSXSAVE: u32 = 1 << 27; + + assert!(core::arch::x86_64::__cpuid(0).eax >= 0x0d); + let feature_info = core::arch::x86_64::__cpuid(1); + assert_eq!( + feature_info.ecx & (CPUID_XSAVE | CPUID_OSXSAVE), + CPUID_XSAVE | CPUID_OSXSAVE, + "XSAVE must be supported and enabled by Windows", + ); + let features = core::arch::x86_64::__cpuid_count(0x0d, 0); + let mask = unsafe { core::arch::x86_64::_xgetbv(0) }; + assert_eq!(mask & 3, 3, "x87 and SSE state must be enabled"); + assert!(features.ebx as usize >= XSAVE_HEADER_OFFSET + 64); + let components = (2..64) + .filter(|id| mask & (1 << id) != 0) + .map(|id| { + let component = core::arch::x86_64::__cpuid_count(0x0d, id); + let component = XsaveComponent { + id, + offset: component.ebx as usize, + size: component.eax as usize, + }; + assert!(component.offset + component.size <= features.ebx as usize); + component + }) + .collect(); + XsaveLayout { + size: features.ebx as usize, + mask, + components, + } + }) + } +} + +/// Represents the standard-layout XSAVE area for a guest context. +struct XsaveArea { + storage: Box<[XsaveChunk]>, +} + +impl XsaveArea { + const GUEST_INITIAL_X87_CONTROL_WORD: u16 = 0x037f; + const GUEST_INITIAL_MXCSR: u32 = core::arch::x86_64::_MM_MASK_MASK; + + fn initial_guest() -> Self { + let chunk_count = XsaveLayout::get().size.div_ceil(size_of::()); + let mut area = Self { + storage: vec![XsaveChunk([0; 64]); chunk_count].into_boxed_slice(), + }; + // SAFETY: The buffer owns aligned, initialized storage for the legacy area. + // Standard XRSTOR loads MXCSR even when XSTATE_BV marks SSE as initial. + unsafe { + (*area + .as_mut_ptr() + .cast::()) + .MxCsr = Self::GUEST_INITIAL_MXCSR; + } + area + } + + fn as_ptr(&self) -> *const u8 { + self.storage.as_ptr().cast() + } + + fn as_mut_ptr(&mut self) -> *mut u8 { + self.storage.as_mut_ptr().cast() + } + + fn xstate_bv(&self) -> u64 { + unsafe { + self.as_ptr() + .add(XSAVE_HEADER_OFFSET) + .cast::() + .read_unaligned() + } + } + + fn restore_to_context( + &self, + context: &mut windows_sys::Win32::System::Diagnostics::Debug::CONTEXT, + ) { + use windows_sys::Win32::System::Diagnostics::Debug::{ + CONTEXT_XSTATE_AMD64, LocateXStateFeature, SetXStateFeaturesMask, + }; + + let legacy_state = self.legacy_state_for_context(); + context.Anonymous.FltSave = legacy_state; + context.MxCsr = legacy_state.MxCsr; + if context.ContextFlags & CONTEXT_XSTATE_AMD64 != CONTEXT_XSTATE_AMD64 { + return; + } + + let layout = XsaveLayout::get(); + let xstate_bv = self.xstate_bv(); + for component in &layout.components { + if xstate_bv & (1 << component.id) == 0 { + continue; + } + let mut length = 0; + // SAFETY: The context has initialized XSTATE storage, checked above. + let destination = + unsafe { LocateXStateFeature(context, component.id, &raw mut length).cast::() }; + assert!(!destination.is_null()); + assert_eq!(length as usize, component.size); + // SAFETY: The component fits both disjoint buffers and is marked valid. + unsafe { + destination + .copy_from_nonoverlapping(self.as_ptr().add(component.offset), component.size); + } + } + // SAFETY: The context owns initialized XSTATE storage for the enabled features. + let ok = unsafe { SetXStateFeaturesMask(context, xstate_bv & layout.mask) }; + assert_ne!(ok, 0, "SetXStateFeaturesMask failed"); + } + + fn legacy_state_for_context( + &self, + ) -> windows_sys::Win32::System::Diagnostics::Debug::XSAVE_FORMAT { + use windows_sys::Win32::System::Diagnostics::Debug::XSAVE_FORMAT; + + let saved = unsafe { self.as_ptr().cast::().read() }; + let mut state = XSAVE_FORMAT { + ControlWord: Self::GUEST_INITIAL_X87_CONTROL_WORD, + MxCsr: saved.MxCsr, + MxCsr_Mask: saved.MxCsr_Mask, + ..Default::default() + }; + let xstate_bv = self.xstate_bv(); + if xstate_bv & 1 != 0 { + state.ControlWord = saved.ControlWord; + state.StatusWord = saved.StatusWord; + state.TagWord = saved.TagWord; + state.ErrorOpcode = saved.ErrorOpcode; + state.ErrorOffset = saved.ErrorOffset; + state.ErrorSelector = saved.ErrorSelector; + state.DataOffset = saved.DataOffset; + state.DataSelector = saved.DataSelector; + state.FloatRegisters = saved.FloatRegisters; + } + if xstate_bv & 2 != 0 { + state.XmmRegisters = saved.XmmRegisters; + } + state + } +} + +/// Represents an extended CPU context, including the XSAVE area. +struct ExtendedContext { + /// Storage for the Windows context, including the XSAVE area. + _storage: Box<[XsaveChunk]>, + /// Pointer to the Windows context (i.e., `_storage`) + context: *mut windows_sys::Win32::System::Diagnostics::Debug::CONTEXT, +} + +impl ExtendedContext { + fn new() -> Self { + static CONTEXT_LENGTH: OnceLock = OnceLock::new(); + + use windows_sys::Win32::System::Diagnostics::Debug::{ + CONTEXT_ALL_AMD64, CONTEXT_XSTATE_AMD64, InitializeContext, SetXStateFeaturesMask, + }; + + let flags = CONTEXT_ALL_AMD64 | CONTEXT_XSTATE_AMD64; + let mut context_length = *CONTEXT_LENGTH.get_or_init(|| { + let mut context_length = 0; + let mut context = core::ptr::null_mut(); + let ok = unsafe { + InitializeContext( + core::ptr::null_mut(), + flags, + &raw mut context, + &raw mut context_length, + ) + }; + assert_eq!(ok, 0); + assert_ne!(context_length, 0); + context_length + }); + + let chunk_count = (context_length as usize).div_ceil(size_of::()); + let mut storage = vec![XsaveChunk([0; 64]); chunk_count].into_boxed_slice(); + let mut context = core::ptr::null_mut(); + let ok = unsafe { + InitializeContext( + storage.as_mut_ptr().cast(), + flags, + &raw mut context, + &raw mut context_length, + ) + }; + assert_ne!(ok, 0, "InitializeContext failed"); + let ok = unsafe { SetXStateFeaturesMask(context, XsaveLayout::get().mask) }; + assert_ne!(ok, 0, "SetXStateFeaturesMask failed"); + Self { + _storage: storage, + context, + } + } + + fn as_ptr(&self) -> *mut windows_sys::Win32::System::Diagnostics::Debug::CONTEXT { + self.context + } + + fn context_mut(&mut self) -> &mut windows_sys::Win32::System::Diagnostics::Debug::CONTEXT { + unsafe { &mut *self.context } + } + + fn prepare_for_capture( + &mut self, + ) -> &mut windows_sys::Win32::System::Diagnostics::Debug::CONTEXT { + use windows_sys::Win32::System::Diagnostics::Debug::{ + CONTEXT_ALL_AMD64, CONTEXT_XSTATE_AMD64, SetXStateFeaturesMask, + }; + + let context = self.context_mut(); + context.ContextFlags = CONTEXT_ALL_AMD64 | CONTEXT_XSTATE_AMD64; + // SAFETY: The context retains its initialized extended storage across captures. + let ok = unsafe { SetXStateFeaturesMask(context, XsaveLayout::get().mask) }; + assert_ne!(ok, 0, "SetXStateFeaturesMask failed"); + context + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +enum GuestXstateFormat { + Native, + Windows, +} + static TLS_INDEX: AtomicU32 = AtomicU32::new(u32::MAX); struct TlsState { @@ -419,8 +745,13 @@ struct TlsState { scratch: Cell, is_in_guest: Cell, interrupt: Cell, - continue_context: - Box>, + continue_context: UnsafeCell, + /// Scratch storage used exclusively under the target thread-handle mutex. + interrupt_context: UnsafeCell, + guest_xsave_ptr: Cell<*mut u8>, + guest_xsave_area: UnsafeCell, + guest_xsave_mask: u64, + guest_xstate_format: Cell, /// Bitmask of pending host-originated signals for this thread. pending_host_signals: AtomicU32, /// Pointer to the `Waker` currently being waited on, or null if not @@ -431,6 +762,8 @@ struct TlsState { impl TlsState { /// Creates a new `TlsState` with all fields zeroed / defaulted. fn new() -> Self { + let mut guest_xsave_area = XsaveArea::initial_guest(); + let guest_xsave_ptr = guest_xsave_area.as_mut_ptr(); Self { host_sp: Cell::new(core::ptr::null_mut()), host_bp: Cell::new(core::ptr::null_mut()), @@ -438,7 +771,12 @@ impl TlsState { scratch: 0.into(), is_in_guest: false.into(), interrupt: false.into(), - continue_context: Box::default(), + continue_context: UnsafeCell::new(ExtendedContext::new()), + interrupt_context: UnsafeCell::new(ExtendedContext::new()), + guest_xsave_ptr: Cell::new(guest_xsave_ptr), + guest_xsave_area: UnsafeCell::new(guest_xsave_area), + guest_xsave_mask: XsaveLayout::get().mask, + guest_xstate_format: Cell::new(GuestXstateFormat::Native), pending_host_signals: AtomicU32::new(0), waiting_waker: std::sync::atomic::AtomicPtr::new(std::ptr::null_mut()), } @@ -567,7 +905,6 @@ syscall_callback: mov QWORD PTR [r11 + {SCRATCH}], rsp mov rsp, QWORD PTR [r11 + {GUEST_CONTEXT_TOP}] - // TODO: save float and vector registers (xsave or fxsave) // Save caller-saved registers push 0x2b // pt_regs->ss = __USER_DS push QWORD PTR [r11 + {SCRATCH}] // pt_regs->sp @@ -592,6 +929,16 @@ syscall_callback: push r14 push r15 + // Save guest XSTATE now that the guest GPRs are preserved. + mov eax, DWORD PTR [r11 + {XSAVE_MASK}] + mov edx, DWORD PTR [r11 + {XSAVE_MASK} + 4] + mov r10, QWORD PTR [r11 + {GUEST_XSAVE_PTR}] + xsave64 [r10] + mov BYTE PTR [r11 + {GUEST_XSTATE_FORMAT}], 0 + // Restore the Windows ABI's standard host x87 control word and mxcsr. + fldcw WORD PTR [rip + {HOST_X87_CONTROL_WORD}] + ldmxcsr DWORD PTR [rip + {HOST_MXCSR}] + /// Reestablish the stack and frame pointers. mov rsp, [r11 + {HOST_SP}] mov rbp, [r11 + {HOST_BP}] @@ -603,6 +950,9 @@ syscall_callback: jmp .Ldone exception_callback: + // Restore the Windows ABI's standard host x87 control word and mxcsr. + fldcw WORD PTR [rip + {HOST_X87_CONTROL_WORD}] + ldmxcsr DWORD PTR [rip + {HOST_MXCSR}] // Handle the exception. The stack and frame pointers are already restored, // and the guest context is up to date. rcx contains a pointer to the // guest pt_regs, and rdx contains a pointer to the exception record. @@ -614,6 +964,9 @@ interrupt_callback: mov r11, QWORD PTR gs:[r11 * 8 + TEB_TLS_SLOTS_OFFSET] mov rsp, [r11 + {HOST_SP}] mov rbp, [r11 + {HOST_BP}] + // Restore the Windows ABI's standard host x87 control word and mxcsr. + fldcw WORD PTR [rip + {HOST_X87_CONTROL_WORD}] + ldmxcsr DWORD PTR [rip + {HOST_MXCSR}] mov rcx, QWORD PTR [rsp] // thread_ctx call {interrupt_handler} jmp .Ldone @@ -653,6 +1006,11 @@ interrupt_callback: GUEST_CONTEXT_TOP = const core::mem::offset_of!(TlsState, guest_context_top), SCRATCH = const core::mem::offset_of!(TlsState, scratch), IS_IN_GUEST = const core::mem::offset_of!(TlsState, is_in_guest), + HOST_X87_CONTROL_WORD = sym HOST_X87_CONTROL_WORD, + HOST_MXCSR = sym HOST_MXCSR, + GUEST_XSAVE_PTR = const core::mem::offset_of!(TlsState, guest_xsave_ptr), + XSAVE_MASK = const core::mem::offset_of!(TlsState, guest_xsave_mask), + GUEST_XSTATE_FORMAT = const core::mem::offset_of!(TlsState, guest_xstate_format), ); } @@ -683,6 +1041,10 @@ unsafe extern "C" fn switch_to_guest(ctx: &litebox_common_linux::PtRegs) -> ! { "je 2f", "jmp {interrupt_callback}", "2:", + "mov r10, [rdx + {GUEST_XSAVE_PTR}]", + "mov eax, DWORD PTR [rdx + {XSAVE_MASK}]", + "mov edx, DWORD PTR [rdx + {XSAVE_MASK} + 4]", + "xrstor64 [r10]", // Load all registers from the guest context structure. "mov rsp, rcx", "pop r15", @@ -709,6 +1071,8 @@ unsafe extern "C" fn switch_to_guest(ctx: &litebox_common_linux::PtRegs) -> ! { "switch_to_guest_end:", IS_IN_GUEST = const core::mem::offset_of!(TlsState, is_in_guest), INTERRUPT = const core::mem::offset_of!(TlsState, interrupt), + GUEST_XSAVE_PTR = const core::mem::offset_of!(TlsState, guest_xsave_ptr), + XSAVE_MASK = const core::mem::offset_of!(TlsState, guest_xsave_mask), interrupt_callback = sym interrupt_callback, ); } @@ -716,7 +1080,7 @@ unsafe extern "C" fn switch_to_guest(ctx: &litebox_common_linux::PtRegs) -> ! { fn switch_to_guest_ntcontinue(tls: &TlsState, ctx: &litebox_common_linux::PtRegs) -> ! { use litebox::utils::ReinterpretSignedExt; use windows_sys::Win32::System::Diagnostics::Debug::{ - CONTEXT, CONTEXT_CONTROL_AMD64, CONTEXT_INTEGER_AMD64, + CONTEXT, CONTEXT_CONTROL_AMD64, CONTEXT_FLOATING_POINT_AMD64, CONTEXT_INTEGER_AMD64, }; #[link(name = "ntdll")] unsafe extern "system" { @@ -725,32 +1089,38 @@ unsafe extern "C" fn switch_to_guest(ctx: &litebox_common_linux::PtRegs) -> ! { raise_alert: u8, ) -> windows_sys::Win32::Foundation::NTSTATUS; } - let win_ctx = tls.continue_context.get(); + let win_ctx = unsafe { (*tls.continue_context.get()).as_ptr() }; + let native_xstate = tls.guest_xstate_format.get() == GuestXstateFormat::Native; // SAFETY: no other code accesses `continue_context` while `is_in_guest` is false. unsafe { - win_ctx.write(CONTEXT { - ContextFlags: CONTEXT_CONTROL_AMD64 | CONTEXT_INTEGER_AMD64, - EFlags: ctx.eflags.trunc(), - Rax: ctx.rax as u64, - Rcx: ctx.rcx as u64, - Rdx: ctx.rdx as u64, - Rbx: ctx.rbx as u64, - Rsp: ctx.rsp as u64, - Rbp: ctx.rbp as u64, - Rsi: ctx.rsi as u64, - Rdi: ctx.rdi as u64, - R8: ctx.r8 as u64, - R9: ctx.r9 as u64, - R10: ctx.r10 as u64, - R11: ctx.r11 as u64, - R12: ctx.r12 as u64, - R13: ctx.r13 as u64, - R14: ctx.r14 as u64, - R15: ctx.r15 as u64, - Rip: ctx.rip as u64, - ..CONTEXT::default() - }); + let win_ctx = &mut *win_ctx; + win_ctx.ContextFlags = CONTEXT_CONTROL_AMD64 + | CONTEXT_INTEGER_AMD64 + | CONTEXT_FLOATING_POINT_AMD64 + | windows_sys::Win32::System::Diagnostics::Debug::CONTEXT_XSTATE_AMD64; + win_ctx.EFlags = ctx.eflags.trunc(); + win_ctx.Rax = ctx.rax as u64; + win_ctx.Rcx = ctx.rcx as u64; + win_ctx.Rdx = ctx.rdx as u64; + win_ctx.Rbx = ctx.rbx as u64; + win_ctx.Rsp = ctx.rsp as u64; + win_ctx.Rbp = ctx.rbp as u64; + win_ctx.Rsi = ctx.rsi as u64; + win_ctx.Rdi = ctx.rdi as u64; + win_ctx.R8 = ctx.r8 as u64; + win_ctx.R9 = ctx.r9 as u64; + win_ctx.R10 = ctx.r10 as u64; + win_ctx.R11 = ctx.r11 as u64; + win_ctx.R12 = ctx.r12 as u64; + win_ctx.R13 = ctx.r13 as u64; + win_ctx.R14 = ctx.r14 as u64; + win_ctx.R15 = ctx.r15 as u64; + win_ctx.Rip = ctx.rip as u64; + if native_xstate { + (*tls.guest_xsave_area.get()).restore_to_context(win_ctx); + } } + tls.guest_xstate_format.set(GuestXstateFormat::Windows); // Ensure the context is written before we set `is_in_guest` so that // `ThreadHandle::interrupt` can see a consistent state. std::sync::atomic::compiler_fence(Ordering::Release); @@ -792,7 +1162,7 @@ unsafe extern "C" fn switch_to_guest(ctx: &litebox_common_linux::PtRegs) -> ! { // // This is much slower, but it is only used for things like signal handlers, // so it should not be on the critical path. - if ctx.rcx == ctx.rip { + if tls.guest_xstate_format.get() == GuestXstateFormat::Native && ctx.rcx == ctx.rip { switch_to_guest_sysret(ctx, tls) } else { switch_to_guest_ntcontinue(tls, ctx) @@ -1173,15 +1543,14 @@ impl ThreadHandle { // 4. In the guest. Save the guest context and jump to the interrupt callback. // Get the current register context. - let mut context = windows_sys::Win32::System::Diagnostics::Debug::CONTEXT { - ContextFlags: windows_sys::Win32::System::Diagnostics::Debug::CONTEXT_CONTROL_AMD64 - | windows_sys::Win32::System::Diagnostics::Debug::CONTEXT_INTEGER_AMD64, - ..Default::default() - }; + // SAFETY: The target thread-handle mutex serializes all users of this + // scratch buffer. It is separate from the saved guest continue_context. + let extended_context = unsafe { &mut *target_tls.interrupt_context.get() }; + let context = extended_context.prepare_for_capture(); let r = unsafe { windows_sys::Win32::System::Diagnostics::Debug::GetThreadContext( inner.handle.as_raw_handle(), - &raw mut context, + context, ) }; assert_ne!( @@ -1209,20 +1578,23 @@ impl ThreadHandle { // SAFETY: `continue_context` is not accessed by user-mode code // while `is_in_guest` is true. - let continue_context = unsafe { &mut *target_tls.continue_context.get() }; - set_context_to_interrupt_callback(continue_context); + let continue_context = unsafe { (*target_tls.continue_context.get()).as_ptr() }; + set_context_to_interrupt_callback(unsafe { &mut *continue_context }); false } else { // Case 4: save the guest context and jump to interrupt callback. - save_guest_context(unsafe { &mut *guest_context }, &context); + save_guest_context(target_tls, unsafe { &mut *guest_context }, context); true }; if run_interrupt_callback { - set_context_to_interrupt_callback(&mut context); + set_context_to_interrupt_callback(context); + context.ContextFlags = + windows_sys::Win32::System::Diagnostics::Debug::CONTEXT_CONTROL_AMD64; + // SAFETY: The target is suspended and only its control state is changed. unsafe { windows_sys::Win32::System::Diagnostics::Debug::SetThreadContext( inner.handle.as_raw_handle(), - &raw const context, + context, ); } } @@ -2116,12 +2488,475 @@ mod tests { use crate::WindowsUserland; use crate::process_memory_range_by_regions; + use crate::{XsaveArea, XsaveLayout}; use litebox::platform::PageManagementProvider; use litebox::platform::RawConstPointer; use litebox::platform::RawMutex; use litebox::platform::page_mgmt::FixedAddressBehavior; use litebox::platform::page_mgmt::MemoryRegionPermissions; + #[test] + fn interrupt_capture_preserves_xstate_on_reuse() { + use litebox::shim::{ContinueOperation, EnterShim, ExceptionInfo}; + use litebox_common_linux::PtRegs; + use std::cell::Cell; + use std::sync::atomic::Ordering; + use windows_sys::Win32::System::Memory::{ + MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_EXECUTE_READ, PAGE_READWRITE, VirtualAlloc, + VirtualFree, VirtualProtect, + }; + + static TEST_VECTOR: [u8; 32] = [0x5a; 32]; + static TEST_NEXT_VECTOR: [u8; 32] = [0x3c; 32]; + static TEST_MXCSR: u32 = 0x3f80; + + #[unsafe(naked)] + unsafe extern "C" fn guest_entry() { + core::arch::naked_asm!( + "ldmxcsr [rip + {mxcsr}]", + "movdqu xmm0, [rip + {vector}]", + "test rsi, rsi", + "jz 2f", + "vmovdqu ymm0, [rip + {vector}]", + "2:", + "jmp rbx", + mxcsr = sym TEST_MXCSR, + vector = sym TEST_VECTOR, + ); + } + + #[unsafe(naked)] + unsafe extern "C" fn guest_after_interrupt() { + core::arch::naked_asm!( + // Change the state before reusing the interrupt capture buffer. + "movdqu xmm0, [rip + {vector}]", + "test rsi, rsi", + "jz 2f", + "vmovdqu ymm0, [rip + {vector}]", + "2:", + "jmp rbx", + vector = sym TEST_NEXT_VECTOR, + ); + } + + #[unsafe(naked)] + unsafe extern "C" fn guest_stop() { + core::arch::naked_asm!( + "jmp {syscall_callback}", + syscall_callback = sym crate::syscall_callback, + ); + } + + struct InterruptShim { + count: Cell, + avx: bool, + } + + impl EnterShim for InterruptShim { + type ExecutionContext = PtRegs; + + fn init(&self, _ctx: &mut PtRegs) -> ContinueOperation { + ContinueOperation::Resume + } + + fn syscall(&self, _ctx: &mut PtRegs) -> ContinueOperation { + ContinueOperation::Terminate + } + + fn exception(&self, _ctx: &mut PtRegs, info: &ExceptionInfo) -> ContinueOperation { + panic!("unexpected guest exception: {info:?}"); + } + + fn interrupt(&self, ctx: &mut PtRegs) -> ContinueOperation { + let expected = [0x5a, 0x3c][self.count.get()]; + // SAFETY: The target has returned to the host; its saved context + // is not concurrently accessible while is_in_guest is false. + let context = unsafe { + &mut *(*(*crate::get_tls_ptr().unwrap()).continue_context.get()).as_ptr() + }; + assert_eq!(context.MxCsr, TEST_MXCSR); + // SAFETY: GetThreadContext and CopyContext initialized FltSave. + let legacy = unsafe { context.Anonymous.FltSave }; + assert_eq!( + legacy.XmmRegisters[0].Low, + u64::from_le_bytes([expected; 8]) + ); + assert_eq!( + legacy.XmmRegisters[0].High.cast_unsigned(), + u64::from_le_bytes([expected; 8]) + ); + if self.avx { + let mut length = 0; + // SAFETY: The saved context has initialized AVX storage. + let upper = unsafe { + windows_sys::Win32::System::Diagnostics::Debug::LocateXStateFeature( + context, + 2, + &raw mut length, + ) + .cast::() + }; + assert!(!upper.is_null() && length >= 16); + // SAFETY: The AVX component contains at least 16 initialized bytes. + assert_eq!( + unsafe { core::slice::from_raw_parts(upper, 16) }, + [expected; 16] + ); + } + let count = self.count.get() + 1; + self.count.set(count); + if count == 2 { + return ContinueOperation::Terminate; + } + ctx.rip = ctx.r12; + ContinueOperation::Resume + } + } + + // Place the spin loop outside this module so `is_in_ntdll_or_this` + // classifies its RIP as guest code and `interrupt` exercises case 4, + // which captures the live guest context and XSTATE. + // SAFETY: Allocate a private page, populate it while writable, then make + // it executable before starting the worker. The page outlives the worker. + let code = unsafe { + VirtualAlloc( + core::ptr::null(), + 4096, + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE, + ) + }; + assert!(!code.is_null()); + let _free = litebox::utils::defer(|| { + // SAFETY: The worker is joined before the uniquely owned page is freed. + assert_ne!(unsafe { VirtualFree(code, 0, MEM_RELEASE) }, 0); + }); + // mov dword ptr [rdi], 1; pause; cmp dword ptr [r14], 0; je pause; jmp r13. + let instructions = [ + 0xc7_u8, 0x07, 1, 0, 0, 0, 0xf3, 0x90, 0x41, 0x83, 0x3e, 0, 0x74, 0xf8, 0x41, 0xff, + 0xe5, + ]; + // SAFETY: The page is writable and large enough for these instructions. + unsafe { + code.cast::() + .copy_from_nonoverlapping(instructions.as_ptr(), instructions.len()); + }; + let mut old_protection = 0; + // SAFETY: The page is exclusively owned and the worker has not started. + assert_ne!( + unsafe { VirtualProtect(code, 4096, PAGE_EXECUTE_READ, &raw mut old_protection) }, + 0 + ); + // SAFETY: Flush the newly populated executable page before running it. + assert_ne!( + unsafe { + windows_sys::Win32::System::Diagnostics::Debug::FlushInstructionCache( + crate::GetCurrentProcess(), + code, + instructions.len(), + ) + }, + 0 + ); + + let code_address = code.addr(); + // Capture 0x5a and then 0x3c into the same interrupt scratch context. + let ready = AtomicU32::new(0); + let stop = AtomicU32::new(0); + std::thread::scope(|scope| { + let _stop_worker = litebox::utils::defer(|| stop.store(1, Ordering::Release)); + let timeout = std::time::Duration::from_secs(5); + let deadline = std::time::Instant::now() + timeout; + let (sender, receiver) = std::sync::mpsc::sync_channel(1); + let ready_ref = &ready; + let stop_ref = &stop; + let worker = scope.spawn(move || { + crate::ensure_tls_index(); + let shim = InterruptShim { + count: Cell::new(0), + avx: std::is_x86_feature_detected!("avx"), + }; + let mut stack = [0_u128; 256]; + let entry = guest_entry as *const () as usize; + let mut ctx = PtRegs { + rip: entry, + rcx: entry, + rbx: code_address, + rdi: core::ptr::from_ref(ready_ref).addr(), + rsi: usize::from(shim.avx), + r12: guest_after_interrupt as *const () as usize, + r13: guest_stop as *const () as usize, + r14: core::ptr::from_ref(stop_ref).addr(), + rsp: stack.as_mut_ptr().wrapping_add(stack.len()).addr(), + eflags: 0x202, + ..Default::default() + }; + let tls = crate::TlsState::new(); + tls.guest_context_top + .set(core::ptr::from_mut(&mut ctx).wrapping_add(1)); + let mut thread_ctx = crate::ThreadContext { + shim: &shim, + ctx: &mut ctx, + tls: &tls, + }; + crate::ThreadHandle::run_with_handle(&tls, || { + sender + .send( + crate::CURRENT_THREAD_HANDLE + .with_borrow(|handle| handle.clone().unwrap()), + ) + .unwrap(); + // SAFETY: The worker owns a live guest stack and TLS until termination. + unsafe { crate::run_thread_arch(&mut thread_ctx, &tls) }; + }); + shim.count.get() + }); + let handle = receiver.recv_timeout(timeout).expect("guest did not start"); + for _ in 0..2 { + // The trampoline publishes readiness only after the new XSTATE + // value is live, so each capture has a deterministic expectation. + while ready.swap(0, Ordering::Acquire) == 0 { + assert!( + !worker.is_finished(), + "guest exited before signaling readiness" + ); + assert!( + std::time::Instant::now() < deadline, + "guest readiness timed out" + ); + std::thread::yield_now(); + } + handle.interrupt(None); + } + while !worker.is_finished() { + assert!(std::time::Instant::now() < deadline, "guest exit timed out"); + std::thread::yield_now(); + } + assert_eq!(worker.join().unwrap(), 2); + }); + } + + #[test] + fn xsave_syscalls_preserve_state_across_resume_paths() { + use litebox::shim::{ContinueOperation, EnterShim, ExceptionInfo}; + use litebox_common_linux::PtRegs; + use std::cell::Cell; + + static TEST_CW: u16 = 0x077f; + static TEST_MXCSR: u32 = 0x3f80; + static TEST_VECTOR: [u8; 32] = [0x5a; 32]; + + #[unsafe(naked)] + unsafe extern "C" fn guest_entry() { + core::arch::naked_asm!( + "stmxcsr [rsi]", + "fnstcw [rsi + 4]", + "fldcw [rip + {control_word}]", + "ldmxcsr [rip + {mxcsr}]", + "movdqu xmm0, [rip + {vector}]", + "test rdi, rdi", + "jz 2f", + "vmovdqu ymm0, [rip + {vector}]", + "2:", + "lea rcx, [rip + 3f]", + "jmp {syscall_callback}", + "3:", + "lea rcx, [rip + 4f]", + "jmp {syscall_callback}", + "4:", + "lea rcx, [rip + 5f]", + "jmp {syscall_callback}", + "5:", + "pxor xmm0, xmm0", + "test rdi, rdi", + "jz 6f", + "vzeroall", + "6:", + "lea rcx, [rip + 7f]", + "jmp {syscall_callback}", + "7:", + "lea rcx, [rip + 8f]", + "jmp {syscall_callback}", + "8:", + "lea rcx, [rip + 9f]", + "jmp {syscall_callback}", + "9:", + "ud2", + control_word = sym TEST_CW, + mxcsr = sym TEST_MXCSR, + vector = sym TEST_VECTOR, + syscall_callback = sym crate::syscall_callback, + ); + } + + struct StateShim { + calls: Cell, + avx: bool, + } + + impl EnterShim for StateShim { + type ExecutionContext = PtRegs; + + fn init(&self, _ctx: &mut PtRegs) -> ContinueOperation { + ContinueOperation::Resume + } + + fn syscall(&self, ctx: &mut PtRegs) -> ContinueOperation { + let call = self.calls.get() + 1; + self.calls.set(call); + // SAFETY: This is the active guest's host callback; capture has finished. + let area = unsafe { &*(*crate::get_tls_ptr().unwrap()).guest_xsave_area.get() }; + let legacy = area.legacy_state_for_context(); + assert_eq!(legacy.ControlWord, TEST_CW); + assert_eq!(legacy.MxCsr, TEST_MXCSR); + let expected = if call >= 4 { 0 } else { 0x5a }; + assert_eq!( + legacy.XmmRegisters[0].Low, + u64::from_le_bytes([expected; 8]) + ); + assert_eq!( + legacy.XmmRegisters[0].High.cast_unsigned(), + u64::from_le_bytes([expected; 8]) + ); + if self.avx { + let component = XsaveLayout::get() + .components + .iter() + .find(|component| component.id == 2) + .unwrap(); + if area.xstate_bv() & 4 != 0 { + // SAFETY: The enabled AVX component contains YMM0's upper 16 bytes. + let upper = unsafe { + core::slice::from_raw_parts(area.as_ptr().add(component.offset), 16) + }; + assert_eq!(upper, [expected; 16]); + } else { + assert_eq!(expected, 0); + } + } + if call == 2 || call == 4 { + // Force the guest to take the slower `NtContinue` resume path. + ctx.rcx = 0; + } + if call == 6 { + ContinueOperation::Terminate + } else { + ContinueOperation::Resume + } + } + + fn exception(&self, _ctx: &mut PtRegs, info: &ExceptionInfo) -> ContinueOperation { + panic!("unexpected guest exception: {info:?}"); + } + + fn interrupt(&self, _ctx: &mut PtRegs) -> ContinueOperation { + ContinueOperation::Resume + } + } + + for fast_entry in [false, true] { + let shim = StateShim { + calls: Cell::new(0), + avx: std::is_x86_feature_detected!("avx"), + }; + let mut stack = [0_u128; 256]; + let mut initial_controls = [0_u32; 2]; + let entry = guest_entry as *const () as usize; + let mut ctx = PtRegs { + rip: entry, + rcx: if fast_entry { entry } else { 0 }, + rsp: stack.as_mut_ptr().wrapping_add(stack.len()).addr(), + rsi: initial_controls.as_mut_ptr().addr(), + rdi: usize::from(shim.avx), + eflags: 0x202, + ..Default::default() + }; + crate::ensure_tls_index(); + crate::run_thread_inner(&shim, &mut ctx); + assert_eq!(shim.calls.get(), 6); + assert_eq!( + initial_controls, + [ + XsaveArea::GUEST_INITIAL_MXCSR, + u32::from(XsaveArea::GUEST_INITIAL_X87_CONTROL_WORD) + ] + ); + } + } + + #[test] + #[ignore = "measures XSAVE guest syscall round trips; run with --release"] + fn benchmark_xsave_syscall_round_trip() { + use litebox::shim::{ContinueOperation, EnterShim, ExceptionInfo}; + use litebox_common_linux::PtRegs; + use std::cell::Cell; + + #[unsafe(naked)] + unsafe extern "C" fn guest_entry() { + core::arch::naked_asm!( + "2:", + "lea rcx, [rip + 2b]", + "jmp {syscall_callback}", + syscall_callback = sym crate::syscall_callback, + ); + } + + struct BenchmarkShim(Cell); + + impl EnterShim for BenchmarkShim { + type ExecutionContext = PtRegs; + + fn init(&self, _ctx: &mut PtRegs) -> ContinueOperation { + ContinueOperation::Resume + } + + fn syscall(&self, _ctx: &mut PtRegs) -> ContinueOperation { + self.0.set(self.0.get() - 1); + if self.0.get() == 0 { + ContinueOperation::Terminate + } else { + ContinueOperation::Resume + } + } + + fn exception(&self, _ctx: &mut PtRegs, info: &ExceptionInfo) -> ContinueOperation { + panic!("unexpected guest exception: {info:?}"); + } + + fn interrupt(&self, _ctx: &mut PtRegs) -> ContinueOperation { + ContinueOperation::Resume + } + } + + const ITERATIONS: usize = 20_000; + crate::ensure_tls_index(); + let mut samples = Vec::new(); + for sample in 0..8 { + let shim = BenchmarkShim(Cell::new(ITERATIONS)); + let mut stack = [0_u128; 256]; + let entry = guest_entry as *const () as usize; + let mut ctx = PtRegs { + rip: entry, + rcx: entry, + rsp: stack.as_mut_ptr().wrapping_add(stack.len()).addr(), + eflags: 0x202, + ..Default::default() + }; + let start = std::time::Instant::now(); + crate::run_thread_inner(&shim, &mut ctx); + let elapsed = start.elapsed().as_nanos(); + assert_eq!(shim.0.get(), 0); + if sample != 0 { + samples.push(elapsed); + } + } + samples.sort_unstable(); + println!( + "XSAVE: median {} ns/round trip over {ITERATIONS} syscalls", + samples[samples.len() / 2] / ITERATIONS as u128 + ); + } + #[test] fn test_raw_mutex() { let mutex = std::sync::Arc::new(super::RawMutex { From b6259e336e354ad71d93a1205f5b2426760a79de Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 9 Sep 2026 16:53:57 -0700 Subject: [PATCH 2/6] remove one test --- litebox_platform_windows_userland/src/lib.rs | 73 -------------------- 1 file changed, 73 deletions(-) diff --git a/litebox_platform_windows_userland/src/lib.rs b/litebox_platform_windows_userland/src/lib.rs index 1a4afbbf3..66277afd3 100644 --- a/litebox_platform_windows_userland/src/lib.rs +++ b/litebox_platform_windows_userland/src/lib.rs @@ -2884,79 +2884,6 @@ mod tests { } } - #[test] - #[ignore = "measures XSAVE guest syscall round trips; run with --release"] - fn benchmark_xsave_syscall_round_trip() { - use litebox::shim::{ContinueOperation, EnterShim, ExceptionInfo}; - use litebox_common_linux::PtRegs; - use std::cell::Cell; - - #[unsafe(naked)] - unsafe extern "C" fn guest_entry() { - core::arch::naked_asm!( - "2:", - "lea rcx, [rip + 2b]", - "jmp {syscall_callback}", - syscall_callback = sym crate::syscall_callback, - ); - } - - struct BenchmarkShim(Cell); - - impl EnterShim for BenchmarkShim { - type ExecutionContext = PtRegs; - - fn init(&self, _ctx: &mut PtRegs) -> ContinueOperation { - ContinueOperation::Resume - } - - fn syscall(&self, _ctx: &mut PtRegs) -> ContinueOperation { - self.0.set(self.0.get() - 1); - if self.0.get() == 0 { - ContinueOperation::Terminate - } else { - ContinueOperation::Resume - } - } - - fn exception(&self, _ctx: &mut PtRegs, info: &ExceptionInfo) -> ContinueOperation { - panic!("unexpected guest exception: {info:?}"); - } - - fn interrupt(&self, _ctx: &mut PtRegs) -> ContinueOperation { - ContinueOperation::Resume - } - } - - const ITERATIONS: usize = 20_000; - crate::ensure_tls_index(); - let mut samples = Vec::new(); - for sample in 0..8 { - let shim = BenchmarkShim(Cell::new(ITERATIONS)); - let mut stack = [0_u128; 256]; - let entry = guest_entry as *const () as usize; - let mut ctx = PtRegs { - rip: entry, - rcx: entry, - rsp: stack.as_mut_ptr().wrapping_add(stack.len()).addr(), - eflags: 0x202, - ..Default::default() - }; - let start = std::time::Instant::now(); - crate::run_thread_inner(&shim, &mut ctx); - let elapsed = start.elapsed().as_nanos(); - assert_eq!(shim.0.get(), 0); - if sample != 0 { - samples.push(elapsed); - } - } - samples.sort_unstable(); - println!( - "XSAVE: median {} ns/round trip over {ITERATIONS} syscalls", - samples[samples.len() / 2] / ITERATIONS as u128 - ); - } - #[test] fn test_raw_mutex() { let mutex = std::sync::Arc::new(super::RawMutex { From 9f226a1d793460fdc96f106a954858dc28eb5962 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 9 Sep 2026 17:07:08 -0700 Subject: [PATCH 3/6] fix ratchet --- dev_tests/src/ratchet.rs | 2 +- litebox_platform_windows_userland/src/lib.rs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index acdd9cea2..86aef3e09 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -38,7 +38,7 @@ fn ratchet_globals() -> Result<()> { ("litebox_platform_linux_kernel/", 6), ("litebox_platform_linux_userland/", 5), ("litebox_platform_lvbs/", 22), - ("litebox_platform_windows_userland/", 8), + ("litebox_platform_windows_userland/", 10), ("litebox_runner_lvbs/", 8), ("litebox_runner_snp/", 2), ("litebox_shim_linux/", 1), diff --git a/litebox_platform_windows_userland/src/lib.rs b/litebox_platform_windows_userland/src/lib.rs index 66277afd3..bdf2e443a 100644 --- a/litebox_platform_windows_userland/src/lib.rs +++ b/litebox_platform_windows_userland/src/lib.rs @@ -457,8 +457,8 @@ fn run_thread_inner( /// Windows x64 ABI default: all x87 exceptions masked, 53-bit precision, round to nearest. /// Unlike the guest's architectural initial value (0x037f), this selects double precision. /// See . -static HOST_X87_CONTROL_WORD: u16 = 0x027f; -static HOST_MXCSR: u32 = core::arch::x86_64::_MM_MASK_MASK; +const HOST_X87_CONTROL_WORD: u16 = 0x027f; +const HOST_MXCSR: u32 = core::arch::x86_64::_MM_MASK_MASK; #[inline] fn debug_assert_host_fx_control_state() { @@ -2506,9 +2506,9 @@ mod tests { VirtualFree, VirtualProtect, }; - static TEST_VECTOR: [u8; 32] = [0x5a; 32]; - static TEST_NEXT_VECTOR: [u8; 32] = [0x3c; 32]; - static TEST_MXCSR: u32 = 0x3f80; + const TEST_VECTOR: [u8; 32] = [0x5a; 32]; + const TEST_NEXT_VECTOR: [u8; 32] = [0x3c; 32]; + const TEST_MXCSR: u32 = 0x3f80; #[unsafe(naked)] unsafe extern "C" fn guest_entry() { @@ -2742,9 +2742,9 @@ mod tests { use litebox_common_linux::PtRegs; use std::cell::Cell; - static TEST_CW: u16 = 0x077f; - static TEST_MXCSR: u32 = 0x3f80; - static TEST_VECTOR: [u8; 32] = [0x5a; 32]; + const TEST_CW: u16 = 0x077f; + const TEST_MXCSR: u32 = 0x3f80; + const TEST_VECTOR: [u8; 32] = [0x5a; 32]; #[unsafe(naked)] unsafe extern "C" fn guest_entry() { From 0172f97c5d424a15c7571c1c8463f28e00368835 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 9 Sep 2026 17:11:48 -0700 Subject: [PATCH 4/6] fix fmt --- litebox_platform_windows_userland/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litebox_platform_windows_userland/src/lib.rs b/litebox_platform_windows_userland/src/lib.rs index bdf2e443a..57f8ae82e 100644 --- a/litebox_platform_windows_userland/src/lib.rs +++ b/litebox_platform_windows_userland/src/lib.rs @@ -457,7 +457,7 @@ fn run_thread_inner( /// Windows x64 ABI default: all x87 exceptions masked, 53-bit precision, round to nearest. /// Unlike the guest's architectural initial value (0x037f), this selects double precision. /// See . -const HOST_X87_CONTROL_WORD: u16 = 0x027f; +const HOST_X87_CONTROL_WORD: u16 = 0x027f; const HOST_MXCSR: u32 = core::arch::x86_64::_MM_MASK_MASK; #[inline] From 597c787622d0e8ae80cedf0a204982e70434d703 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Wed, 9 Sep 2026 17:23:55 -0700 Subject: [PATCH 5/6] fix ratchet --- dev_tests/src/ratchet.rs | 2 +- litebox_platform_windows_userland/src/lib.rs | 64 ++++++++++++++------ 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index 86aef3e09..41dc17bb9 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -38,7 +38,7 @@ fn ratchet_globals() -> Result<()> { ("litebox_platform_linux_kernel/", 6), ("litebox_platform_linux_userland/", 5), ("litebox_platform_lvbs/", 22), - ("litebox_platform_windows_userland/", 10), + ("litebox_platform_windows_userland/", 12), ("litebox_runner_lvbs/", 8), ("litebox_runner_snp/", 2), ("litebox_shim_linux/", 1), diff --git a/litebox_platform_windows_userland/src/lib.rs b/litebox_platform_windows_userland/src/lib.rs index 57f8ae82e..79134938a 100644 --- a/litebox_platform_windows_userland/src/lib.rs +++ b/litebox_platform_windows_userland/src/lib.rs @@ -457,8 +457,8 @@ fn run_thread_inner( /// Windows x64 ABI default: all x87 exceptions masked, 53-bit precision, round to nearest. /// Unlike the guest's architectural initial value (0x037f), this selects double precision. /// See . -const HOST_X87_CONTROL_WORD: u16 = 0x027f; -const HOST_MXCSR: u32 = core::arch::x86_64::_MM_MASK_MASK; +static HOST_X87_CONTROL_WORD: u16 = 0x027f; +static HOST_MXCSR: u32 = core::arch::x86_64::_MM_MASK_MASK; #[inline] fn debug_assert_host_fx_control_state() { @@ -2506,22 +2506,30 @@ mod tests { VirtualFree, VirtualProtect, }; - const TEST_VECTOR: [u8; 32] = [0x5a; 32]; - const TEST_NEXT_VECTOR: [u8; 32] = [0x3c; 32]; + const TEST_VECTOR_QWORD: u64 = 0x5a5a_5a5a_5a5a_5a5a; + const TEST_NEXT_VECTOR_QWORD: u64 = 0x3c3c_3c3c_3c3c_3c3c; const TEST_MXCSR: u32 = 0x3f80; #[unsafe(naked)] unsafe extern "C" fn guest_entry() { core::arch::naked_asm!( - "ldmxcsr [rip + {mxcsr}]", - "movdqu xmm0, [rip + {vector}]", + "sub rsp, 40", + "mov DWORD PTR [rsp + 32], {mxcsr}", + "mov rax, {vector}", + "mov QWORD PTR [rsp], rax", + "mov QWORD PTR [rsp + 8], rax", + "mov QWORD PTR [rsp + 16], rax", + "mov QWORD PTR [rsp + 24], rax", + "ldmxcsr [rsp + 32]", + "movdqu xmm0, [rsp]", "test rsi, rsi", "jz 2f", - "vmovdqu ymm0, [rip + {vector}]", + "vmovdqu ymm0, [rsp]", "2:", + "add rsp, 40", "jmp rbx", - mxcsr = sym TEST_MXCSR, - vector = sym TEST_VECTOR, + mxcsr = const TEST_MXCSR, + vector = const TEST_VECTOR_QWORD, ); } @@ -2529,13 +2537,20 @@ mod tests { unsafe extern "C" fn guest_after_interrupt() { core::arch::naked_asm!( // Change the state before reusing the interrupt capture buffer. - "movdqu xmm0, [rip + {vector}]", + "sub rsp, 32", + "mov rax, {vector}", + "mov QWORD PTR [rsp], rax", + "mov QWORD PTR [rsp + 8], rax", + "mov QWORD PTR [rsp + 16], rax", + "mov QWORD PTR [rsp + 24], rax", + "movdqu xmm0, [rsp]", "test rsi, rsi", "jz 2f", - "vmovdqu ymm0, [rip + {vector}]", + "vmovdqu ymm0, [rsp]", "2:", + "add rsp, 32", "jmp rbx", - vector = sym TEST_NEXT_VECTOR, + vector = const TEST_NEXT_VECTOR_QWORD, ); } @@ -2744,20 +2759,29 @@ mod tests { const TEST_CW: u16 = 0x077f; const TEST_MXCSR: u32 = 0x3f80; - const TEST_VECTOR: [u8; 32] = [0x5a; 32]; + const TEST_VECTOR_QWORD: u64 = 0x5a5a_5a5a_5a5a_5a5a; #[unsafe(naked)] unsafe extern "C" fn guest_entry() { core::arch::naked_asm!( "stmxcsr [rsi]", "fnstcw [rsi + 4]", - "fldcw [rip + {control_word}]", - "ldmxcsr [rip + {mxcsr}]", - "movdqu xmm0, [rip + {vector}]", + "sub rsp, 40", + "mov WORD PTR [rsp + 32], {control_word}", + "mov DWORD PTR [rsp + 36], {mxcsr}", + "mov rax, {vector}", + "mov QWORD PTR [rsp], rax", + "mov QWORD PTR [rsp + 8], rax", + "mov QWORD PTR [rsp + 16], rax", + "mov QWORD PTR [rsp + 24], rax", + "fldcw [rsp + 32]", + "ldmxcsr [rsp + 36]", + "movdqu xmm0, [rsp]", "test rdi, rdi", "jz 2f", - "vmovdqu ymm0, [rip + {vector}]", + "vmovdqu ymm0, [rsp]", "2:", + "add rsp, 40", "lea rcx, [rip + 3f]", "jmp {syscall_callback}", "3:", @@ -2782,9 +2806,9 @@ mod tests { "jmp {syscall_callback}", "9:", "ud2", - control_word = sym TEST_CW, - mxcsr = sym TEST_MXCSR, - vector = sym TEST_VECTOR, + control_word = const TEST_CW, + mxcsr = const TEST_MXCSR, + vector = const TEST_VECTOR_QWORD, syscall_callback = sym crate::syscall_callback, ); } From 4b62e4713202ce8ba53cb040b8f4b1fdf4c1bbb6 Mon Sep 17 00:00:00 2001 From: Weiteng Chen Date: Fri, 11 Sep 2026 12:24:38 -0700 Subject: [PATCH 6/6] Fix pending x87 exceptions on guest exit --- litebox_platform_windows_userland/src/lib.rs | 32 +++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/litebox_platform_windows_userland/src/lib.rs b/litebox_platform_windows_userland/src/lib.rs index 79134938a..caa84907b 100644 --- a/litebox_platform_windows_userland/src/lib.rs +++ b/litebox_platform_windows_userland/src/lib.rs @@ -107,6 +107,8 @@ unsafe extern "system" fn vectored_exception_handler( ".seh_endprologue", "fnstcw WORD PTR [rsp + 32]", "stmxcsr DWORD PTR [rsp + 36]", + // Clear the x87 exception flags and restore the host FP control state + "fnclex", "fldcw WORD PTR [rip + {HOST_X87_CONTROL_WORD}]", "ldmxcsr DWORD PTR [rip + {HOST_MXCSR}]", "call {handler}", @@ -935,7 +937,8 @@ syscall_callback: mov r10, QWORD PTR [r11 + {GUEST_XSAVE_PTR}] xsave64 [r10] mov BYTE PTR [r11 + {GUEST_XSTATE_FORMAT}], 0 - // Restore the Windows ABI's standard host x87 control word and mxcsr. + // Clear the x87 exception flags and restore the Windows ABI's standard host x87 control word and mxcsr. + fnclex fldcw WORD PTR [rip + {HOST_X87_CONTROL_WORD}] ldmxcsr DWORD PTR [rip + {HOST_MXCSR}] @@ -950,7 +953,8 @@ syscall_callback: jmp .Ldone exception_callback: - // Restore the Windows ABI's standard host x87 control word and mxcsr. + // Clear the x87 exception flags and restore the Windows ABI's standard host x87 control word and mxcsr. + fnclex fldcw WORD PTR [rip + {HOST_X87_CONTROL_WORD}] ldmxcsr DWORD PTR [rip + {HOST_MXCSR}] // Handle the exception. The stack and frame pointers are already restored, @@ -964,7 +968,8 @@ interrupt_callback: mov r11, QWORD PTR gs:[r11 * 8 + TEB_TLS_SLOTS_OFFSET] mov rsp, [r11 + {HOST_SP}] mov rbp, [r11 + {HOST_BP}] - // Restore the Windows ABI's standard host x87 control word and mxcsr. + // Clear the x87 exception flags and restore the Windows ABI's standard host x87 control word and mxcsr. + fnclex fldcw WORD PTR [rip + {HOST_X87_CONTROL_WORD}] ldmxcsr DWORD PTR [rip + {HOST_MXCSR}] mov rcx, QWORD PTR [rsp] // thread_ctx @@ -2757,9 +2762,11 @@ mod tests { use litebox_common_linux::PtRegs; use std::cell::Cell; - const TEST_CW: u16 = 0x077f; + const TEST_CW: u16 = 0x077e; const TEST_MXCSR: u32 = 0x3f80; const TEST_VECTOR_QWORD: u64 = 0x5a5a_5a5a_5a5a_5a5a; + const X87_STATUS_INVALID_OPERATION: u16 = 1 << 0; + const X87_STATUS_EXCEPTION_SUMMARY: u16 = 1 << 7; #[unsafe(naked)] unsafe extern "C" fn guest_entry() { @@ -2781,6 +2788,10 @@ mod tests { "jz 2f", "vmovdqu ymm0, [rsp]", "2:", + // Leave an unmasked invalid-operation exception pending. + "fldz", + "fldz", + "fdivp st(1), st(0)", "add rsp, 40", "lea rcx, [rip + 3f]", "jmp {syscall_callback}", @@ -2826,12 +2837,25 @@ mod tests { } fn syscall(&self, ctx: &mut PtRegs) -> ContinueOperation { + let pending_status = X87_STATUS_INVALID_OPERATION | X87_STATUS_EXCEPTION_SUMMARY; + let host_status: u16; + // SAFETY: FNSTSW only reads the current thread's x87 status word. + unsafe { + core::arch::asm!( + "fnstsw ax", + lateout("ax") host_status, + options(nomem, nostack, preserves_flags), + ); + } + assert_eq!(host_status & pending_status, 0); + let call = self.calls.get() + 1; self.calls.set(call); // SAFETY: This is the active guest's host callback; capture has finished. let area = unsafe { &*(*crate::get_tls_ptr().unwrap()).guest_xsave_area.get() }; let legacy = area.legacy_state_for_context(); assert_eq!(legacy.ControlWord, TEST_CW); + assert_eq!(legacy.StatusWord & pending_status, pending_status); assert_eq!(legacy.MxCsr, TEST_MXCSR); let expected = if call >= 4 { 0 } else { 0x5a }; assert_eq!(