Skip to content

Commit 21d3079

Browse files
committed
add CI gdb test
Signed-off-by: Doru Blânzeanu <[email protected]>
1 parent d41a4d9 commit 21d3079

File tree

5 files changed

+244
-0
lines changed

5 files changed

+244
-0
lines changed

.github/workflows/dep_rust.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,13 @@ jobs:
115115
RUST_LOG: debug
116116
run: just run-rust-examples-linux ${{ matrix.config }} ${{ matrix.hypervisor == 'mshv3' && 'mshv3' || ''}}
117117

118+
- name: Run Rust Gdb tests - linux
119+
if: runner.os == 'Linux' && matrix.hypervisor == 'kvm'
120+
env:
121+
CARGO_TERM_COLOR: always
122+
RUST_LOG: debug
123+
run: just test-rust-gdb-debugging ${{ matrix.config }}
124+
118125
### Benchmarks ###
119126
- name: Install github-cli (Linux mariner)
120127
if: runner.os == 'Linux' && matrix.hypervisor == 'mshv'

Justfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ test-rust-feature-compilation-fail target=default-target:
110110
@# the following should fail on linux because one of kvm, mshv, or mshv3 feature must be specified, which is why the exit code is inverted with an !.
111111
{{ if os() == "linux" { "! cargo check -p hyperlight-host --no-default-features 2> /dev/null"} else { "" } }}
112112

113+
# Test rust gdb debugging
114+
test-rust-gdb-debugging target=default-target: (build-rust target)
115+
{{ set-trace-env-vars }} cargo test --profile={{ if target == "debug" { "dev" } else { target } }} --example guest-debugging --features gdb
116+
{{ set-trace-env-vars }} cargo test --profile={{ if target == "debug" { "dev" } else { target } }} --features gdb -- test_gdb
117+
113118
test target=default-target: (test-rust target)
114119

115120
# RUST LINTING

src/hyperlight_host/examples/guest-debugging/main.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,166 @@ fn main() -> hyperlight_host::Result<()> {
8080

8181
Ok(())
8282
}
83+
84+
#[cfg(gdb)]
85+
#[cfg(test)]
86+
mod tests {
87+
use std::fs::File;
88+
use std::io;
89+
use std::process::{Command, Stdio};
90+
use std::time::Duration;
91+
92+
use hyperlight_host::{new_error, Result};
93+
use io::{BufReader, BufWriter, Read, Write};
94+
95+
use super::*;
96+
97+
fn write_cmds_file(cmd_file_path: &str, out_file_path: &str) -> io::Result<()> {
98+
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get manifest dir");
99+
let file = File::create(cmd_file_path)?;
100+
let mut writer = BufWriter::new(file);
101+
102+
// write from string to file
103+
writer.write_all(
104+
format!(
105+
"file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest
106+
target remote :8080
107+
108+
set pagination off
109+
set logging file {out_file_path}
110+
set logging enabled on
111+
112+
break hyperlight_main
113+
commands
114+
echo \"Stopped at hyperlight_main breakpoint\\n\"
115+
backtrace
116+
continue
117+
end
118+
119+
continue
120+
121+
set logging enabled off
122+
quit
123+
"
124+
)
125+
.as_bytes(),
126+
)?;
127+
128+
writer.flush()
129+
}
130+
131+
fn run_guest_and_gdb(cmd_file_path: &str, out_file_path: &str) -> Result<()> {
132+
// write gdb commands to file
133+
134+
write_cmds_file(&cmd_file_path, &out_file_path)
135+
.expect("Failed to write gdb commands to file");
136+
137+
let mut guest_child = Command::new("cargo")
138+
.arg("run")
139+
.arg("--example")
140+
.arg("guest-debugging")
141+
.arg("--features")
142+
.arg("gdb")
143+
.stdin(Stdio::piped())
144+
.stdout(Stdio::piped())
145+
.spawn()
146+
.map_err(|e| new_error!("Failed to start guest process: {}", e))?;
147+
148+
// wait 3 seconds for the gdb to connect
149+
thread::sleep(Duration::from_secs(3));
150+
151+
let mut gdb = Command::new("rust-gdb")
152+
.arg("--nw")
153+
.arg("-x")
154+
.arg(cmd_file_path)
155+
.spawn()
156+
.map_err(|e| new_error!("Failed to start gdb process: {}", e))?;
157+
158+
// wait 3 seconds for the gdb to connect
159+
thread::sleep(Duration::from_secs(10));
160+
161+
// check if the guest process has finished
162+
match guest_child.try_wait() {
163+
Ok(Some(status)) => {
164+
if !status.success() {
165+
Err(new_error!(
166+
"Guest process exited with non-zero status: {}",
167+
status
168+
))?;
169+
}
170+
}
171+
Ok(None) => {
172+
guest_child
173+
.kill()
174+
.map_err(|e| new_error!("Failed to kill child process: {}", e))?;
175+
}
176+
Err(e) => {
177+
Err(new_error!("error attempting to wait guest: {}", e))?;
178+
}
179+
}
180+
181+
// check if the gdb process has finished
182+
match gdb.try_wait() {
183+
Ok(Some(status)) => {
184+
if !status.success() {
185+
Err(new_error!(
186+
"Gdb process exited with non-zero status: {}",
187+
status
188+
))?;
189+
}
190+
}
191+
Ok(None) => {
192+
gdb.kill()
193+
.map_err(|e| new_error!("Failed to kill guest process: {}", e))?;
194+
}
195+
Err(e) => {
196+
Err(new_error!("error attempting to wait gdb: {}", e))?;
197+
}
198+
}
199+
200+
check_output(&out_file_path)
201+
}
202+
203+
fn check_output(out_file_path: &str) -> Result<()> {
204+
let results = File::open(out_file_path)
205+
.map_err(|e| new_error!("Failed to open gdb.output file: {}", e))?;
206+
let mut reader = BufReader::new(results);
207+
let mut contents = String::new();
208+
reader.read_to_string(&mut contents).unwrap();
209+
210+
if contents.contains("Stopped at hyperlight_main breakpoint") {
211+
Ok(())
212+
} else {
213+
Err(new_error!(
214+
"Failed to find expected output in gdb.output file"
215+
))
216+
}
217+
}
218+
219+
fn cleanup(out_file_path: &str, cmd_file_path: &str) -> Result<()> {
220+
let res1 = std::fs::remove_file(out_file_path)
221+
.map_err(|e| new_error!("Failed to remove gdb.output file: {}", e));
222+
let res2 = std::fs::remove_file(cmd_file_path)
223+
.map_err(|e| new_error!("Failed to remove gdb-commands.txt file: {}", e));
224+
225+
res1?;
226+
res2?;
227+
228+
Ok(())
229+
}
230+
231+
#[test]
232+
fn test_gdb_end_to_end() {
233+
let out_dir = std::env::var("OUT_DIR").expect("Failed to get out dir");
234+
let out_file_path = format!("{out_dir}/gdb.output");
235+
let cmd_file_path = format!("{out_dir}/gdb-commands.txt");
236+
237+
let result = run_guest_and_gdb(&cmd_file_path, &out_file_path);
238+
239+
// cleanup
240+
let cleanup_result = cleanup(&out_file_path, &cmd_file_path);
241+
assert!(cleanup_result.is_ok(), "{}", cleanup_result.unwrap_err());
242+
// check if the test passed - done at the end to ensure cleanup is done
243+
assert!(result.is_ok(), "{}", result.unwrap_err());
244+
}
245+
}

