Skip to content

Commit 3543cfb

Browse files
committed
crashdump: allow crashdump toggle at sandbox level
- this allows a user to configure the crash dump feature at sandbox level - create a SandboxRuntimeConfig struct to contain all the configuration a sandbox would need at runtime to avoid passing the information as multiple functions arguments - add unit tests to verify crashdump behavior Signed-off-by: Doru Blânzeanu <[email protected]>
1 parent e88b55f commit 3543cfb

File tree

11 files changed

+503
-262
lines changed

11 files changed

+503
-262
lines changed

.github/workflows/dep_rust.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ jobs:
135135
RUST_LOG: debug
136136
run: just test-rust-gdb-debugging ${{ matrix.config }} ${{ matrix.hypervisor == 'mshv3' && 'mshv3' || ''}}
137137

138+
- name: Run Rust Crashdump tests
139+
env:
140+
CARGO_TERM_COLOR: always
141+
RUST_LOG: debug
142+
run: just test-rust-crashdump ${{ matrix.config }} ${{ matrix.hypervisor == 'mshv3' && 'mshv3' || ''}}
143+
138144
### Benchmarks ###
139145
- name: Install github-cli (Linux mariner)
140146
if: runner.os == 'Linux' && matrix.hypervisor == 'mshv'

Justfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ test-like-ci config=default-target hypervisor="kvm":
7272
@# without any driver (should fail to compile)
7373
just test-compilation-fail {{config}}
7474

75+
@# test the crashdump feature
76+
just test-rust-crashdump {{config}}
77+
7578
# runs all tests
7679
test target=default-target features="": (test-unit target features) (test-isolated target features) (test-integration "rust" target features) (test-integration "c" target features) (test-seccomp target features)
7780

@@ -116,6 +119,10 @@ test-rust-gdb-debugging target=default-target features="":
116119
cargo test --profile={{ if target == "debug" { "dev" } else { target } }} --example guest-debugging {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }}
117120
cargo test --profile={{ if target == "debug" { "dev" } else { target } }} {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }} -- test_gdb
118121

122+
# rust test for crashdump
123+
test-rust-crashdump target=default-target features="":
124+
cargo test --profile={{ if target == "debug" { "dev" } else { target } }} {{ if features =="" {'--features crashdump'} else { "--features crashdump," + features } }} -- test_crashdump
125+
119126

120127
################
121128
### LINTING ####

src/hyperlight_host/src/hypervisor/crashdump.rs

Lines changed: 198 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::cmp::min;
2+
use std::io::Write;
23

34
use chrono;
45
use elfcore::{
@@ -232,44 +233,78 @@ impl ReadProcessMemory for GuestMemReader {
232233
}
233234
}
234235

