Skip to content

Commit 3b8f97c

Browse files
committed
crashdump: create core dump file when a guest crashes
- the core dump file is an ELF file with special segments that describe the guest's memory when it crashed, the CPU register's values and other special notes that tell the debugger how to set up a debugging session starting from the core dump Signed-off-by: Doru Blânzeanu <[email protected]>
1 parent 8c73c32 commit 3b8f97c

File tree

8 files changed

+386
-37
lines changed

8 files changed

+386
-37
lines changed

Cargo.lock

Lines changed: 25 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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ tempfile = { version = "3.19", optional = true }
4646
serde_yaml = "0.9"
4747
anyhow = "1.0"
4848
metrics = "0.24.2"
49+
elfcore = { git = "https://github.com/dblnz/elfcore.git", branch = "split-linux-impl-from-elfcore" }
4950

5051
[target.'cfg(windows)'.dependencies]
5152
windows = { version = "0.61", features = [
@@ -118,7 +119,7 @@ cfg_aliases = "0.2.1"
118119
built = { version = "0.7.7", features = ["chrono", "git2"] }
119120

120121
[features]
121-
default = ["kvm", "mshv2", "seccomp"]
122+
default = ["crashdump", "kvm", "mshv2", "seccomp"]
122123
seccomp = ["dep:seccompiler"]
123124
function_call_metrics = []
124125
executable_heap = []
Lines changed: 221 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,240 @@
1-
use std::io::Write;
1+
use std::cmp::min;
22

3+
use elfcore::{
4+
ArchComponentState, ArchState, CoreDumpBuilder, CoreError, Elf64_Auxv, ProcessInfoSource,
5+
ReadProcessMemory, ThreadView, VaProtection, VaRegion,
6+
};
37
use tempfile::NamedTempFile;
48

59
use super::Hypervisor;
10+
use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags};
611
use crate::{new_error, Result};
712

8-
/// Dump registers + memory regions + raw memory to a tempfile
9-
#[cfg(crashdump)]
10-
pub(crate) fn crashdump_to_tempfile(hv: &dyn Hypervisor) -> Result<()> {
11-
let mut temp_file = NamedTempFile::with_prefix("mem")?;
12-
let hv_details = format!("{:#x?}", hv);
13+
// amd64 notes
14+
pub const NT_X86_XSTATE: u32 = 0x202;
1315

14-
// write hypervisor details such as registers, info about mapped memory regions, etc.
15-
temp_file.write_all(hv_details.as_bytes())?;
16-
temp_file.write_all(b"================ MEMORY DUMP =================\n")?;
16+
#[derive(Debug)]
17+
pub(crate) struct CrashDumpContext<'a> {
18+
regions: &'a [MemoryRegion],
19+
regs: [u64; 27],
20+
xsave: Vec<u8>,
21+
entry: u64,
22+
}
1723

18-
// write the raw memory dump for each memory region
19-
for region in hv.get_memory_regions() {
20-
if region.host_region.start == 0 || region.host_region.is_empty() {
21-
continue;
24+
impl<'a> CrashDumpContext<'a> {
25+
pub(crate) fn new(
26+
regions: &'a [MemoryRegion],
27+
regs: [u64; 27],
28+
xsave: Vec<u8>,
29+
entry: u64,
30+
) -> Self {
31+
Self {
32+
regions,
33+
regs,
34+
xsave,
35+
entry,
2236
}
23-
// SAFETY: we got this memory region from the hypervisor so should never be invalid
24-
let region_slice = unsafe {
25-
std::slice::from_raw_parts(
26-
region.host_region.start as *const u8,
27-
region.host_region.len(),
28-
)
37+
}
38+
}
39+
40+
struct GuestView {
41+
regions: Vec<VaRegion>,
42+
threads: Vec<ThreadView>,
43+
aux_vector: Vec<elfcore::Elf64_Auxv>,
44+
}
45+
46+
impl GuestView {
47+
fn new(ctx: &CrashDumpContext) -> Self {
48+
let regions = ctx
49+
.regions
50+
.iter()
51+
.filter(|r| !r.host_region.is_empty())
52+
.map(|r| VaRegion {
53+
begin: r.guest_region.start as u64,
54+
end: r.guest_region.end as u64,
55+
offset: r.host_region.start as u64,
56+
protection: VaProtection {
57+
is_private: false,
58+
read: r.flags.contains(MemoryRegionFlags::READ),
59+
write: r.flags.contains(MemoryRegionFlags::WRITE),
60+
execute: r.flags.contains(MemoryRegionFlags::EXECUTE),
61+
},
62+
mapped_file_name: None,
63+
})
64+
.collect();
65+
66+
let mut components = vec![];
67+
if !ctx.xsave.is_empty() {
68+
components.push(ArchComponentState {
69+
name: "XSAVE",
70+
note_type: NT_X86_XSTATE,
71+
note_name: b"LINUX",
72+
data: ctx.xsave.clone(),
73+
});
74+
}
75+
let thread = ThreadView {
76+
flags: 0, // Kernel flags for the process
77+
tid: 1,
78+
uid: 0, // User ID
79+
gid: 0, // Group ID
80+
comm: "\0".to_string(),
81+
ppid: 0, // Parent PID
82+
pgrp: 0, // Process group ID
83+
nice: 0, // Nice value
84+
state: 0, // Process state
85+
utime: 0, // User time
86+
stime: 0, // System time
87+
cutime: 0, // Children User time
88+
cstime: 0, // Children User time
89+
cursig: 0, // Current signal
90+
session: 0, // Session ID of the process
91+
sighold: 0, // Blocked signal
92+
sigpend: 0, // Pending signal
93+
cmd_line: "\0".to_string(),
94+
95+
arch_state: Box::new(ArchState {
96+
gpr_state: ctx.regs.to_vec(),
97+
components,
98+
}),
2999
};
30-
temp_file.write_all(region_slice)?;
100+
101+
// Create the auxv vector
102+
// The first entry is AT_ENTRY, which is the entry point of the program
103+
// The entry point is the address where the program starts executing
104+
// This helps the debugger to know that the entry is changed by an offset
105+
// so the symbols can be loaded correctly.
106+
// The second entry is AT_NULL, which marks the end of the vector
107+
let auxv = vec![
108+
Elf64_Auxv {
109+
a_type: 9, // AT_ENTRY
110+
a_val: ctx.entry,
111+
},
112+
Elf64_Auxv {
113+
a_type: 0, // AT_NULL
114+
a_val: 0,
115+
},
116+
];
117+
118+
Self {
119+
regions,
120+
threads: vec![thread],
121+
aux_vector: auxv,
122+
}
123+
}
124+
}
125+
126+
impl ProcessInfoSource for GuestView {
127+
fn pid(&self) -> i32 {
128+
1
129+
}
130+
fn threads(&self) -> &[elfcore::ThreadView] {
131+
&self.threads
132+
}
133+
fn page_size(&self) -> usize {
134+
0x1000
135+
}
136+
fn aux_vector(&self) -> Option<&[elfcore::Elf64_Auxv]> {
137+
Some(&self.aux_vector)
138+
}
139+
fn va_regions(&self) -> &[elfcore::VaRegion] {
140+
&self.regions
141+
}
142+
fn mapped_files(&self) -> Option<&[elfcore::MappedFile]> {
143+
None
31144
}
32-
temp_file.flush()?;
145+
}
146+
147+
struct GuestMemReader {
148+
regions: Vec<MemoryRegion>,
149+
}
150+
151+
impl GuestMemReader {
152+
fn new(ctx: &CrashDumpContext) -> Self {
153+
Self {
154+
regions: ctx.regions.to_vec(),
155+
}
156+
}
157+
}
158+
159+
impl ReadProcessMemory for GuestMemReader {
160+
fn read_process_memory(
161+
&mut self,
162+
base: usize,
163+
buf: &mut [u8],
164+
) -> std::result::Result<usize, CoreError> {
165+
for r in self.regions.iter() {
166+
if base >= r.guest_region.start && base < r.guest_region.end {
167+
let offset = base - r.guest_region.start;
168+
let region_slice = unsafe {
169+
std::slice::from_raw_parts(
170+
r.host_region.start as *const u8,
171+
r.host_region.len(),
172+
)
173+
};
174+
175+
// Calculate how much we can copy
176+
let copy_size = min(buf.len(), region_slice.len() - offset);
177+
if copy_size == 0 {
178+
return std::result::Result::Ok(0);
179+
}
180+
181+
// Only copy the amount that fits in both buffers
182+
buf[..copy_size].copy_from_slice(&region_slice[offset..offset + copy_size]);
183+
return std::result::Result::Ok(copy_size);
184+
}
185+
}
186+
187+
// If we reach here, we didn't find a matching region
188+
std::result::Result::Ok(0)
189+
}
190+
}
191+
192+
/// Create core dump file from the hypervisor information
193+
///
194+
/// This function generates an ELF core dump file capturing the hypervisor's state,
195+
/// which can be used for debugging when crashes occur. The file is created in the
196+
/// system's temporary directory with extension '.dmp' and the path is printed to stdout and logs.
197+
///
198+
/// # Arguments
199+
/// * `hv`: Reference to the hypervisor implementation
200+
///
201+
/// # Returns
202+
/// * `Result<()>`: Success or error
203+
pub(crate) fn crashdump_to_tempfile(hv: &dyn Hypervisor) -> Result<()> {
204+
log::info!("Creating core dump file...");
205+
206+
// Create a temporary file with a recognizable prefix
207+
let temp_file = NamedTempFile::with_prefix("hl_core_")
208+
.map_err(|e| new_error!("Failed to create temporary file: {:?}", e))?;
209+
210+
// Get crash context from hypervisor
211+
let ctx = hv
212+
.crashdump_context()
213+
.map_err(|e| new_error!("Failed to get crashdump context: {:?}", e))?;
214+
215+
// Set up data sources for the core dump
216+
let guest_view = GuestView::new(&ctx);
217+
let memory_reader = GuestMemReader::new(&ctx);
218+
219+
// Create and write core dump
220+
let core_builder = CoreDumpBuilder::from_source(
221+
Box::new(guest_view) as Box<dyn ProcessInfoSource>,
222+
Box::new(memory_reader) as Box<dyn ReadProcessMemory>,
223+
);
224+
225+
core_builder
226+
.write(&temp_file)
227+
.map_err(|e| new_error!("Failed to write core dump: {:?}", e))?;
33228

34-
// persist the tempfile to disk
229+
// Persist the file with a .dmp extension
35230
let persist_path = temp_file.path().with_extension("dmp");
36231
temp_file
37232
.persist(&persist_path)
38-
.map_err(|e| new_error!("Failed to persist crashdump file: {:?}", e))?;
233+
.map_err(|e| new_error!("Failed to persist core dump file: {:?}", e))?;
39234

40-
println!("Memory dumped to file: {:?}", persist_path);
41-
log::error!("Memory dumped to file: {:?}", persist_path);
235+
let path_string = persist_path.to_string_lossy().to_string();
236+
println!("Core dump created successfully: {}", path_string);
237+
log::error!("Core dump file: {}", path_string);
42238

43239
Ok(())
44240
}

src/hyperlight_host/src/hypervisor/hyperv_linux.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ use mshv_bindings::{
4949
use mshv_ioctls::{Mshv, VcpuFd, VmFd};
5050
use tracing::{instrument, Span};
5151

52+
#[cfg(crashdump)]
53+
use super::crashdump;
5254
use super::fpu::{FP_CONTROL_WORD_DEFAULT, FP_TAG_WORD_DEFAULT, MXCSR_DEFAULT};
5355
#[cfg(gdb)]
5456
use super::gdb::{DebugCommChannel, DebugMsg, DebugResponse, GuestDebug, MshvDebug};
@@ -658,8 +660,47 @@ impl Hypervisor for HypervLinuxDriver {
658660
}
659661

660662
#[cfg(crashdump)]
661-
fn get_memory_regions(&self) -> &[MemoryRegion] {
662-
&self.mem_regions
663+
fn crashdump_context(&self) -> Result<super::crashdump::CrashDumpContext> {
664+
let mut regs = [0; 27];
665+
666+
let vcpu_regs = self.vcpu_fd.get_regs()?;
667+
let sregs = self.vcpu_fd.get_sregs()?;
668+
let xsave = self.vcpu_fd.get_xsave()?;
669+
670+
regs[0] = vcpu_regs.r15; // r15
671+
regs[1] = vcpu_regs.r14; // r14
672+
regs[2] = vcpu_regs.r13; // r13
673+
regs[3] = vcpu_regs.r12; // r12
674+
regs[4] = vcpu_regs.rbp; // rbp
675+
regs[5] = vcpu_regs.rbx; // rbx
676+
regs[6] = vcpu_regs.r11; // r11
677+
regs[7] = vcpu_regs.r10; // r10
678+
regs[8] = vcpu_regs.r9; // r9
679+
regs[9] = vcpu_regs.r8; // r8
680+
regs[10] = vcpu_regs.rax; // rax
681+
regs[11] = vcpu_regs.rcx; // rcx
682+
regs[12] = vcpu_regs.rdx; // rdx
683+
regs[13] = vcpu_regs.rsi; // rsi
684+
regs[14] = vcpu_regs.rdi; // rdi
685+
regs[15] = 0; // orig rax
686+
regs[16] = vcpu_regs.rip; // rip
687+
regs[17] = sregs.cs.selector as u64; // cs
688+
regs[18] = vcpu_regs.rflags; // eflags
689+
regs[19] = vcpu_regs.rsp; // rsp
690+
regs[20] = sregs.ss.selector as u64; // ss
691+
regs[21] = sregs.fs.base; // fs_base
692+
regs[22] = sregs.gs.base; // gs_base
693+
regs[23] = sregs.ds.selector as u64; // ds
694+
regs[24] = sregs.es.selector as u64; // es
695+
regs[25] = sregs.fs.selector as u64; // fs
696+
regs[26] = sregs.gs.selector as u64; // gs
697+
698+
Ok(crashdump::CrashDumpContext::new(
699+
&self.mem_regions,
700+
regs,
701+
xsave.buffer.to_vec(),
702+
self.entrypoint,
703+
))
663704
}
664705

665706
#[cfg(gdb)]

0 commit comments

Comments
 (0)