Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/hyperlight_host/src/hypervisor/fpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ limitations under the License.
*/

pub(crate) const FP_CONTROL_WORD_DEFAULT: u16 = 0x37f; // mask all fp-exception, set rounding to nearest, set precision to 64-bit
pub(crate) const FP_TAG_WORD_DEFAULT: u8 = 0xff; // each 8 of x87 fpu registers is empty
pub(crate) const FP_TAG_WORD_DEFAULT: u8 = 0x00; // 11.5.10.1 FXSAVE Format for x87 Tag Word: A two-bit value of 11 is encoded as a 0, indicating the corresponding x87 FPRn is empty.
pub(crate) const MXCSR_DEFAULT: u32 = 0x1f80; // mask simd fp-exceptions, clear exception flags, set rounding to nearest, disable flush-to-zero mode, disable denormals-are-zero mode
23 changes: 22 additions & 1 deletion src/hyperlight_host/src/hypervisor/kvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,9 @@ impl Hypervisor for KVMDriver {
};
self.vcpu_fd.set_regs(&regs)?;

// reset fpu state
// Note kvm set_fpu doesn't actually set or read the mxcsr value
// We reset it in a call below
// https://elixir.bootlin.com/linux/v6.16/source/arch/x86/kvm/x86.c#L12229
let fpu = kvm_fpu {
fcw: FP_CONTROL_WORD_DEFAULT,
ftwx: FP_TAG_WORD_DEFAULT,
Copy link
Contributor

@ludfjig ludfjig Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Since it's 0 we can just remove it from here and let the ..Default::default() zero it. We can probably remove the constant completely in this case unless it's used for something else

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking on this a bit and I think keeping it here as a const is a good idea since it allows us to be explicit about using the FXSAVE format (00 vs 11). It shouldn't have any impact but allows us to document that we are using the setting we explicitly want.

Expand All @@ -588,6 +590,25 @@ impl Hypervisor for KVMDriver {
};
self.vcpu_fd.set_fpu(&fpu)?;

// Set MXCSR from XSAVE
// Locations are from
// AMD64 Architecture Programmer's Manual, Volume 2
// 11.5.10 Mode-Specific XSAVE/XRSTOR State Management
// MXCSR is at byte offset 24 -> u32 index 6
let mut xsave = match self.vcpu_fd.get_xsave() {
Copy link
Contributor

@ludfjig ludfjig Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're doing xsave, can we omit the existing self.vcpu_fd.set_fpu(&fpu)?; call? Do they overlap in functionality?

Also: Maybe we should save xsave after calling initialize, and restore to that known good state after each to dispatch_call_from_host?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're doing xsave, can we omit the existing self.vcpu_fd.set_fpu(&fpu)?; call?

I considered this but felt the the xsave structure was pretty opaque with lots that could change. It felt clearer/safer to do this and there didn't seem to be a noticeable perf difference.

Also: Maybe we should save xsave after calling initialize, and restore to that known good state after each to dispatch_call_from_host?

This is a good idea! Let me take a look at this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also: Maybe we should save xsave after calling initialize, and restore to that known good state after each to dispatch_call_from_host?

This is a good idea! Let me take a look at this.

I tried this out by initializing the FP registers once and storing them. I didn't notice any impact on performance (but it's hard to measure at the function call level due to so many variances). Saving does incur an additional ~4k of memory per vm instance. I lean towards leaving this approach and revisiting it if required. what do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's fine. However, I think I am a little skeptical in general about using xsave just for the purpose of resetting just the single MXCSR register. There are plenty other registers we don't reset currently, that we probably should. Maybe let's take the opportunity to reset some more registers? What do you think?

Also, regarding

If we're doing xsave, can we omit the existing self.vcpu_fd.set_fpu(&fpu)?; call?

I considered this but felt the the xsave structure was pretty opaque with lots that could change. It felt clearer/safer to do this and there didn't seem to be a noticeable perf difference.

I think xsave should be pretty standardized, at least per-hypervisor, so I do think it's worth looking into this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are plenty other registers we don't reset currently, that we probably should. Maybe let's take the opportunity to reset some more registers? What do you think?

Yes, we have #791 and I started to look into other registers. I was using this as a way to get a pattern set up without going after everything in one go.

I think xsave should be pretty standardized, at least per-hypervisor, so I do think it's worth looking into this

xsave does have a specific layout depending on the features enabled. Maybe we could do this after #907 and other related changes.

Ok(xsave) => xsave,
Err(e) => {
return Err(new_error!("Could not write guest registers: {:?}", e));
}
};

xsave.region[6] = MXCSR_DEFAULT;
unsafe {
self.vcpu_fd
.set_xsave(&xsave)
.map_err(|e| new_error!("Could not write guest registers: {:?}", e))?
};

