Skip to content

Commit 22f1308

Browse files
committed
refactor: split OutBAction into OutBAction + VmAction, move hw_interrupts to x86_64/
- Split OutBAction enum: PvTimerConfig and Halt moved to new VmAction enum. VmAction ports are intercepted at the hypervisor level in run_vcpu and never reach the sandbox outb handler, so the split eliminates unreachable match arms. - Move hw_interrupts.rs to virtual_machine/x86_64/hw_interrupts.rs since it contains x86-64 specific helpers (LAPIC, PIC, PIT). - Remove halt() from hyperlight_guest::exit — all halt sites use inline assembly with options(noreturn) to avoid stack epilogue issues. - Extract handle_pv_timer_config() method in KVM backend. - Remove intermediate variable in WhpVm::new(). - Restore create_vm_with_args() comment in MSHV backend. - Add memory fence after IDT writes in simpleguest for CoW safety. Signed-off-by: danbugs <danilochiarlone@gmail.com>
1 parent 8014fb5 commit 22f1308

12 files changed

Lines changed: 134 additions & 118 deletions

File tree

src/hyperlight_common/src/outb.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ impl TryFrom<u8> for Exception {
8686
}
8787

8888
/// Supported actions when issuing an OUTB actions by Hyperlight.
89+
/// These are handled by the sandbox-level outb dispatcher.
8990
/// - Log: for logging,
9091
/// - CallFunction: makes a call to a host function,
9192
/// - Abort: aborts the execution of the guest,
@@ -104,6 +105,13 @@ pub enum OutBAction {
104105
TraceMemoryAlloc = 105,
105106
#[cfg(feature = "mem_profile")]
106107
TraceMemoryFree = 106,
108+
}
109+
110+
/// IO-port actions intercepted at the hypervisor level (in `run_vcpu`)
111+
/// before they ever reach the sandbox outb handler. These are split
112+
/// from [`OutBAction`] so the outb handler does not need unreachable
113+
/// match arms for ports it can never see.
114+
pub enum VmAction {
107115
/// IO port for PV timer configuration. The guest writes a 32-bit
108116
/// LE value representing the desired timer period in microseconds.
109117
/// A value of 0 disables the timer.
@@ -129,9 +137,18 @@ impl TryFrom<u16> for OutBAction {
129137
105 => Ok(OutBAction::TraceMemoryAlloc),
130138
#[cfg(feature = "mem_profile")]
131139
106 => Ok(OutBAction::TraceMemoryFree),
132-
107 => Ok(OutBAction::PvTimerConfig),
133-
108 => Ok(OutBAction::Halt),
134140
_ => Err(anyhow::anyhow!("Invalid OutBAction value: {}", val)),
135141
}
136142
}
137143
}
144+
145+
impl TryFrom<u16> for VmAction {
146+
type Error = anyhow::Error;
147+
fn try_from(val: u16) -> anyhow::Result<Self> {
148+
match val {
149+
107 => Ok(VmAction::PvTimerConfig),
150+
108 => Ok(VmAction::Halt),
151+
_ => Err(anyhow::anyhow!("Invalid VmAction value: {}", val)),
152+
}
153+
}
154+
}

src/hyperlight_guest/src/exit.rs

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,34 +18,6 @@ use core::arch::asm;
1818
use core::ffi::{CStr, c_char};
1919

2020
use hyperlight_common::outb::OutBAction;
21-
use tracing::instrument;
22-
23-
/// Signal successful completion of the guest's work and return control
24-
/// to the host. This replaces the previous `hlt`-based exit: under the
25-
/// `hw-interrupts` feature, `hlt` becomes a wait-for-interrupt (the
26-
/// in-kernel IRQ chip wakes the vCPU), so we use an explicit IO-port
27-
/// write (port 108) to trigger a VM exit that the host treats as a
28-
/// clean shutdown.
29-
///
30-
/// This function never returns — the host does not re-enter the guest
31-
/// after seeing the Halt port.
32-
#[inline(never)]
33-
#[instrument(skip_all, level = "Trace")]
34-
pub fn halt() {
35-
#[cfg(all(feature = "trace_guest", target_arch = "x86_64"))]
36-
{
37-
// Send data before halting
38-
// If there is no data, this doesn't do anything
39-
// The reason we do this here instead of in `hlt` asm function
40-
// is to avoid allocating before halting, which leaks memory
41-
// because the guest is not expected to resume execution after halting.
42-
hyperlight_guest_tracing::flush();
43-
}
44-
45-
unsafe {
46-
out32(OutBAction::Halt as u16, 0);
47-
}
48-
}
4921

5022
/// Exits the VM with an Abort OUT action and code 0.
5123
#[unsafe(no_mangle)]