src/hyperlight_host/src/hypervisor/gdb/mod.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,29 @@ pub fn create_gdb_thread(
208208

209209
Ok(gdb_conn)
210210
}
211+
212+
#[cfg(test)]
213+
mod tests {
214+
use super::*;
215+
216+
#[test]
217+
fn test_gdb_debug_comm_channel() {
218+
let (gdb_conn, hyp_conn) = DebugCommChannel::<DebugMsg, DebugResponse>::unbounded();
219+
220+
let msg = DebugMsg::ReadRegisters;
221+
let res = gdb_conn.send(msg);
222+
assert!(res.is_ok());
223+
224+
let res = hyp_conn.recv();
225+
assert!(res.is_ok());
226+
227+
let res = gdb_conn.try_recv();
228+
assert!(res.is_err());
229+
230+
let res = hyp_conn.send(DebugResponse::ReadRegisters(X86_64Regs::default()));
231+
assert!(res.is_ok());
232+
233+
let res = gdb_conn.recv();
234+
assert!(res.is_ok());
235+
}
236+
}

src/hyperlight_host/src/hypervisor/gdb/x86_64_target.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,3 +393,46 @@ impl SingleThreadSingleStep for HyperlightSandboxTarget {
393393
}
394394
}
395395
}
396+
397+
#[cfg(test)]
398+
mod tests {
399+
use gdbstub_arch::x86::reg::X86_64CoreRegs;
400+
401+
use super::*;
402+
403+
#[test]
404+
fn test_gdb_target() {
405+
let (gdb_conn, hyp_conn) = DebugCommChannel::unbounded();
406+
407+
let mut target = HyperlightSandboxTarget::new(hyp_conn);
408+
409+
// Check response to read registers - send the response first to not be blocked
410+
// by the recv call in the target
411+
let msg = DebugResponse::ReadRegisters(X86_64Regs::default());
412+
let res = gdb_conn.send(msg);
413+
assert!(res.is_ok());
414+
415+
let mut regs = X86_64CoreRegs::default();
416+
assert!(
417+
target.read_registers(&mut regs).is_ok(),
418+
"Failed to read registers"
419+
);
420+
421+
// Check response to write registers
422+
let msg = DebugResponse::WriteRegisters;
423+
let res = gdb_conn.send(msg);
424+
assert!(res.is_ok());
425+
assert!(
426+
target.write_registers(&regs).is_ok(),
427+
"Failed to write registers"
428+
);
429+
430+
// Check response when the channel is dropped
431+
drop(gdb_conn);
432+
assert!(
433+
target.read_registers(&mut regs).is_err(),
434+
"Succeeded to read registers when
435+
expected to fail"
436+
);
437+
}
438+
}

0 commit comments

Comments
 (0)