235-
/// Create core dump file from the hypervisor information
236+
/// Create core dump file from the hypervisor information if the sandbox is configured
237+
/// to allow core dumps.
236238
///
237239
/// This function generates an ELF core dump file capturing the hypervisor's state,
238-
/// which can be used for debugging when crashes occur. The file is created in the
239-
/// system's temporary directory with extension '.elf' and the path is printed to stdout and logs.
240+
/// which can be used for debugging when crashes occur.
241+
/// The location of the core dump file is determined by the `HYPERLIGHT_CORE_DUMP_DIR`
242+
/// environment variable. If not set, it defaults to the system's temporary directory.
240243
///
241244
/// # Arguments
242245
/// * `hv`: Reference to the hypervisor implementation
243246
///
244247
/// # Returns
245248
/// * `Result<()>`: Success or error
246-
pub(crate) fn crashdump_to_tempfile(hv: &dyn Hypervisor) -> Result<()> {
249+
pub(crate) fn generate_crashdump(hv: &dyn Hypervisor) -> Result<()> {
247250
log::info!("Creating core dump file...");
248251

249252
// Get crash context from hypervisor
250253
let ctx = hv
251254
.crashdump_context()
252255
.map_err(|e| new_error!("Failed to get crashdump context: {:?}", e))?;
253256

254-
// Set up data sources for the core dump
255-
let guest_view = GuestView::new(&ctx);
256-
let memory_reader = GuestMemReader::new(&ctx);
257+
// Get env variable for core dump directory
258+
let core_dump_dir = std::env::var("HYPERLIGHT_CORE_DUMP_DIR").ok();
257259

258-
// Create and write core dump
259-
let core_builder = CoreDumpBuilder::from_source(guest_view, memory_reader);
260+
// Compute file path on the filesystem
261+
let file_path = core_dump_file_path(core_dump_dir);
260262

263+
let create_dump_file = || {
264+
// Create the file
265+
Ok(Box::new(
266+
std::fs::File::create(&file_path)
267+
.map_err(|e| new_error!("Failed to create core dump file: {:?}", e))?,
268+
) as Box<dyn Write>)
269+
};
270+
271+
checked_core_dump(ctx, create_dump_file).map(|_| {
272+
println!("Core dump created successfully: {}", file_path);
273+
log::error!("Core dump file: {}", file_path);
274+
})
275+
}
276+
277+
/// Computes the file path for the core dump file.
278+
///
279+
/// The file path is generated based on the current timestamp and an
280+
/// output directory.
281+
/// If the directory does not exist, it falls back to the system's temp directory.
282+
/// If the variable is not set, it defaults to the system's temporary directory.
283+
/// The filename is formatted as `hl_core_<timestamp>.elf`.
284+
///
285+
/// Arguments:
286+
/// * `dump_dir`: The environment variable value to check for the output directory.
287+
///
288+
/// Returns:
289+
/// * `String`: The file path for the core dump file.
290+
fn core_dump_file_path(dump_dir: Option<String>) -> String {
261291
// Generate timestamp string for the filename using chrono
262292
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S").to_string();
263293

264294
// Determine the output directory based on environment variable
265-
let output_dir = if let Ok(dump_dir) = std::env::var("HYPERLIGHT_CORE_DUMP_DIR") {
266-
// Create the directory if it doesn't exist
267-
let path = std::path::Path::new(&dump_dir);
268-
if !path.exists() {
269-
std::fs::create_dir_all(path)
270-
.map_err(|e| new_error!("Failed to create core dump directory: {:?}", e))?;
295+
let output_dir = if let Some(dump_dir) = dump_dir {
296+
// Check if the directory exists
297+
// If it doesn't exist, fall back to the system temp directory
298+
// This is to ensure that the core dump can be created even if the directory is not set
299+
if std::path::Path::new(&dump_dir).exists() {
300+
std::path::PathBuf::from(dump_dir)
301+
} else {
302+
log::warn!(
303+
"Directory \"{}\" does not exist, falling back to temp directory",
304+
dump_dir
305+
);
306+
std::env::temp_dir()
271307
}
272-
std::path::PathBuf::from(dump_dir)
273308
} else {
274309
// Fall back to the system temp directory
275310
std::env::temp_dir()
@@ -279,19 +314,155 @@ pub(crate) fn crashdump_to_tempfile(hv: &dyn Hypervisor) -> Result<()> {
279314
let filename = format!("hl_core_{}.elf", timestamp);
280315
let file_path = output_dir.join(filename);
281316

282-
// Create the file
283-
let file = std::fs::File::create(&file_path)
284-
.map_err(|e| new_error!("Failed to create core dump file: {:?}", e))?;
317+
file_path.to_string_lossy().to_string()
318+
}
285319

286-
// Write the core dump directly to the file
287-
core_builder
288-
.write(&file)
289-
.map_err(|e| new_error!("Failed to write core dump: {:?}", e))?;
320+
/// Create core dump from Hypervisor context if the sandbox is configured to allow core dumps.
321+
///
322+
/// Arguments:
323+
/// * `ctx`: Optional crash dump context from the hypervisor. This contains the information
324+
/// needed to create the core dump. If `None`, no core dump will be created.
325+
/// * `get_writer`: Closure that returns a writer to the output destination.
326+
///
327+
/// Returns:
328+
/// * `Result<usize>`: The number of bytes written to the core dump file.
329+
fn checked_core_dump(
330+
ctx: Option<CrashDumpContext>,
331+
get_writer: impl FnOnce() -> Result<Box<dyn Write>>,
332+
) -> Result<usize> {
333+
let mut nbytes = 0;
334+
// If the HV returned a context it means we can create a core dump
335+
// This is the case when the sandbox has been configured at runtime to allow core dumps
336+
if let Some(ctx) = ctx {
337+
// Set up data sources for the core dump
338+
let guest_view = GuestView::new(&ctx);
339+
let memory_reader = GuestMemReader::new(&ctx);
340+
341+
// Create and write core dump
342+
let core_builder = CoreDumpBuilder::from_source(guest_view, memory_reader);
343+
344+
let writer = get_writer()?;
345+
// Write the core dump directly to the file
346+
nbytes = core_builder
347+
.write(writer)
348+
.map_err(|e| new_error!("Failed to write core dump: {:?}", e))?;
349+
}
290350

291-
let path_string = file_path.to_string_lossy().to_string();
351+
Ok(nbytes)
352+
}
353+
354+
/// Test module for the crash dump functionality
355+
#[cfg(test)]
356+
mod test {
357+
use super::*;
358+
359+
/// Test the core_dump_file_path function when the environment variable is set to an existing
360+
/// directory
361+
#[test]
362+
fn test_crashdump_file_path_valid() {
363+
// Get CWD
364+
let valid_dir = std::env::current_dir()
365+
.unwrap()
366+
.to_string_lossy()
367+
.to_string();
368+
369+
// Call the function
370+
let path = core_dump_file_path(Some(valid_dir.clone()));
371+
372+
// Check if the path is correct
373+
assert!(path.contains(&valid_dir));
374+
}
292375

293-
println!("Core dump created successfully: {}", path_string);
294-
log::error!("Core dump file: {}", path_string);
376+
/// Test the core_dump_file_path function when the environment variable is set to an invalid
377+
/// directory
378+
#[test]
379+
fn test_crashdump_file_path_invalid() {
380+
// Call the function
381+
let path = core_dump_file_path(Some("/tmp/not_existing_dir".to_string()));
295382

296-
Ok(())
383+
// Get the temp directory
384+
let temp_dir = std::env::temp_dir().to_string_lossy().to_string();
385+
386+
// Check if the path is correct
387+
assert!(path.contains(&temp_dir));
388+
}
389+
390+
/// Test the core_dump_file_path function when the environment is not set
391+
/// Check against the default temp directory by using the env::temp_dir() function
392+
#[test]
393+
fn test_crashdump_file_path_default() {
394+
// Call the function
395+
let path = core_dump_file_path(None);
396+
397+
let temp_dir = std::env::temp_dir().to_string_lossy().to_string();
398+
399+
// Check if the path is correct
400+
assert!(path.starts_with(&temp_dir));
401+
}
402+
403+
/// Test core is not created when the context is None
404+
#[test]
405+
fn test_crashdump_not_created_when_context_is_none() {
406+
// Call the function with None context
407+
let result = checked_core_dump(None, || Ok(Box::new(std::io::empty())));
408+
409+
// Check if the result is ok and the number of bytes is 0
410+
assert!(result.is_ok());
411+
assert_eq!(result.unwrap(), 0);
412+
}
413+
414+
/// Test the core dump creation with no regions fails
415+
#[test]
416+
fn test_crashdump_write_fails_when_no_regions() {
417+
// Create a dummy context
418+
let ctx = CrashDumpContext::new(
419+
&[],
420+
[0; 27],
421+
vec![],
422+
0,
423+
Some("dummy_binary".to_string()),
424+
Some("dummy_filename".to_string()),
425+
);
426+
427+
let get_writer = || Ok(Box::new(std::io::empty()) as Box<dyn Write>);
428+
429+
// Call the function
430+
let result = checked_core_dump(Some(ctx), get_writer);
431+
432+
// Check if the result is an error
433+
// This should fail because there are no regions
434+
assert!(result.is_err());
435+
}
436+
437+
/// Check core dump with a dummy region to local vec
438+
/// This test checks if the core dump is created successfully
439+
#[test]
440+
fn test_crashdump_dummy_core_dump() {
441+
let dummy_vec = vec![0; 0x1000];
442+
let regions = vec![MemoryRegion {
443+
guest_region: 0x1000..0x2000,
444+
host_region: dummy_vec.as_ptr() as usize..dummy_vec.as_ptr() as usize + dummy_vec.len(),
445+
flags: MemoryRegionFlags::READ | MemoryRegionFlags::WRITE,
446+
region_type: crate::mem::memory_region::MemoryRegionType::Code,
447+
}];
448+
// Create a dummy context
449+
let ctx = CrashDumpContext::new(
450+
&regions,
451+
[0; 27],
452+
vec![],
453+
0x1000,
454+
Some("dummy_binary".to_string()),
455+
Some("dummy_filename".to_string()),
456+
);
457+
458+
let get_writer = || Ok(Box::new(std::io::empty()) as Box<dyn Write>);
459+
460+
// Call the function
461+
let result = checked_core_dump(Some(ctx), get_writer);
462+
463+
// Check if the result is ok and the number of bytes is 0
464+
assert!(result.is_ok());
465+
// Check the number of bytes written is more than 0x1000 (the size of the region)
466+
assert_eq!(result.unwrap(), 0x2000);
467+
}
297468
}

0 commit comments

Comments
 (0)