#![cfg(target_os = "linux")]
#![expect(missing_docs)]
#![expect(unsafe_code)]
mod elf;
mod options;
mod proto;
pub use options::Options;
use crate::elf::Elf64_Ehdr;
use crate::elf::Elf64_Nhdr;
use crate::elf::Elf64_Phdr;
use crate::elf::PT_NOTE;
use crate::proto::check_header;
use crate::proto::make_header;
use fs_err::File;
use fs_err::os::unix::fs::OpenOptionsExt;
use futures::AsyncRead;
use futures::AsyncReadExt;
use futures::FutureExt;
use futures::io::AllowStdIo;
use get_protocol::crash;
use get_protocol::crash::Header;
use libc::O_NONBLOCK;
use libc::STDERR_FILENO;
use libc::STDOUT_FILENO;
use pal_async::local::block_with_io;
use std::fs::OpenOptions;
use std::io::ErrorKind;
use std::io::Read;
use std::os::fd::AsRawFd;
use std::pin::pin;
use vmbus_async::async_dgram::AsyncRecvExt;
use vmbus_async::async_dgram::AsyncSendExt;
use vmbus_async::pipe::MessagePipe;
use vmbus_async::pipe::MessageReadHalf;
use vmbus_async::pipe::MessageWriteHalf;
use vmbus_user_channel::MappedRingMem;
use zerocopy::FromBytes;
use zerocopy::FromZeros;
use zerocopy::Immutable;
use zerocopy::IntoBytes;
use zerocopy::KnownLayout;
const CRASHDMP_VDEV_MAX_TX_BYTES: usize = 4096 * 4; const KMSG_NOTE_BYTES: usize = 1024 * 256; struct OsVersionInfo {
banner: [u8; 256],
major_minor: (u32, u32),
}
impl OsVersionInfo {
pub fn new() -> Self {
let mut banner = [0u8; 256];
if let Ok(version) = std::fs::read("/proc/version") {
let bytes_to_copy = std::cmp::min(banner.len() - 1, version.len());
banner[..bytes_to_copy].copy_from_slice(&version[..bytes_to_copy]);
}
let major_minor = {
let mut utsname: libc::utsname = unsafe { std::mem::zeroed() };
if unsafe { libc::uname(&mut utsname) } == 0 {
let release = unsafe { std::str::from_utf8_unchecked(utsname.release.as_bytes()) };
let mut parts = release.split('.').take(2);
let major_iter = parts.next();
let minor_iter = parts.next();
match (major_iter, minor_iter) {
(Some(major), None) => (major.parse().unwrap_or(0), 0),
(Some(major), Some(minor)) => {
(major.parse().unwrap_or(0), minor.parse().unwrap_or(0))
}
_ => (0, 0),
}
} else {
(0, 0)
}
};
Self {
banner,
major_minor,
}
}
pub fn major(&self) -> u32 {
self.major_minor.0
}
pub fn minor(&self) -> u32 {
self.major_minor.1
}
pub fn banner(&self) -> &[u8; 256] {
&self.banner
}
}
async fn read_message<T: IntoBytes + FromBytes + Immutable + KnownLayout>(
pipe: &mut MessageReadHalf<'_, MappedRingMem>,
) -> anyhow::Result<T> {
let mut message = T::new_zeroed();
pipe.recv_exact(message.as_mut_bytes()).await?;
let header = Header::read_from_prefix(message.as_bytes()).unwrap().0; check_header(&header)?;
Ok(message)
}
async fn send_dump(
mut pipe: MessagePipe<MappedRingMem>,
dump_stream: &mut (impl AsyncRead + Unpin),
os_version: &OsVersionInfo,
include_kmsg: bool,
) -> anyhow::Result<()> {
let (mut reader, mut writer) = pipe.split();
let cap_rq = crash::DumpCapabilitiesRequestV1 {
header: make_header(None, crash::MessageType::REQUEST_GET_CAPABILITIES_V1),
};
writer.send(cap_rq.as_bytes()).await?;
let cap_resp: crash::DumpCapabilitiesResponseV1 = read_message(&mut reader).await?;
let caps = cap_resp.capabilities;
if !caps.linux_config_v1() {
anyhow::bail!("Nix dump files are not supported by the host");
}
let max_dump_size = {
let cfg_rq = crash::DumpConfigRequestV1 {
header: make_header(None, crash::MessageType::REQUEST_GET_NIX_DUMP_CONFIG_V1),
};
writer.send(cfg_rq.as_bytes()).await?;
let cfg_resp: crash::DumpConfigResponseV1 = read_message(&mut reader).await?;
let cfg = cfg_resp.config;
if cfg.max_dump_size == 0 {
anyhow::bail!("The host does not allow sending crash dump files");
}
let dump_type = cfg.dump_type;
if dump_type != crash::DumpType::ELF {
anyhow::bail!("The host does not accept ELF core dump files");
}
cfg.max_dump_size
};
tracing::debug!(max_dump_size, "Got host config");
let dump_start_rq = crash::DumpStartRequestV1 {
header: make_header(None, crash::MessageType::REQUEST_NIX_DUMP_START_V1),
};
writer.send(dump_start_rq.as_bytes()).await?;
let dump_start_resp: crash::DumpStartResponseV1 = read_message(&mut reader).await?;
let start_status = dump_start_resp.status;
if start_status != 0 {
anyhow::bail!("The host reported error 0x{:x}", start_status);
}
let write_task = pin!(async move {
let mut buf = [0u8; CRASHDMP_VDEV_MAX_TX_BYTES];
let now = std::time::Instant::now();
let mut streamer = DumpStreamer::new(
writer,
dump_stream,
dump_start_resp.header,
max_dump_size as usize,
);
if include_kmsg {
if let Err(e) = streamer.insert_kmsg_note(&mut buf).await {
tracing::error!("Error occurred while adding kmsg note: {:?}", e);
}
}
if let Err(e) = streamer.stream_all(&mut buf).await {
tracing::error!("Error occurred while streaming dump: {:?}", e);
}
if let Err(e) = streamer.complete(os_version).await {
tracing::error!("Error occurred while completing dump: {:?}", e);
}
let wrote_bytes_total = streamer.wrote_bytes_total();
let nanos = now.elapsed().as_nanos();
let speed = if nanos != 0 {
(wrote_bytes_total as u128) * 1_000_000_000 / nanos
} else {
0
};
tracing::info!(size = wrote_bytes_total, speed, "Reported crash");
Ok::<(), anyhow::Error>(())
});
let read_task = pin!(async move {
while let Ok(dump_write_resp) =
read_message::<crash::DumpWriteResponseV1>(&mut reader).await
{
let resp_status = dump_write_resp.status;
if resp_status != 0 {
anyhow::bail!("Host error {resp_status:#x}");
}
}
Ok::<(), anyhow::Error>(())
});
futures::select! { _ = write_task.fuse() => {},
_ = read_task.fuse() => {}
}
Ok(())
}
pub fn main() -> ! {
let options = Options::parse();
if !options.no_redirect {
let ttyprintk = OpenOptions::new().write(true).open("/dev/ttyprintk");
if let Ok(ttyprintk) = &ttyprintk {
unsafe {
libc::dup2(ttyprintk.as_raw_fd(), STDOUT_FILENO);
libc::dup2(ttyprintk.as_raw_fd(), STDERR_FILENO);
}
}
}
tracing_subscriber::fmt()
.with_max_level(if options.verbose {
tracing::Level::TRACE
} else {
tracing::Level::INFO
})
.log_internal_errors(true)
.with_timer(tracing_subscriber::fmt::time::uptime())
.compact()
.with_ansi(false)
.init();
if underhill_confidentiality::confidential_filtering_enabled() {
tracing::info!("crash reporting disabled due to CVM");
std::process::exit(libc::EXIT_FAILURE);
}
let os_version = OsVersionInfo::new();
let crate_revision = option_env!("VERGEN_GIT_SHA").unwrap_or("UNKNOWN_REVISION");
let os_version_major = os_version.major();
let os_version_minor = os_version.minor();
tracing::error!(
?crate_revision,
?options.comm,
?options.pid,
?options.tid,
?options.sig,
?os_version_major,
?os_version_minor,
?options.timeout,
"Process crashed"
);
let _watchdog = std::thread::spawn(move || {
std::thread::sleep(options.timeout);
tracing::error!("Crash reporting timed out");
std::process::exit(-libc::ETIMEDOUT);
});
if let Err(e) = block_with_io(async |driver| {
let mut dump_stream = AllowStdIo::new(std::io::stdin());
let pipe = vmbus_user_channel::message_pipe(
&driver,
vmbus_user_channel::open_uio_device(&crash::CRASHDUMP_GUID)?,
)?;
send_dump(pipe, &mut dump_stream, &os_version, !options.no_kmsg).await?;
Ok::<(), anyhow::Error>(())
}) {
tracing::error!(?e, "crash dump error");
std::process::exit(-libc::EXIT_FAILURE)
}
std::process::exit(libc::EXIT_SUCCESS)
}
struct DumpStreamer<'a> {
dump_stream: &'a mut (dyn AsyncRead + Unpin),
writer: MessageWriteHalf<'a, MappedRingMem>,
header: Header,
max_dump_size: usize,
read_bytes_total: usize,
wrote_bytes_total: usize,
}
impl<'a> DumpStreamer<'a> {
fn new(
writer: MessageWriteHalf<'a, MappedRingMem>,
dump_stream: &'a mut (impl AsyncRead + Unpin),
header: Header,
max_dump_size: usize,
) -> Self {
Self {
dump_stream,
writer,
header,
max_dump_size,
read_bytes_total: 0,
wrote_bytes_total: 0,
}
}
async fn read(&mut self, buf: &mut [u8], fill: bool) -> usize {
let mut n = 0;
while let Ok(read_bytes) = self.dump_stream.read(&mut buf[n..]).await {
n += read_bytes;
if !fill || read_bytes == 0 || n >= buf.len() {
break;
}
}
self.read_bytes_total += n;
if fill && n != buf.len() {
tracing::error!(
"Unable to fill buffer. Expected {:#x}, got {:#x}",
buf.len(),
n
);
}
n
}
async fn write(&mut self, data: &[u8]) -> anyhow::Result<()> {
if self.wrote_bytes_total < self.max_dump_size {
let can_write_bytes = if self.wrote_bytes_total + data.len() > self.max_dump_size {
tracing::error!("Dump has been partially sent due to the dump size limit");
self.max_dump_size - self.wrote_bytes_total
} else {
data.len()
};
let mut data_next = Some(&data[..can_write_bytes]);
while let Some(data) = data_next {
let data = if data.len() <= CRASHDMP_VDEV_MAX_TX_BYTES {
data_next = None;
data
} else {
data_next = Some(&data[CRASHDMP_VDEV_MAX_TX_BYTES..]);
&data[..CRASHDMP_VDEV_MAX_TX_BYTES]
};
let dump_write_rq = crash::DumpWriteRequestV1 {
header: make_header(
Some(&self.header),
crash::MessageType::REQUEST_NIX_DUMP_WRITE_V1,
),
offset: self.wrote_bytes_total as u64,
size: data.len() as u32,
};
self.writer.send(dump_write_rq.as_bytes()).await?;
self.writer.send(data).await?;
self.wrote_bytes_total += data.len();
}
}
Ok(())
}
async fn stream_all(&mut self, buf: &mut [u8]) -> anyhow::Result<()> {
loop {
let n = self.read(buf, false).await;
if n == 0 {
break;
}
self.write(&buf[..n]).await?;
}
Ok(())
}
async fn stream_n(&mut self, buf: &mut [u8], bytes: usize) -> anyhow::Result<()> {
let mut total = 0;
while total < bytes {
let remaining = if total + buf.len() > bytes {
bytes - total
} else {
buf.len()
};
let n = self.read(&mut buf[..remaining], false).await;
if n == 0 {
break;
}
self.write(&buf[..n]).await?;
total += n;
}
if bytes != total {
tracing::error!("Unable to stream {:#x} bytes, got {:#x}", bytes, total);
}
Ok(())
}
async fn stream_file(&mut self, buf: &mut [u8], file: &mut File, max_len: usize) -> usize {
let mut total = 0;
loop {
match file.read(buf) {
Ok(0) => break,
Err(ref err) if err.kind() == ErrorKind::WouldBlock => break,
Err(ref err)
if err.kind() == ErrorKind::Interrupted
|| err.kind() == ErrorKind::BrokenPipe => {}
Ok(len) => {
if total + len > max_len {
tracing::error!("file will be truncated.");
let len = max_len - total;
total += len;
if let Err(e) = self.write(&buf[..len]).await {
tracing::error!("error writing file: {:?}", e);
}
break;
}
total += len;
if let Err(e) = self.write(&buf[..len]).await {
tracing::error!("error writing file: {:?}", e);
break;
}
}
Err(e) => {
tracing::error!("error reading file: {:?}", e);
break;
}
}
}
total
}
async fn write_padding(&mut self, buf: &mut [u8], bytes: usize) -> anyhow::Result<()> {
buf.fill(0);
let mut written = 0;
while written < bytes {
let n = std::cmp::min(bytes - written, buf.len());
self.write(&buf[..n]).await?;
written += n;
}
Ok(())
}
async fn insert_kmsg_note(&mut self, buf: &mut [u8]) -> anyhow::Result<()> {
let mut ehdr: Elf64_Ehdr = Elf64_Ehdr::new_zeroed();
self.read(ehdr.as_mut_bytes(), true).await;
self.write(ehdr.as_bytes()).await?;
tracing::trace!("ehdr: {:#x?}", &ehdr);
if ehdr.e_shoff != 0 {
tracing::error!("Dump contains section headers, which are not supported");
}
let mut notes_phdr: Elf64_Phdr = Elf64_Phdr::new_zeroed();
self.read(notes_phdr.as_mut_bytes(), true).await;
tracing::trace!("initial notes_phdr: {:#x?}", notes_phdr);
if notes_phdr.p_type != PT_NOTE {
tracing::error!("Expected type {:#x}, got {:#x}", PT_NOTE, notes_phdr.p_type);
}
let initial_notes_size = notes_phdr.p_filesz as usize;
notes_phdr.p_filesz += KMSG_NOTE_BYTES as u64;
tracing::trace!("modified notes_phdr: {:#x?}", notes_phdr);
self.write(notes_phdr.as_bytes()).await?;
let mut phnum_remaining = ehdr.e_phnum as usize - 1;
let max = buf.len() / size_of::<Elf64_Phdr>();
while phnum_remaining > 0 {
let phnum = std::cmp::min(phnum_remaining, max);
let phdrs_size = phnum * size_of::<Elf64_Phdr>();
self.read(&mut buf[..phdrs_size], true).await;
let phdrs: &mut [Elf64_Phdr] =
<[Elf64_Phdr]>::mut_from_bytes(&mut buf[..phdrs_size]).unwrap();
tracing::trace!("initial phdrs: {:#x?}", phdrs);
for phdr in &mut phdrs[..] {
phdr.p_offset += KMSG_NOTE_BYTES as u64;
}
tracing::trace!("modified phdrs: {:#x?}", phdrs);
self.write(&buf[..phdrs_size]).await?;
phnum_remaining -= phnum;
}
let padding_before_notes = notes_phdr.p_offset as usize - self.read_bytes_total;
tracing::trace!("padding_before_notes: {:#x?}", padding_before_notes);
self.stream_n(buf, padding_before_notes + initial_notes_size)
.await?;
let name = b"KMSG\0\0\0\0";
let header_and_name = size_of::<Elf64_Nhdr>() + name.len();
let kmsg_header = Elf64_Nhdr {
namesz: 5,
descsz: (KMSG_NOTE_BYTES - header_and_name) as u32,
ntype: 0xffffffff,
};
let max_kmsg_len = KMSG_NOTE_BYTES - header_and_name - size_of::<u32>();
let mut kmsg = fs_err::OpenOptions::new()
.read(true)
.custom_flags(O_NONBLOCK)
.open("/dev/kmsg")?;
self.write(kmsg_header.as_bytes()).await?;
self.write(name).await?;
let kmsg_len = self.stream_file(buf, &mut kmsg, max_kmsg_len).await;
self.write_padding(buf, max_kmsg_len - kmsg_len).await?;
self.write((kmsg_len as u32).as_bytes()).await?;
tracing::debug!(len = kmsg_len, "wrote kmsg");
Ok(())
}
async fn complete(&mut self, os_version: &OsVersionInfo) -> anyhow::Result<()> {
tracing::debug!(
"Read {} bytes, wrote {} bytes",
self.read_bytes_total,
self.wrote_bytes_total
);
if self.read_bytes_total + KMSG_NOTE_BYTES == self.wrote_bytes_total {
tracing::debug!(
"Bytes written includes {} bytes for kmsg note",
KMSG_NOTE_BYTES,
);
} else {
tracing::error!(
"wrote - read = {}, expected {} to account for kmsg note",
self.wrote_bytes_total as isize - self.read_bytes_total as isize,
KMSG_NOTE_BYTES,
);
}
let dump_complete = crash::DumpCompleteRequestV1 {
header: make_header(
Some(&self.header),
crash::MessageType::REQUEST_NIX_DUMP_COMPLETE_V1,
),
info: crash::CompletionInfoV1 {
major_version: os_version.major(),
minor_version: os_version.minor(),
version_banner: *os_version.banner(),
vtl: 2,
},
};
self.writer.send(dump_complete.as_bytes()).await?;
Ok(())
}
fn wrote_bytes_total(&self) -> usize {
self.wrote_bytes_total
}
}