// run
VirtualCPU::run(
self.as_mut_hypervisor(),
Expand Down
41 changes: 41 additions & 0 deletions src/hyperlight_host/src/sandbox/initialized_multi_use.rs
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ mod tests {
use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
use hyperlight_testing::simple_guest_as_string;

use crate::hypervisor::fpu;
#[cfg(target_os = "linux")]
use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType};
#[cfg(target_os = "linux")]
Expand Down Expand Up @@ -1021,4 +1022,44 @@ mod tests {
};
assert_ne!(sandbox3.id, sandbox_id);
}

#[test]
fn test_fpu_mxcsr_register_reset() {
let path = simple_guest_as_string().unwrap();
let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
let mut sandbox = u_sbox.evolve().unwrap();

// The x87 Tag Word Register (FTW) all ones means empty state but we are setting it with xsave
// where the value should be all 00's for empty state
// https://github.com/hyperlight-dev/hyperlight/issues/904
let expected = format!(
"fcw:{:04X},ftw:{:02X},mxcsr:{:08X}",
fpu::FP_CONTROL_WORD_DEFAULT,
0xFFFF,
fpu::MXCSR_DEFAULT
);

// First, read the initial state of FPU control word, tag word, and MXCSR registers
let initial_state: String = sandbox.call("ReadFpuMxcsr", ()).unwrap();
assert_eq!(
initial_state, expected,
"Initial FPU/tag word/MXCSR state does not match expected defaults"
);

// Modify the FPU and MXCSR registers to non-default values
let modify_result: String = sandbox.call("ModifyFpuMxcsr", ()).unwrap();
let modified_expected =
format!("fcw:{:04X},ftw:{:02X},mxcsr:{:08X}", 0x027F, 0x1113, 0x1F00);
assert_eq!(
modify_result, modified_expected,
"Modified state does not match"
);

// A new call should reset these
let reset_state: String = sandbox.call("ReadFpuMxcsr", ()).unwrap();
assert_eq!(
reset_state, expected,
"FPU/tag word/MXCSR registers were not properly reset between calls"
);
}
}
33 changes: 33 additions & 0 deletions src/hyperlight_host/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,39 @@ fn log_message() {
assert_eq!(1, LOGGER.num_log_calls());
}

#[test]
fn test_fpu_mxcsr_snapshot_restore() {
let mut sandbox = new_uninit_rust().unwrap().evolve().unwrap();

// The x87 Tag Word Register (FTW) all ones means empty state but we are setting it with xsave
// where the value should be all 00's for empty state
// https://github.com/hyperlight-dev/hyperlight/issues/904
let expected = format!("fcw:{:04X},ftw:{:02X},mxcsr:{:08X}", 0x37F, 0xFFFF, 0x1F80);

let initial_state: String = sandbox.call("ReadFpuMxcsr", ()).unwrap();
assert_eq!(
initial_state, expected,
"Initial FPU/tag word/MXCSR state does not match expected defaults"
);

let snapshot = sandbox.snapshot().unwrap();

let modify_result: String = sandbox.call("ModifyFpuMxcsr", ()).unwrap();
let modified_expected = format!("fcw:{:04X},ftw:{:02X},mxcsr:{:08X}", 0x027F, 0x1113, 0x1F00);
assert_eq!(
modify_result, modified_expected,
"Modified state does not match expected values"
);

sandbox.restore(&snapshot).unwrap();

let restored_state: String = sandbox.call("ReadFpuMxcsr", ()).unwrap();
assert_eq!(
restored_state, expected,
"FPU/tag word/MXCSR registers should be restored to defaults after snapshot restore"
);
}

fn log_test_messages(levelfilter: Option<log::LevelFilter>) {
LOGGER.clear_log_calls();
assert_eq!(0, LOGGER.num_log_calls());
Expand Down
4 changes: 2 additions & 2 deletions src/tests/rust_guests/dummyguest/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/tests/rust_guests/simpleguest/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

121 changes: 121 additions & 0 deletions src/tests/rust_guests/simpleguest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,110 @@ fn exec_mapped_buffer(function_call: &FunctionCall) -> Result<Vec<u8>> {
}
}

