use std::cmp::min;
use std::io::Write;
use chrono;
use elfcore::{
ArchComponentState, ArchState, CoreDumpBuilder, CoreError, Elf64_Auxv, ProcessInfoSource,
ReadProcessMemory, ThreadView, VaProtection, VaRegion,
};
use crate::hypervisor::hyperlight_vm::HyperlightVm;
use crate::mem::memory_region::{CrashDumpRegion, MemoryRegionFlags};
use crate::mem::mgr::SandboxMemoryManager;
use crate::mem::shared_mem::HostSharedMemory;
use crate::{Result, new_error};
const NT_X86_XSTATE: u32 = 0x202;
const AT_ENTRY: u64 = 9;
const AT_NULL: u64 = 0;
const CORE_DUMP_PID: i32 = 1;
const CORE_DUMP_PAGE_SIZE: usize = 0x1000;
#[derive(Debug)]
pub(crate) struct CrashDumpContext {
regions: Vec<CrashDumpRegion>,
regs: [u64; 27],
xsave: Vec<u8>,
entry: u64,
binary: Option<String>,
filename: Option<String>,
}
impl CrashDumpContext {
pub(crate) fn new(
regions: Vec<CrashDumpRegion>,
regs: [u64; 27],
xsave: Vec<u8>,
entry: u64,
binary: Option<String>,
filename: Option<String>,
) -> Self {
Self {
regions,
regs,
xsave,
entry,
binary,
filename,
}
}
}
struct GuestView {
regions: Vec<VaRegion>,
threads: Vec<ThreadView>,
aux_vector: Vec<elfcore::Elf64_Auxv>,
}
impl GuestView {
fn new(ctx: &CrashDumpContext) -> Self {
let regions = ctx
.regions
.iter()
.filter(|r| !r.guest_region.is_empty())
.map(|r| VaRegion {
begin: r.guest_region.start as u64,
end: r.guest_region.end as u64,
offset: r.host_region.start as u64,
protection: VaProtection {
is_private: false,
read: r.flags.contains(MemoryRegionFlags::READ),
write: r.flags.contains(MemoryRegionFlags::WRITE),
execute: r.flags.contains(MemoryRegionFlags::EXECUTE),
},
mapped_file_name: None,
})
.collect();
let filename = ctx
.filename
.as_ref()
.map_or("<unknown>".to_string(), |s| s.to_string());
let cmd = ctx
.binary
.as_ref()
.map_or("<unknown>".to_string(), |s| s.to_string());
let mut components = vec![];
if !ctx.xsave.is_empty() {
components.push(ArchComponentState {
name: "XSAVE",
note_type: NT_X86_XSTATE,
note_name: b"LINUX",
data: ctx.xsave.clone(),
});
}
let thread = ThreadView {
flags: 0, tid: 1,
uid: 0, gid: 0, comm: filename,
ppid: 0, pgrp: 0, nice: 0, state: 0, utime: 0, stime: 0, cutime: 0, cstime: 0, cursig: 0, session: 0, sighold: 0, sigpend: 0, cmd_line: cmd,
arch_state: Box::new(ArchState {
gpr_state: ctx.regs.to_vec(),
components,
}),
};
let auxv = vec![
Elf64_Auxv {
a_type: AT_ENTRY,
a_val: ctx.entry,
},
Elf64_Auxv {
a_type: AT_NULL,
a_val: 0,
},
];
Self {
regions,
threads: vec![thread],
aux_vector: auxv,
}
}
}
impl ProcessInfoSource for GuestView {
fn pid(&self) -> i32 {
CORE_DUMP_PID
}
fn threads(&self) -> &[elfcore::ThreadView] {
&self.threads
}
fn page_size(&self) -> usize {
CORE_DUMP_PAGE_SIZE
}
fn aux_vector(&self) -> Option<&[elfcore::Elf64_Auxv]> {
Some(&self.aux_vector)
}
fn va_regions(&self) -> &[elfcore::VaRegion] {
&self.regions
}
fn mapped_files(&self) -> Option<&[elfcore::MappedFile]> {
None
}
}
struct GuestMemReader {
regions: Vec<CrashDumpRegion>,
}
impl GuestMemReader {
fn new(ctx: &CrashDumpContext) -> Self {
Self {
regions: ctx.regions.clone(),
}
}
}
impl ReadProcessMemory for GuestMemReader {
fn read_process_memory(
&mut self,
base: usize,
buf: &mut [u8],
) -> std::result::Result<usize, CoreError> {
for r in self.regions.iter() {
if base >= r.guest_region.start && base < r.guest_region.end {
let offset = base - r.guest_region.start;
let region_slice = unsafe {
std::slice::from_raw_parts(
r.host_region.start as *const u8,
r.guest_region.len(),
)
};
let copy_size = min(buf.len(), region_slice.len() - offset);
if copy_size == 0 {
return std::result::Result::Ok(0);
}
buf[..copy_size].copy_from_slice(®ion_slice[offset..offset + copy_size]);
return std::result::Result::Ok(copy_size);
}
}
std::result::Result::Ok(0)
}
}
pub(crate) fn generate_crashdump(
hv: &HyperlightVm,
mem_mgr: &mut SandboxMemoryManager<HostSharedMemory>,
override_dir: Option<String>,
) -> Result<()> {
let ctx = hv
.crashdump_context(mem_mgr)
.map_err(|e| new_error!("Failed to get crashdump context: {:?}", e))?;
let core_dump_dir = override_dir.or_else(|| std::env::var("HYPERLIGHT_CORE_DUMP_DIR").ok());
let file_path = core_dump_file_path(core_dump_dir);
let create_dump_file = || {
Ok(Box::new(
std::fs::File::create(&file_path)
.map_err(|e| new_error!("Failed to create core dump file: {:?}", e))?,
) as Box<dyn Write>)
};
if let Ok(nbytes) = checked_core_dump(ctx, create_dump_file) {
if nbytes > 0 {
println!("Core dump created successfully: {}", file_path);
tracing::error!("Core dump file: {}", file_path);
}
} else {
tracing::error!("Failed to create core dump file");
}
Ok(())
}
fn core_dump_file_path(dump_dir: Option<String>) -> String {
let timestamp = chrono::Local::now()
.format("%Y%m%d_T%H%M%S%.3f")
.to_string();
let output_dir = if let Some(dump_dir) = dump_dir {
if std::path::Path::new(&dump_dir).exists() {
std::path::PathBuf::from(dump_dir)
} else {
tracing::warn!(
"Directory \"{}\" does not exist, falling back to temp directory",
dump_dir
);
std::env::temp_dir()
}
} else {
std::env::temp_dir()
};
let filename = format!("hl_core_{}.elf", timestamp);
let file_path = output_dir.join(filename);
file_path.to_string_lossy().to_string()
}
fn checked_core_dump(
ctx: Option<CrashDumpContext>,
get_writer: impl FnOnce() -> Result<Box<dyn Write>>,
) -> Result<usize> {
let mut nbytes = 0;
if let Some(ctx) = ctx {
tracing::info!("Creating core dump file...");
let guest_view = GuestView::new(&ctx);
let memory_reader = GuestMemReader::new(&ctx);
let core_builder = CoreDumpBuilder::from_source(guest_view, memory_reader);
let writer = get_writer()?;
nbytes = core_builder
.write(writer)
.map_err(|e| new_error!("Failed to write core dump: {:?}", e))?;
}
Ok(nbytes)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_crashdump_file_path_valid() {
let valid_dir = std::env::current_dir()
.unwrap()
.to_string_lossy()
.to_string();
let path = core_dump_file_path(Some(valid_dir.clone()));
assert!(path.contains(&valid_dir));
}
#[test]
fn test_crashdump_file_path_invalid() {
let path = core_dump_file_path(Some("/tmp/not_existing_dir".to_string()));
let temp_dir = std::env::temp_dir().to_string_lossy().to_string();
assert!(path.contains(&temp_dir));
}
#[test]
fn test_crashdump_file_path_default() {
let path = core_dump_file_path(None);
let temp_dir = std::env::temp_dir().to_string_lossy().to_string();
assert!(path.starts_with(&temp_dir));
}
#[test]
fn test_crashdump_not_created_when_context_is_none() {
let result = checked_core_dump(None, || Ok(Box::new(std::io::empty())));
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0);
}
#[test]
fn test_crashdump_write_fails_when_no_regions() {
let ctx = CrashDumpContext::new(
vec![],
[0; 27],
vec![],
0,
Some("dummy_binary".to_string()),
Some("dummy_filename".to_string()),
);
let get_writer = || Ok(Box::new(std::io::empty()) as Box<dyn Write>);
let result = checked_core_dump(Some(ctx), get_writer);
assert!(result.is_err());
}
#[test]
fn test_crashdump_dummy_core_dump() {
let dummy_vec = vec![0; 0x1000];
let ptr = dummy_vec.as_ptr() as usize;
let regions = vec![CrashDumpRegion {
guest_region: 0x1000..0x2000,
host_region: ptr..ptr + dummy_vec.len(),
flags: MemoryRegionFlags::READ | MemoryRegionFlags::WRITE,
region_type: crate::mem::memory_region::MemoryRegionType::Code,
}];
let ctx = CrashDumpContext::new(
regions,
[0; 27],
vec![],
0x1000,
Some("dummy_binary".to_string()),
Some("dummy_filename".to_string()),
);
let get_writer = || Ok(Box::new(std::io::empty()) as Box<dyn Write>);
let result = checked_core_dump(Some(ctx), get_writer);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0x2000);
}
}