Skip to content

Commit 4caccbc

Browse files
authored
Add debug register abstraction for Vm (#1180)
* Add debug register abstraction for Vm Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> * Review comments Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --------- Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
1 parent b4cc26a commit 4caccbc

File tree

6 files changed

+369
-133
lines changed

6 files changed

+369
-133
lines changed

src/hyperlight_host/src/hypervisor/regs.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
mod debug_regs;
1718
mod fpu;
1819
mod special_regs;
1920
mod standard_regs;
2021

2122
#[cfg(target_os = "windows")]
2223
use std::collections::HashSet;
2324

25+
pub(crate) use debug_regs::*;
2426
pub(crate) use fpu::*;
2527
pub(crate) use special_regs::*;
2628
pub(crate) use standard_regs::*;
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
/*
2+
Copyright 2025 The Hyperlight Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
#[cfg(kvm)]
18+
use kvm_bindings::kvm_debugregs;
19+
#[cfg(mshv3)]
20+
use mshv_bindings::DebugRegisters;
21+
22+
/// Common abstraction for x86 debug registers (DR0-DR7).
23+
#[derive(Debug, Default, Copy, Clone, PartialEq)]
24+
pub(crate) struct CommonDebugRegs {
25+
pub dr0: u64,
26+
pub dr1: u64,
27+
pub dr2: u64,
28+
pub dr3: u64,
29+
pub dr6: u64,
30+
pub dr7: u64,
31+
}
32+
33+
#[cfg(kvm)]
34+
impl From<kvm_debugregs> for CommonDebugRegs {
35+
fn from(kvm_regs: kvm_debugregs) -> Self {
36+
Self {
37+
dr0: kvm_regs.db[0],
38+
dr1: kvm_regs.db[1],
39+
dr2: kvm_regs.db[2],
40+
dr3: kvm_regs.db[3],
41+
dr6: kvm_regs.dr6,
42+
dr7: kvm_regs.dr7,
43+
}
44+
}
45+
}
46+
#[cfg(kvm)]
47+
impl From<&CommonDebugRegs> for kvm_debugregs {
48+
fn from(common_regs: &CommonDebugRegs) -> Self {
49+
kvm_debugregs {
50+
db: [
51+
common_regs.dr0,
52+
common_regs.dr1,
53+
common_regs.dr2,
54+
common_regs.dr3,
55+
],
56+
dr6: common_regs.dr6,
57+
dr7: common_regs.dr7,
58+
..Default::default()
59+
}
60+
}
61+
}
62+
#[cfg(mshv3)]
63+
impl From<DebugRegisters> for CommonDebugRegs {
64+
fn from(mshv_regs: DebugRegisters) -> Self {
65+
Self {
66+
dr0: mshv_regs.dr0,
67+
dr1: mshv_regs.dr1,
68+
dr2: mshv_regs.dr2,
69+
dr3: mshv_regs.dr3,
70+
dr6: mshv_regs.dr6,
71+
dr7: mshv_regs.dr7,
72+
}
73+
}
74+
}
75+
#[cfg(mshv3)]
76+
impl From<&CommonDebugRegs> for DebugRegisters {
77+
fn from(common_regs: &CommonDebugRegs) -> Self {
78+
DebugRegisters {
79+
dr0: common_regs.dr0,
80+
dr1: common_regs.dr1,
81+
dr2: common_regs.dr2,
82+
dr3: common_regs.dr3,
83+
dr6: common_regs.dr6,
84+
dr7: common_regs.dr7,
85+
}
86+
}
87+
}
88+
89+
#[cfg(target_os = "windows")]
90+
use windows::Win32::System::Hypervisor::*;
91+
92+
#[cfg(target_os = "windows")]
93+
impl From<&CommonDebugRegs>
94+
for [(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>); WHP_DEBUG_REGS_NAMES_LEN]
95+
{
96+
fn from(regs: &CommonDebugRegs) -> Self {
97+
[
98+
(
99+
WHvX64RegisterDr0,
100+
Align16(WHV_REGISTER_VALUE { Reg64: regs.dr0 }),
101+
),
102+
(
103+
WHvX64RegisterDr1,
104+
Align16(WHV_REGISTER_VALUE { Reg64: regs.dr1 }),
105+
),
106+
(
107+
WHvX64RegisterDr2,
108+
Align16(WHV_REGISTER_VALUE { Reg64: regs.dr2 }),
109+
),
110+
(
111+
WHvX64RegisterDr3,
112+
Align16(WHV_REGISTER_VALUE { Reg64: regs.dr3 }),
113+
),
114+
(
115+
WHvX64RegisterDr6,
116+
Align16(WHV_REGISTER_VALUE { Reg64: regs.dr6 }),
117+
),
118+
(
119+
WHvX64RegisterDr7,
120+
Align16(WHV_REGISTER_VALUE { Reg64: regs.dr7 }),
121+
),
122+
]
123+
}
124+
}
125+
126+
#[cfg(target_os = "windows")]
127+
use std::collections::HashSet;
128+
129+
#[cfg(target_os = "windows")]
130+
use super::{Align16, FromWhpRegisterError};
131+
132+
#[cfg(target_os = "windows")]
133+
pub(crate) const WHP_DEBUG_REGS_NAMES_LEN: usize = 6;
134+
#[cfg(target_os = "windows")]
135+
pub(crate) const WHP_DEBUG_REGS_NAMES: [WHV_REGISTER_NAME; WHP_DEBUG_REGS_NAMES_LEN] = [
136+
WHvX64RegisterDr0,
137+
WHvX64RegisterDr1,
138+
WHvX64RegisterDr2,
139+
WHvX64RegisterDr3,
140+
WHvX64RegisterDr6,
141+
WHvX64RegisterDr7,
142+
];
143+
144+
#[cfg(target_os = "windows")]
145+
impl TryFrom<&[(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>)]> for CommonDebugRegs {
146+
type Error = FromWhpRegisterError;
147+
148+
#[expect(
149+
non_upper_case_globals,
150+
reason = "Windows API has lowercase register names"
151+
)]
152+
fn try_from(
153+
regs: &[(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>)],
154+
) -> Result<Self, Self::Error> {
155+
if regs.len() != WHP_DEBUG_REGS_NAMES_LEN {
156+
return Err(FromWhpRegisterError::InvalidLength(regs.len()));
157+
}
158+
let mut registers = CommonDebugRegs::default();
159+
let mut seen_registers = HashSet::new();
160+
161+
for &(name, value) in regs {
162+
let name_id = name.0;
163+
164+
// Check for duplicates
165+
if !seen_registers.insert(name_id) {
166+
return Err(FromWhpRegisterError::DuplicateRegister(name_id));
167+
}
168+
169+
unsafe {
170+
match name {
171+
WHvX64RegisterDr0 => registers.dr0 = value.0.Reg64,
172+
WHvX64RegisterDr1 => registers.dr1 = value.0.Reg64,
173+
WHvX64RegisterDr2 => registers.dr2 = value.0.Reg64,
174+
WHvX64RegisterDr3 => registers.dr3 = value.0.Reg64,
175+
WHvX64RegisterDr6 => registers.dr6 = value.0.Reg64,
176+
WHvX64RegisterDr7 => registers.dr7 = value.0.Reg64,
177+
_ => {
178+
// Given unexpected register
179+
return Err(FromWhpRegisterError::InvalidRegister(name_id));
180+
}
181+
}
182+
}
183+
}
184+
185+
// Set of all expected register names
186+
let expected_registers: HashSet<i32> = WHP_DEBUG_REGS_NAMES
187+
.map(|name| name.0)
188+
.into_iter()
189+
.collect();
190+
191+
// Technically it should not be possible to have any missing registers at this point
192+
// since we are guaranteed to have WHP_DEBUG_REGS_NAMES_LEN (6) non-duplicate registers that have passed the match-arm above, but leaving this here for safety anyway
193+
let missing: HashSet<_> = expected_registers
194+
.difference(&seen_registers)
195+
.cloned()
196+
.collect();
197+
198+
if !missing.is_empty() {
199+
return Err(FromWhpRegisterError::MissingRegister(missing));
200+
}
201+
202+
Ok(registers)
203+
}
204+
}
205+
206+
#[cfg(test)]
207+
mod tests {
208+
use super::*;
209+
210+
fn common_debug_regs() -> CommonDebugRegs {
211+
CommonDebugRegs {
212+
dr0: 1,
213+
dr1: 2,
214+
dr2: 3,
215+
dr3: 4,
216+
dr6: 5,
217+
dr7: 6,
218+
}
219+
}
220+
221+
#[cfg(kvm)]
222+
#[test]
223+
fn round_trip_kvm_debug_regs() {
224+
let original = common_debug_regs();
225+
let kvm_regs: kvm_debugregs = (&original).into();
226+
let converted: CommonDebugRegs = kvm_regs.into();
227+
assert_eq!(original, converted);
228+
}
229+
230+
#[cfg(mshv3)]
231+
#[test]
232+
fn round_trip_mshv_debug_regs() {
233+
let original = common_debug_regs();
234+
let mshv_regs: DebugRegisters = (&original).into();
235+
let converted: CommonDebugRegs = mshv_regs.into();
236+
assert_eq!(original, converted);
237+
}
238+
239+
#[cfg(target_os = "windows")]
240+
#[test]
241+
fn round_trip_whp_debug_regs() {
242+
let original = common_debug_regs();
243+
let whp_regs: [(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>); WHP_DEBUG_REGS_NAMES_LEN] =
244+
(&original).into();
245+
let converted: CommonDebugRegs = whp_regs.as_ref().try_into().unwrap();
246+
assert_eq!(original, converted);
247+
248+
// test for duplicate register error handling
249+
let original = common_debug_regs();
250+
let mut whp_regs: [(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>);
251+
WHP_DEBUG_REGS_NAMES_LEN] = (&original).into();
252+
whp_regs[0].0 = WHvX64RegisterDr1;
253+
let err = CommonDebugRegs::try_from(whp_regs.as_ref()).unwrap_err();
254+
assert_eq!(
255+
err,
256+
FromWhpRegisterError::DuplicateRegister(WHvX64RegisterDr1.0)
257+
);
258+
259+
// test for passing non-standard register (e.g. CR8)
260+
let original = common_debug_regs();
261+
let mut whp_regs: [(WHV_REGISTER_NAME, Align16<WHV_REGISTER_VALUE>);
262+
WHP_DEBUG_REGS_NAMES_LEN] = (&original).into();
263+
whp_regs[0].0 = WHvX64RegisterCr8;
264+
let err = CommonDebugRegs::try_from(whp_regs.as_ref()).unwrap_err();
265+
assert_eq!(
266+
err,
267+
FromWhpRegisterError::InvalidRegister(WHvX64RegisterCr8.0)
268+
);
269+
}
270+
}

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ use std::sync::LazyLock;
1818

1919
#[cfg(gdb)]
2020
use kvm_bindings::kvm_guest_debug;
21-
use kvm_bindings::{kvm_fpu, kvm_regs, kvm_sregs, kvm_userspace_memory_region};
21+
use kvm_bindings::{kvm_debugregs, kvm_fpu, kvm_regs, kvm_sregs, kvm_userspace_memory_region};
2222
use kvm_ioctls::Cap::UserMemory;
2323
use kvm_ioctls::{Kvm, VcpuExit, VcpuFd, VmFd};
2424
use tracing::{Span, instrument};
2525

2626
#[cfg(gdb)]
2727
use crate::hypervisor::gdb::DebuggableVm;
28-
use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters};
28+
use crate::hypervisor::regs::{
29+
CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters,
30+
};
2931
use crate::hypervisor::virtual_machine::{VirtualMachine, VmExit};
3032
use crate::mem::memory_region::MemoryRegion;
3133
use crate::{Result, new_error};
@@ -58,7 +60,7 @@ pub(crate) struct KvmVm {
5860
vm_fd: VmFd,
5961
vcpu_fd: VcpuFd,
6062

61-
// KVM as opposed to mshv/whp has no way to get current debug regs, so need to keep a copy here
63+
// KVM, as opposed to mshv/whp, has no get_guest_debug() ioctl, so we must track the state ourselves
6264
#[cfg(gdb)]
6365
debug_regs: kvm_guest_debug,
6466
}
@@ -161,6 +163,17 @@ impl VirtualMachine for KvmVm {
161163
Ok(())
162164
}
163165

166+
fn debug_regs(&self) -> Result<CommonDebugRegs> {
167+
let kvm_debug_regs = self.vcpu_fd.get_debug_regs()?;
168+
Ok(kvm_debug_regs.into())
169+
}
170+
171+
fn set_debug_regs(&self, drs: &CommonDebugRegs) -> Result<()> {
172+
let kvm_debug_regs: kvm_debugregs = drs.into();
173+
self.vcpu_fd.set_debug_regs(&kvm_debug_regs)?;
174+
Ok(())
175+
}
176+
164177
#[cfg(crashdump)]
165178
fn xsave(&self) -> Result<Vec<u8>> {
166179
let xsave = self.vcpu_fd.get_xsave()?;

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ use std::sync::OnceLock;
2020
use tracing::{Span, instrument};
2121

2222
use crate::Result;
23-
use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters};
23+
use crate::hypervisor::regs::{
24+
CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters,
25+
};
2426
use crate::mem::memory_region::MemoryRegion;
2527

2628
/// KVM (Kernel-based Virtual Machine) functionality (linux)
@@ -164,6 +166,12 @@ pub(crate) trait VirtualMachine: Debug + Send {
164166
fn sregs(&self) -> Result<CommonSpecialRegisters>;
165167
/// Set special regs
166168
fn set_sregs(&self, sregs: &CommonSpecialRegisters) -> Result<()>;
169+
/// Get the debug registers of the vCPU
170+
#[allow(dead_code)]
171+
fn debug_regs(&self) -> Result<CommonDebugRegs>;
172+
/// Set the debug registers of the vCPU
173+
#[allow(dead_code)]
174+
fn set_debug_regs(&self, drs: &CommonDebugRegs) -> Result<()>;
167175

168176
/// xsave
169177
#[cfg(crashdump)]

0 commit comments

Comments
 (0)