/// Simple function to modify FPU control word and MXCSR register
#[hyperlight_guest_tracing::trace_function]
fn modify_fpu_mxcsr(_function_call: &FunctionCall) -> Result<Vec<u8>> {
unsafe {
// Set FPU control word to a non-default value (default is usually 0x037F)
// We'll set it to 0x027F (change precision control and rounding)
let new_fcw: u16 = 0x027F;
core::arch::asm!(
"fldcw [{fcw_ptr}]",
fcw_ptr = in(reg) &new_fcw,
);

// Load some FPU registers to change the tag word from default 0xFF
// This will mark some registers as valid, changing the tag word
// Sets up FPR with: 00 01 00 01 00 01 00 11 which is 0x1113;
core::arch::asm!(
"fld1", // ST(6)
"fldz", // ST(5)
"fld1", // ST(4)
"fldz", // ST(3)
"fld1", // ST(2)
"fldz", // ST(1)
"fld1", // ST(0)
);

// Set MXCSR to a non-default value (default is usually 0x1F80)
// We'll set it to 0x1F00 (disable some exception masks)
let new_mxcsr: u32 = 0x1F00;
core::arch::asm!(
"ldmxcsr [{mxcsr_ptr}]",
mxcsr_ptr = in(reg) &new_mxcsr,
);
}

let mut fcw: u16 = 0;
let ftw: u16;
let mut mxcsr: u32 = 0;

unsafe {
// Read FPU control word
core::arch::asm!(
"fnstcw [{fcw_ptr}]",
fcw_ptr = in(reg) &mut fcw,
);

// Read FPU status word and tag word using fnstenv
// fnstenv stores the complete FPU environment (28 bytes)
// The format is the the FTW format where 11 == empty
// This is different from the FXSAVE format
// https://github.com/hyperlight-dev/hyperlight/issues/904
let mut fpu_env: [u8; 28] = [0; 28];
core::arch::asm!(
"fnstenv [{env_ptr}]",
env_ptr = in(reg) &mut fpu_env,
);
ftw = u16::from_le_bytes([fpu_env[8], fpu_env[9]]);

// Read MXCSR
core::arch::asm!(
"stmxcsr [{mxcsr_ptr}]",
mxcsr_ptr = in(reg) &mut mxcsr,
);
}

let result = format!("fcw:{:04X},ftw:{:02X},mxcsr:{:08X}", fcw, ftw, mxcsr);
Ok(get_flatbuffer_result(result.as_str()))
}

#[hyperlight_guest_tracing::trace_function]
fn read_fpu_mxcsr(_function_call: &FunctionCall) -> Result<Vec<u8>> {
let mut fcw: u16 = 0;
let ftw: u16;
let mut mxcsr: u32 = 0;

unsafe {
// Read FPU control word
core::arch::asm!(
"fnstcw [{fcw_ptr}]",
fcw_ptr = in(reg) &mut fcw,
);

// Read FPU status word and tag word using fnstenv
// fnstenv stores the complete FPU environment (28 bytes)
// The format is the the FTW format where 11 == empty
// This is different from the FXSAVE format
// https://github.com/hyperlight-dev/hyperlight/issues/904
let mut fpu_env: [u8; 28] = [0; 28];
core::arch::asm!(
"fnstenv [{env_ptr}]",
env_ptr = in(reg) &mut fpu_env,
);
ftw = u16::from_le_bytes([fpu_env[8], fpu_env[9]]);

// Read MXCSR
core::arch::asm!(
"stmxcsr [{mxcsr_ptr}]",
mxcsr_ptr = in(reg) &mut mxcsr,
);
}

let result = format!("fcw:{:04X},ftw:{:02X},mxcsr:{:08X}", fcw, ftw, mxcsr);
Ok(get_flatbuffer_result(result.as_str()))
}

#[no_mangle]
#[hyperlight_guest_tracing::trace_function]
pub extern "C" fn hyperlight_main() {
Expand Down Expand Up @@ -1477,6 +1581,23 @@ pub extern "C" fn hyperlight_main() {
call_given_paramless_hostfunc_that_returns_i64 as usize,
);
register_function(call_given_hostfunc_def);

// Register FPU and MXCSR test functions
let modify_fpu_mxcsr_def = GuestFunctionDefinition::new(
"ModifyFpuMxcsr".to_string(),
Vec::new(),
ReturnType::String,
modify_fpu_mxcsr as usize,
);
register_function(modify_fpu_mxcsr_def);

let read_fpu_mxcsr_def = GuestFunctionDefinition::new(
"ReadFpuMxcsr".to_string(),
Vec::new(),
ReturnType::String,
read_fpu_mxcsr as usize,
);
register_function(read_fpu_mxcsr_def);
}

#[hyperlight_guest_tracing::trace_function]
Expand Down
4 changes: 2 additions & 2 deletions src/tests/rust_guests/witguest/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ extend-exclude = ["**/*.patch", "src/hyperlight_guest_bin/third_party/**/*", "NO
# typ is used for field name as type is a reserved keyword
typ="typ"
mmaped="mmapped"
FPR="FPR"
Loading