Skip to content

Commit bf8362c

Browse files
committed
Initial support for gdb debugging in Hyperlight KVM guest
- The current implementation supports only 4 hardware breakpoints. - There might be some bugs, testing is still needed Signed-off-by: Doru Blânzeanu <[email protected]>
1 parent b282110 commit bf8362c

File tree

13 files changed

+971
-113
lines changed

13 files changed

+971
-113
lines changed

Cargo.lock

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/hyperlight_host/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ rust-embed = { version = "8.3.0", features = ["debug-embed", "include-exclude",
6969
sha256 = "1.4.0"
7070

7171
[target.'cfg(unix)'.dependencies]
72+
gdbstub = "0.7.3"
73+
gdbstub_arch = "0.3.1"
7274
seccompiler = { version = "0.4.0", optional = true }
7375
mshv-bindings = { workspace = true, optional = true }
7476
mshv-ioctls = { workspace = true, optional = true }
@@ -113,7 +115,7 @@ cfg_aliases = "0.2.1"
113115
built = { version = "0.7.0", features = ["chrono", "git2"] }
114116

115117
[features]
116-
default = ["kvm", "mshv", "seccomp"]
118+
default = ["gdb", "kvm", "mshv", "seccomp"]
117119
seccomp = ["dep:seccompiler"]
118120
function_call_metrics = []
119121
executable_heap = []
@@ -123,6 +125,8 @@ crashdump = ["dep:tempfile"] # Dumps the VM state to a file on unexpected errors
123125
kvm = ["dep:kvm-bindings", "dep:kvm-ioctls"]
124126
mshv = ["dep:mshv-bindings", "dep:mshv-ioctls"]
125127
inprocess = []
128+
# This enables compilation of gdb stub for easy debug in the guest
129+
gdb = []
126130

127131
[[bench]]
128132
name = "benchmarks"

src/hyperlight_host/build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ fn main() -> Result<()> {
8989
// Essentially the kvm and mshv features are ignored on windows as long as you use #[cfg(kvm)] and not #[cfg(feature = "kvm")].
9090
// You should never use #[cfg(feature = "kvm")] or #[cfg(feature = "mshv")] in the codebase.
9191
cfg_aliases::cfg_aliases! {
92+
gdb: { all(feature = "gdb", debug_assertions, target_os = "linux") },
9293
kvm: { all(feature = "kvm", target_os = "linux") },
9394
mshv: { all(feature = "mshv", target_os = "linux") },
9495
// inprocess feature is aliased with debug_assertions to make it only available in debug-builds.

src/hyperlight_host/examples/hello-world/main.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,28 @@ limitations under the License.
1616

1717
use std::sync::{Arc, Mutex};
1818
use std::thread;
19+
use std::time::Duration;
1920

2021
use hyperlight_common::flatbuffer_wrappers::function_types::{ParameterValue, ReturnType};
2122
use hyperlight_host::func::HostFunction0;
23+
use hyperlight_host::sandbox::SandboxConfiguration;
2224
use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox;
2325
use hyperlight_host::sandbox_state::transition::Noop;
2426
use hyperlight_host::{MultiUseSandbox, UninitializedSandbox};
2527

2628
fn main() -> hyperlight_host::Result<()> {
2729
// Create an uninitialized sandbox with a guest binary
30+
let mut cfg = SandboxConfiguration::default();
31+
cfg.set_max_execution_time(Duration::from_secs(0xffff_ffff_ffff));
32+
cfg.set_max_initialization_time(Duration::from_secs(0xffff_ffff_ffff));
33+
2834
let mut uninitialized_sandbox = UninitializedSandbox::new(
2935
hyperlight_host::GuestBinary::FilePath(
3036
hyperlight_testing::simple_guest_as_string().unwrap(),
3137
),
32-
None, // default configuration
33-
None, // default run options
34-
None, // default host print function
38+
Some(cfg), // default configuration
39+
None, // default run options
40+
None, // default host print function
3541
)?;
3642

3743
// Register a host functions
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
use gdbstub::common::Signal;
2+
use gdbstub::conn::ConnectionExt;
3+
use gdbstub::stub::run_blocking::{self, WaitForStopReasonError};
4+
use gdbstub::stub::{DisconnectReason, GdbStub, SingleThreadStopReason};
5+
6+
use super::target::HyperlightKvmSandboxTarget;
7+
use super::GdbTargetError;
8+
use crate::hypervisor::gdb::GdbDebug;
9+
10+
pub struct GdbBlockingEventLoop;
11+
12+
impl run_blocking::BlockingEventLoop for GdbBlockingEventLoop {
13+
type Connection = Box<dyn ConnectionExt<Error = std::io::Error>>;
14+
type StopReason = SingleThreadStopReason<u64>;
15+
type Target = HyperlightKvmSandboxTarget;
16+
17+
fn wait_for_stop_reason(
18+
target: &mut Self::Target,
19+
conn: &mut Self::Connection,
20+
) -> Result<
21+
run_blocking::Event<Self::StopReason>,
22+
run_blocking::WaitForStopReasonError<
23+
<Self::Target as gdbstub::target::Target>::Error,
24+
<Self::Connection as gdbstub::conn::Connection>::Error,
25+
>,
26+
> {
27+
loop {
28+
match target.try_recv() {
29+
Ok(_) => {
30+
target.pause_vcpu();
31+
32+
// Get the stop reason from the target
33+
let stop_reason = target
34+
.get_stop_reason()
35+
.map_err(WaitForStopReasonError::Target)?;
36+
37+
// Resume execution if unknown reason for stop
38+
let Some(stop_response) = stop_reason else {
39+
target
40+
.resume_vcpu()
41+
.map_err(WaitForStopReasonError::Target)?;
42+
43+
continue;
44+
};
45+
46+
return Ok(run_blocking::Event::TargetStopped(stop_response));
47+
}
48+
Err(crossbeam_channel::TryRecvError::Empty) => (),
49+
Err(_) => {
50+
return Err(run_blocking::WaitForStopReasonError::Target(
51+
GdbTargetError::GdbQueueError,
52+
));
53+
}
54+
}
55+
56+
if conn.peek().map(|b| b.is_some()).unwrap_or(false) {
57+
let byte = conn
58+
.read()
59+
.map_err(run_blocking::WaitForStopReasonError::Connection)?;
60+
61+
return Ok(run_blocking::Event::IncomingData(byte));
62+
}
63+
}
64+
}
65+
66+
fn on_interrupt(
67+
target: &mut Self::Target,
68+
) -> Result<Option<Self::StopReason>, <Self::Target as gdbstub::target::Target>::Error> {
69+
target.pause_vcpu();
70+
71+
Ok(Some(SingleThreadStopReason::SignalWithThread {
72+
tid: (),
73+
signal: Signal::SIGINT,
74+
}))
75+
}
76+
}
77+
78+
pub fn event_loop_thread(
79+
debugger: GdbStub<HyperlightKvmSandboxTarget, Box<dyn ConnectionExt<Error = std::io::Error>>>,
80+
mut target: HyperlightKvmSandboxTarget,
81+
) {
82+
match debugger.run_blocking::<GdbBlockingEventLoop>(&mut target) {
83+
Ok(disconnect_reason) => match disconnect_reason {
84+
DisconnectReason::Disconnect => log::info!("Gdb client disconnected"),
85+
DisconnectReason::TargetExited(code) => {
86+
log::info!("Gdb target exited with code {}", code)
87+
}
88+
DisconnectReason::TargetTerminated(sig) => {
89+
log::info!("Gdb target terminated with signale {}", sig)
90+
}
91+
DisconnectReason::Kill => log::info!("Gdb sent a kill command"),
92+
},
93+
Err(e) => {
94+
if e.is_target_error() {
95+
log::error!("Target encountered a fatal error: {e:?}");
96+
} else if e.is_connection_error() {
97+
log::error!("connection error: {:?}", e);
98+
} else {
99+
log::error!("gdbstub got a fatal error {:?}", e);
100+
}
101+
}
102+
}
103+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
mod event_loop;
2+
pub mod target;
3+
4+
use std::net::TcpListener;
5+
use std::thread;
6+
7+
use crossbeam_channel::{Receiver, Sender, TryRecvError};
8+
use event_loop::event_loop_thread;
9+
use gdbstub::conn::ConnectionExt;
10+
use gdbstub::stub::GdbStub;
11+
use target::HyperlightKvmSandboxTarget;
12+
13+
#[allow(dead_code)]
14+
#[derive(Debug)]
15+
pub enum GdbTargetError {
16+
/// Error to set breakpoint
17+
GdbBindError,
18+
GdbBreakpointError,
19+
GdbInstructionPointerError,
20+
GdbListenerError,
21+
GdbQueueError,
22+
GdbReadRegistersError,
23+
GdbReceiveMsgError,
24+
GdbResumeError,
25+
GdbSendMsgError,
26+
GdbSetGuestDebugError,
27+
GdbSpawnThreadError,
28+
GdbStepError,
29+
GdbTranslateGvaError,
30+
GdbUnexpectedMessageError,
31+
GdbWriteRegistersError,
32+
}
33+
34+
/// Trait that provides common communication methods for targets
35+
pub trait GdbDebug {
36+
/// Sends a message to the Hypervisor
37+
fn send(&self, ev: DebugMessage) -> Result<(), GdbTargetError>;
38+
/// Waits for a message from the Hypervisor
39+
fn recv(&self) -> Result<DebugMessage, GdbTargetError>;
40+
/// Checks for a pending message from the Hypervisor
41+
fn try_recv(&self) -> Result<DebugMessage, TryRecvError>;
42+
}
43+
44+
/// Event sent to the VCPU execution loop
45+
#[derive(Debug)]
46+
pub enum DebugMessage {
47+
/// VCPU stopped in debug
48+
VcpuStoppedEv,
49+
/// Resume VCPU execution
50+
VcpuResumeEv,
51+
/// Response ok
52+
VcpuOk,
53+
/// Response error
54+
VcpuErr,
55+
}
56+
57+
/// Type that takes care of communication between Hypervisor and Gdb
58+
pub struct GdbConnection {
59+
/// Transmit channel
60+
tx: Sender<DebugMessage>,
61+
/// Receive channel
62+
rx: Receiver<DebugMessage>,
63+
}
64+
65+
impl GdbConnection {
66+
pub fn new_pair() -> (Self, Self) {
67+
let (hyp_tx, gdb_rx) = crossbeam_channel::unbounded();
68+
let (gdb_tx, hyp_rx) = crossbeam_channel::unbounded();
69+
70+
let gdb_conn = GdbConnection {
71+
tx: gdb_tx,
72+
rx: gdb_rx,
73+
};
74+
75+
let hyp_conn = GdbConnection {
76+
tx: hyp_tx,
77+
rx: hyp_rx,
78+
};
79+
80+
(gdb_conn, hyp_conn)
81+
}
82+
83+
/// Sends message over the transmit channel
84+
pub fn send(&self, msg: DebugMessage) -> Result<(), GdbTargetError> {
85+
self.tx
86+
.send(msg)
87+
.map_err(|_| GdbTargetError::GdbSendMsgError)
88+
}
89+
90+
/// Waits for a message over the receive channel
91+
pub fn recv(&self) -> Result<DebugMessage, GdbTargetError> {
92+
self.rx
93+
.recv()
94+
.map_err(|_| GdbTargetError::GdbReceiveMsgError)
95+
}
96+
97+
/// Checks whether there's a message waiting on the receive channel
98+
pub fn try_recv(&self) -> Result<DebugMessage, TryRecvError> {
99+
self.rx.try_recv()
100+
}
101+
}
102+
103+
/// Creates a thread that handles gdb protocol
104+
pub fn create_gdb_thread(mut target: HyperlightKvmSandboxTarget) -> Result<(), GdbTargetError> {
105+
// TODO: Address multiple sandboxes scenario
106+
let socket = format!("localhost:{}", 8081);
107+
108+
log::info!("Listening on {:?}", socket);
109+
let listener = TcpListener::bind(socket).map_err(|_| GdbTargetError::GdbBindError)?;
110+
111+
log::info!("Starting GDB thread");
112+
let _handle = thread::Builder::new()
113+
.name("GDB handler".to_string())
114+
.spawn(move || -> Result<(), GdbTargetError> {
115+
log::info!("Waiting for GDB connection ... ");
116+
let (conn, _) = listener
117+
.accept()
118+
.map_err(|_| GdbTargetError::GdbListenerError)?;
119+
let conn: Box<dyn ConnectionExt<Error = std::io::Error>> = Box::new(conn);
120+
let debugger = GdbStub::new(conn);
121+
122+
if let DebugMessage::VcpuStoppedEv = target.recv()? {
123+
target.pause_vcpu();
124+
125+
event_loop_thread(debugger, target);
126+
127+
Ok(())
128+
} else {
129+
Err(GdbTargetError::GdbUnexpectedMessageError)
130+
}
131+
})
132+
.map_err(|_| GdbTargetError::GdbSpawnThreadError)?;
133+
134+
Ok(())
135+
}

0 commit comments

Comments
 (0)