From 51d9c696efa418e807a8c4b687fb78bb192ba8d2 Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Mon, 5 Oct 2026 13:18:40 +0200 Subject: [PATCH] fix: clear DF before calling exception handler Exception and interrupt gates do not clear RFLAGS.DF, so a guest exception raised with DF set entered hl_exception_handler with DF still set. The x86-64 ABI requires DF clear on function entry: The direction flag DF in the %rFLAGS register must be clear (set to forward direction) on function entry and return. See: https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf 3.2.1 A concrete failure scenario is: 1. A guest memory move routine sets DF to copy overlapping memory backward, 2. a write hits a cow page and causes a page fault, before the routine clears DF, 3. the cpu saves the interrupted flags but does not clear DF before entering our exception handler, 4. the handler calls Rust with the wrong abi state. Its own memory operations, including the CoW page copy, could run backward and corrupt guest memory or cause another fault. Signed-off-by: Tomasz Andrzejak --- CHANGELOG.md | 1 + .../src/arch/amd64/exception/entry.rs | 2 + src/tests/rust_guests/simpleguest/src/main.rs | 71 +++++++++++-------- 3 files changed, 43 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be8502080..ea3599c85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). * Embedded byte payload tables and their value-union variants. ### Fixed +* AMD64 exception handlers clear the direction flag before calling Rust. * Linear-time segment consumption for fragmented virtqueue messages. * Allow reclaimed virtqueue completions to span multiple ring reuse cycles. * Virtqueue consumers return errors when payload copies or runtime bookkeeping diff --git a/src/hyperlight_guest_bin/src/arch/amd64/exception/entry.rs b/src/hyperlight_guest_bin/src/arch/amd64/exception/entry.rs index b1914d648..894ba6c20 100644 --- a/src/hyperlight_guest_bin/src/arch/amd64/exception/entry.rs +++ b/src/hyperlight_guest_bin/src/arch/amd64/exception/entry.rs @@ -123,6 +123,8 @@ macro_rules! generate_exceptions { // bottom of the context struct, which happens to be the // stack pointer just before it was called. " mov rdi, rsp\n", + // Rust requires DF clear. IRETQ restores the interrupted flags. + " cld\n", " call {hl_exception_handler}\n", context::restore!(), " add rsp, 8\n", // error code diff --git a/src/tests/rust_guests/simpleguest/src/main.rs b/src/tests/rust_guests/simpleguest/src/main.rs index 49f041ab1..2195a7f7c 100644 --- a/src/tests/rust_guests/simpleguest/src/main.rs +++ b/src/tests/rust_guests/simpleguest/src/main.rs @@ -58,6 +58,8 @@ const TEST_R9_VALUE: u64 = 0x1234567890ABCDEF; const TEST_R9_MODIFIED_VALUE: u64 = 0xBADC0FFEE; #[cfg(target_arch = "x86_64")] const TEST_R10_VALUE: u64 = 0xDEADBEEF; +#[cfg(target_arch = "x86_64")] +const RFLAGS_DF: u64 = 1 << 10; #[guest_function("SetStatic")] fn set_static() -> i32 { @@ -74,7 +76,7 @@ fn echo_double(value: f64) -> f64 { value } -// Test exception handler that validates stack layout and records invocation +// Test exception handler that validates DF, stack layout and records invocation // It is designed to interact with the trigger_int3 breakpoint exception function below #[cfg(target_arch = "x86_64")] fn test_exception_handler( @@ -83,6 +85,15 @@ fn test_exception_handler( context: *mut Context, _page_fault_address: u64, ) -> bool { + let flags: u64; + + // SAFETY: Read DF and clear it so a failed assertion can call Rust safely. + unsafe { + core::arch::asm!("pushfq", "pop {flags}", "cld", flags = out(reg) flags); + } + + assert_eq!(flags & RFLAGS_DF, 0, "exception handler requires DF clear"); + // Record invocation HANDLER_INVOCATION_COUNT.fetch_add(1, Ordering::SeqCst); @@ -145,44 +156,42 @@ fn get_exception_handler_call_count() -> i32 { count as i32 } -/// Trigger an INT3 breakpoint exception (vector 3) +/// Check the exception-entry ABI and preservation of interrupted state. #[guest_function("TriggerInt3")] #[cfg(target_arch = "x86_64")] fn trigger_int3() -> i32 { - // Set up test value in R9 before triggering exception - let test_value: u64 = TEST_R9_VALUE; + let r9_result: u64; + let r10_result: u64; + let flags: u64; + // SAFETY: The installed handler resumes at the next instruction. DF is + // cleared before Rust resumes, after reading the flags restored by IRETQ. unsafe { - // Store test value in R9 register - core::arch::asm!( - "mov r9, {0}", - in(reg) test_value - ); - - // This will trigger exception vector 3 (#BP - Breakpoint) - core::arch::asm!("int3"); - - // After returning from exception handler, verify registers - let r9_result: u64; - let r10_result: u64; core::arch::asm!( - "mov {0}, r9", - "mov {1}, r10", - out(reg) r9_result, - out(reg) r10_result - ); - - // R9 should be restored to original value (context restore working) - assert_eq!( - r9_result, test_value, - "R9 register was not properly restored by exception handler" - ); - // R10 should have the value written to context - assert_eq!( - r10_result, TEST_R10_VALUE, - "R10 register was not modified via context by exception handler" + "std", + "int3", + "pushfq", + "pop {flags}", + "cld", + flags = lateout(reg) flags, + inout("r9") TEST_R9_VALUE => r9_result, + lateout("r10") r10_result, ); } + + assert_eq!( + flags & RFLAGS_DF, + RFLAGS_DF, + "interrupted DF was not restored" + ); + assert_eq!( + r9_result, TEST_R9_VALUE, + "R9 register was not properly restored by exception handler" + ); + assert_eq!( + r10_result, TEST_R10_VALUE, + "R10 register was not modified via context by exception handler" + ); 0 }