src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use std::sync::LazyLock;
2020
#[cfg(feature = "hw-interrupts")]
2121
use std::sync::atomic::{AtomicBool, Ordering};
2222

23-
use hyperlight_common::outb::OutBAction;
23+
use hyperlight_common::outb::VmAction;
2424
#[cfg(gdb)]
2525
use kvm_bindings::kvm_guest_debug;
2626
use kvm_bindings::{
@@ -135,7 +135,7 @@ impl KvmVm {
135135
//
136136
// Instead of creating an in-kernel PIT (create_pit2), we use a
137137
// host-side timer thread + irqfd to inject IRQ0 at the rate
138-
// requested by the guest via OutBAction::PvTimerConfig (port 107).
138+
// requested by the guest via VmAction::PvTimerConfig (port 107).
139139
// This eliminates the in-kernel PIT device. Guest PIT port writes
140140
// (0x40, 0x43) become no-ops handled in the run loop.
141141
#[cfg(feature = "hw-interrupts")]
@@ -201,7 +201,7 @@ impl KvmVm {
201201
/// When hw-interrupts is enabled, the in-kernel PIC + LAPIC deliver
202202
/// interrupts triggered by the host-side timer thread via irqfd.
203203
/// There is no in-kernel PIT; guest PIT port writes are no-ops.
204-
/// The guest signals "I'm done" by writing to OutBAction::Halt
204+
/// The guest signals "I'm done" by writing to VmAction::Halt
205205
/// (an IO port exit) instead of using HLT, because the in-kernel
206206
/// LAPIC absorbs HLT (never returns VcpuExit::Hlt to userspace).
207207
#[cfg(feature = "hw-interrupts")]
@@ -214,47 +214,20 @@ impl KvmVm {
214214
continue;
215215
}
216216
Ok(VcpuExit::IoOut(port, data)) => {
217-
if port == OutBAction::Halt as u16 {
217+
if port == VmAction::Halt as u16 {
218218
// Stop the timer thread before returning.
219219
self.timer_stop.store(true, Ordering::Relaxed);
220220
if let Some(h) = self.timer_thread.take() {
221221
let _ = h.join();
222222
}
223223
return Ok(VmExit::Halt());
224224
}
225-
if port == OutBAction::PvTimerConfig as u16 {
226-
// The guest is configuring the timer period.
227-
// Extract the period in microseconds (LE u32).
228-
if let Some(bytes4) = data.get(..4)
229-
&& let Ok(bytes) = bytes4.try_into()
230-
{
231-
let period_us = u32::from_le_bytes(bytes) as u64;
232-
if period_us == 0 {
233-
// Stop existing timer if any.
234-
if let Some(thread) = self.timer_thread.take() {
235-
self.timer_stop.store(true, Ordering::Relaxed);
236-
let _ = thread.join();
237-
}
238-
} else if self.timer_thread.is_none() {
239-
// Reset the stop flag — a previous halt (e.g. the
240-
// init halt during evolve()) may have set it.
241-
self.timer_stop.store(false, Ordering::Relaxed);
242-
let eventfd = self.timer_irq_eventfd.try_clone().map_err(|e| {
243-
RunVcpuError::Unknown(HypervisorError::KvmError(e.into()))
244-
})?;
245-
let stop = self.timer_stop.clone();
246-
let period = std::time::Duration::from_micros(period_us);
247-
self.timer_thread = Some(std::thread::spawn(move || {
248-
while !stop.load(Ordering::Relaxed) {
249-
std::thread::sleep(period);
250-
if stop.load(Ordering::Relaxed) {
251-
break;
252-
}
253-
let _ = eventfd.write(1);
254-
}
255-
}));
256-
}
257-
}
225+
if port == VmAction::PvTimerConfig as u16 {
226+
let data_copy: [u8; 4] = data
227+
.get(..4)
228+
.and_then(|s| s.try_into().ok())
229+
.unwrap_or([0; 4]);
230+
self.handle_pv_timer_config(&data_copy)?;
258231
continue;
259232
}
260233
// PIT ports (0x40-0x43): no in-kernel PIT, so these
@@ -288,12 +261,44 @@ impl KvmVm {
288261
}
289262
}
290263

264+
#[cfg(feature = "hw-interrupts")]
265+
fn handle_pv_timer_config(&mut self, data: &[u8; 4]) -> std::result::Result<(), RunVcpuError> {
266+
let period_us = u32::from_le_bytes(*data) as u64;
267+
if period_us == 0 {
268+
// Stop existing timer if any.
269+
if let Some(thread) = self.timer_thread.take() {
270+
self.timer_stop.store(true, Ordering::Relaxed);
271+
let _ = thread.join();
272+
}
273+
} else if self.timer_thread.is_none() {
274+
// Reset the stop flag — a previous halt (e.g. the
275+
// init halt during evolve()) may have set it.
276+
self.timer_stop.store(false, Ordering::Relaxed);
277+
let eventfd = self
278+
.timer_irq_eventfd
279+
.try_clone()
280+
.map_err(|e| RunVcpuError::Unknown(HypervisorError::KvmError(e.into())))?;
281+
let stop = self.timer_stop.clone();
282+
let period = std::time::Duration::from_micros(period_us);
283+
self.timer_thread = Some(std::thread::spawn(move || {
284+
while !stop.load(Ordering::Relaxed) {
285+
std::thread::sleep(period);
286+
if stop.load(Ordering::Relaxed) {
287+
break;
288+
}
289+
let _ = eventfd.write(1);
290+
}
291+
}));
292+
}
293+
Ok(())
294+
}
295+
291296
/// Run the vCPU once without hardware interrupt support (default path).
292297
#[cfg(not(feature = "hw-interrupts"))]
293298
fn run_vcpu_default(&mut self) -> std::result::Result<VmExit, RunVcpuError> {
294299
match self.vcpu_fd.run() {
295300
Ok(VcpuExit::Hlt) => Ok(VmExit::Halt()),
296-
Ok(VcpuExit::IoOut(port, _)) if port == OutBAction::Halt as u16 => Ok(VmExit::Halt()),
301+
Ok(VcpuExit::IoOut(port, _)) if port == VmAction::Halt as u16 => Ok(VmExit::Halt()),
297302
Ok(VcpuExit::IoOut(port, data)) => Ok(VmExit::IoOut(port, data.to_vec())),
298303
Ok(VcpuExit::MmioRead(addr, _)) => Ok(VmExit::MmioRead(addr)),
299304
Ok(VcpuExit::MmioWrite(addr, _)) => Ok(VmExit::MmioWrite(addr)),
@@ -609,8 +614,8 @@ mod hw_interrupt_tests {
609614

610615
#[test]
611616
fn halt_port_is_not_standard_device() {
612-
// OutBAction::Halt port must not overlap in-kernel PIC/PIT/speaker ports
613-
const HALT: u16 = OutBAction::Halt as u16;
617+
// VmAction::Halt port must not overlap in-kernel PIC/PIT/speaker ports
618+
const HALT: u16 = VmAction::Halt as u16;
614619
const _: () = assert!(HALT != 0x20 && HALT != 0x21);
615620
const _: () = assert!(HALT != 0xA0 && HALT != 0xA1);
616621
const _: () = assert!(HALT != 0x40 && HALT != 0x41 && HALT != 0x42 && HALT != 0x43);

src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ pub(crate) mod mshv;
3838
#[cfg(target_os = "windows")]
3939
pub(crate) mod whp;
4040

41-
/// Shared helpers for hardware interrupt support (MSHV and WHP)
41+
/// Shared x86-64 helpers for hardware interrupt support (MSHV and WHP)
4242
#[cfg(feature = "hw-interrupts")]
43-
pub(crate) mod hw_interrupts;
43+
pub(crate) mod x86_64;
4444

4545
static AVAILABLE_HYPERVISOR: OnceLock<Option<HypervisorType>> = OnceLock::new();
4646

src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use std::sync::LazyLock;
2222
#[cfg(feature = "hw-interrupts")]
2323
use std::sync::atomic::{AtomicBool, Ordering};
2424

25-
use hyperlight_common::outb::OutBAction;
25+
use hyperlight_common::outb::VmAction;
2626
#[cfg(feature = "hw-interrupts")]
2727
use mshv_bindings::LapicState;
2828
#[cfg(gdb)]
@@ -128,6 +128,8 @@ impl MshvVm {
128128
{
129129
pr.pt_flags = 1u64; // LAPIC
130130
}
131+
// It's important to use create_vm_with_args() (not create_vm()),
132+
// because create_vm() sets up a SynIC partition by default.
131133
let vm_fd = mshv
132134
.create_vm_with_args(&pr)
133135
.map_err(|e| CreateVmError::CreateVmFd(e.into()))?;
@@ -156,7 +158,7 @@ impl MshvVm {
156158
// interrupts can be delivered (request_virtual_interrupt would fail).
157159
#[cfg(feature = "hw-interrupts")]
158160
{
159-
use super::hw_interrupts::init_lapic_registers;
161+
use super::super::x86_64::hw_interrupts::init_lapic_registers;
160162

161163
let mut lapic: LapicState = vcpu_fd
162164
.get_lapic()
@@ -261,9 +263,9 @@ impl VirtualMachine for MshvVm {
261263
}])
262264
.map_err(|e| RunVcpuError::IncrementRip(e.into()))?;
263265

264-
// OutBAction::Halt always means "I'm done", regardless
266+
// VmAction::Halt always means "I'm done", regardless
265267
// of whether a timer is active.
266-
if is_write && port_number == OutBAction::Halt as u16 {
268+
if is_write && port_number == VmAction::Halt as u16 {
267269
// Stop the timer thread before returning.
268270
#[cfg(feature = "hw-interrupts")]
269271
{
@@ -283,7 +285,7 @@ impl VirtualMachine for MshvVm {
283285
continue;
284286
}
285287
} else if let Some(val) =
286-
super::hw_interrupts::handle_io_in(port_number)
288+
super::super::x86_64::hw_interrupts::handle_io_in(port_number)
287289
{
288290
self.vcpu_fd
289291
.set_reg(&[hv_register_assoc {
@@ -629,13 +631,13 @@ impl MshvVm {
629631
/// acknowledges via PIC.
630632
fn do_lapic_eoi(&self) {
631633
if let Ok(mut lapic) = self.vcpu_fd.get_lapic() {
632-
super::hw_interrupts::lapic_eoi(lapic_regs_as_u8_mut(&mut lapic.regs));
634+
super::super::x86_64::hw_interrupts::lapic_eoi(lapic_regs_as_u8_mut(&mut lapic.regs));
633635
let _ = self.vcpu_fd.set_lapic(&lapic);
634636
}
635637
}
636638

637639
fn handle_hw_io_out(&mut self, port: u16, data: &[u8]) -> bool {
638-
if port == OutBAction::PvTimerConfig as u16 {
640+
if port == VmAction::PvTimerConfig as u16 {
639641
if data.len() >= 4 {
640642
let period_us = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
641643
if period_us == 0 {
@@ -677,17 +679,19 @@ impl MshvVm {
677679
// guest disabled the APIC globally)
678680
if let Ok(mut lapic) = self.vcpu_fd.get_lapic() {
679681
let regs = lapic_regs_as_u8(&lapic.regs);
680-
let svr = super::hw_interrupts::read_lapic_u32(regs, 0xF0);
682+
let svr = super::super::x86_64::hw_interrupts::read_lapic_u32(regs, 0xF0);
681683
if svr & 0x100 == 0 {
682684
let regs_mut = lapic_regs_as_u8_mut(&mut lapic.regs);
683-
super::hw_interrupts::write_lapic_u32(regs_mut, 0xF0, 0x1FF);
684-
super::hw_interrupts::write_lapic_u32(regs_mut, 0x80, 0); // TPR
685+
super::super::x86_64::hw_interrupts::write_lapic_u32(
686+
regs_mut, 0xF0, 0x1FF,
687+
);
688+
super::super::x86_64::hw_interrupts::write_lapic_u32(regs_mut, 0x80, 0); // TPR
685689
let _ = self.vcpu_fd.set_lapic(&lapic);
686690
}
687691
}
688692

689693
let vm_fd = self.vm_fd.clone();
690-
let vector = super::hw_interrupts::TIMER_VECTOR;
694+
let vector = super::super::x86_64::hw_interrupts::TIMER_VECTOR;
691695
let stop = self.timer_stop.clone();
692696
let period = std::time::Duration::from_micros(period_us as u64);
693697
self.timer_thread = Some(std::thread::spawn(move || {
@@ -711,7 +715,9 @@ impl MshvVm {
711715
return true;
712716
}
713717
let timer_active = self.timer_thread.is_some();
714-
super::hw_interrupts::handle_common_io_out(port, data, timer_active, || self.do_lapic_eoi())
718+
super::super::x86_64::hw_interrupts::handle_common_io_out(port, data, timer_active, || {
719+
self.do_lapic_eoi()
720+
})
715721
}
716722
}
717723

@@ -734,10 +740,10 @@ mod hw_interrupt_tests {
734740
fn lapic_regs_conversion_roundtrip() {
735741
let mut regs = [0i8; 1024];
736742
let bytes = lapic_regs_as_u8_mut(&mut regs);
737-
super::super::hw_interrupts::write_lapic_u32(bytes, 0xF0, 0xDEAD_BEEF);
743+
super::super::super::x86_64::hw_interrupts::write_lapic_u32(bytes, 0xF0, 0xDEAD_BEEF);
738744
let bytes = lapic_regs_as_u8(&regs);
739745
assert_eq!(
740-
super::super::hw_interrupts::read_lapic_u32(bytes, 0xF0),
746+
super::super::super::x86_64::hw_interrupts::read_lapic_u32(bytes, 0xF0),
741747
0xDEAD_BEEF
742748
);
743749
}

0 commit comments

Comments
 (0)