underhill_dump/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Underhill process for writing core dumps.
5//!
6//! `underhill_dump <pid>`
7//!
8//! This command writes a core dump of process `pid` to stdout.
9//!
10//! This is done as a separate process instead of inside the diagnostics process
11//! for two reasons:
12//!
13//! 1. To allow us to dump the diagnostics process.
14//! 2. To ensure that waitpid() calls by the diagnostics process do not get
15//!    tracing stop notifications.
16
17#![cfg(target_os = "linux")]
18#![expect(missing_docs)]
19#![forbid(unsafe_code)]
20
21use anyhow::Context;
22use std::fs::File;
23use std::io::ErrorKind;
24use std::os::unix::fs::OpenOptionsExt;
25use std::path::Path;
26use tracing::Level;
27
28const KMSG_NOTE_BYTES: usize = 1024 * 256; // 256 KB
29
30pub fn main() -> ! {
31    if let Err(e) = do_main() {
32        tracing::error!(?e, "core dump error");
33        std::process::exit(libc::EXIT_FAILURE)
34    }
35
36    std::process::exit(libc::EXIT_SUCCESS)
37}
38
39pub fn do_main() -> anyhow::Result<()> {
40    let mut args = std::env::args().skip(1).peekable();
41
42    let level = if args.peek().is_some_and(|x| x == "-v") {
43        args.next();
44        Level::DEBUG
45    } else {
46        Level::INFO
47    };
48
49    tracing_subscriber::fmt()
50        .with_writer(std::io::stderr)
51        .log_internal_errors(true)
52        .with_max_level(level)
53        .with_timer(tracing_subscriber::fmt::time::uptime())
54        .compact()
55        .with_ansi(false)
56        .init();
57
58    // We should have checks in our callers so this is never hit, but let's be safe.
59    if underhill_confidentiality::confidential_filtering_enabled() {
60        tracing::info!("crash reporting disabled due to CVM");
61        std::process::exit(libc::EXIT_FAILURE);
62    }
63
64    let pid: i32 = args
65        .next()
66        .context("missing pid")?
67        .parse()
68        .context("failed to parse pid")?;
69
70    if args.next().is_some() {
71        anyhow::bail!("unexpected extra arguments");
72    }
73
74    let mut builder = elfcore::CoreDumpBuilder::new(pid)?;
75
76    let mut kmsg_file = NonBlockingFile::new("/dev/kmsg");
77    match kmsg_file.as_mut() {
78        Ok(kmsg_file) => _ = builder.add_custom_file_note("KMSG", kmsg_file, KMSG_NOTE_BYTES),
79        Err(e) => tracing::error!("Failed to open KMSG file: {:?}", e),
80    }
81
82    let n = builder
83        .write(std::io::stdout().lock())
84        .context("failed to write core dump")?;
85
86    tracing::info!("dump: {} bytes", n);
87
88    Ok(())
89}
90
91struct NonBlockingFile(File);
92
93impl NonBlockingFile {
94    fn new<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
95        Ok(Self(
96            std::fs::OpenOptions::new()
97                .read(true)
98                .custom_flags(libc::O_NONBLOCK)
99                .open(path)?,
100        ))
101    }
102}
103
104impl std::io::Read for NonBlockingFile {
105    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
106        match self.0.read(buf) {
107            // return data
108            Ok(len) => Ok(len),
109            // would block, we are done
110            Err(ref err) if err.kind() == ErrorKind::WouldBlock => Ok(0),
111            // continue on interruptions or broken pipe, since
112            // if old messages are overwritten while /dev/kmsg is open,
113            // the next read returns -EPIPE
114            Err(ref err)
115                if err.kind() == ErrorKind::Interrupted || err.kind() == ErrorKind::BrokenPipe =>
116            {
117                self.read(buf)
118            }
119            Err(e) => Err(e),
120        }
121    }
122}