use std::fs::File;
use std::path::Path;
#[cfg(unix)]
use std::time::Duration;
use anyhow::{Context, Result};
use flate2::Compression;
use flate2::write::GzEncoder;
#[cfg(unix)]
use crate::doctor::exec::try_exec;
use crate::doctor::redact::{RedactOptions, scrub};
use crate::doctor::report::{render_human_string, render_json_string};
use crate::doctor::{DoctorOptions, Report};
pub fn write_bundle(path: &Path, report: &Report, opts: &DoctorOptions) -> Result<()> {
let redact = opts.redact_options();
let entries = collect_entries(report, opts, &redact)?;
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create bundle parent {parent:?}"))?;
}
let f =
open_bundle_file(path).with_context(|| format!("failed to create bundle file {path:?}"))?;
let gz = GzEncoder::new(f, Compression::default());
let mut tar = tar::Builder::new(gz);
for (name, bytes) in &entries {
let mut header = tar::Header::new_gnu();
header.set_size(bytes.len() as u64);
header.set_mode(0o600);
header.set_mtime(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
);
header.set_cksum();
tar.append_data(&mut header, name, bytes.as_slice())
.with_context(|| format!("failed to append {name} to bundle"))?;
}
let gz = tar.into_inner().context("failed to finalise tar stream")?;
let f = gz.finish().context("failed to finalise gzip stream")?;
if let Err(e) = f.sync_all() {
return Err(e).with_context(|| format!("failed to fsync bundle {path:?}"));
}
Ok(())
}
fn open_bundle_file(path: &Path) -> std::io::Result<File> {
use std::fs::OpenOptions;
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.custom_flags(libc::O_NOFOLLOW)
.mode(0o600)
.open(path)
}
#[cfg(all(windows, not(unix)))]
{
use std::os::windows::fs::OpenOptionsExt;
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.share_mode(0)
.open(path)
}
#[cfg(not(any(unix, windows)))]
{
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
}
}
fn collect_entries(
report: &Report,
opts: &DoctorOptions,
redact: &RedactOptions,
) -> Result<Vec<(String, Vec<u8>)>> {
let mut out: Vec<(String, Vec<u8>)> = vec![
(
"all-smi-doctor/report.txt".to_string(),
render_human_string(report, redact, opts)?.into_bytes(),
),
(
"all-smi-doctor/report.json".to_string(),
render_json_string(report, redact)?.into_bytes(),
),
(
"all-smi-doctor/env.txt".to_string(),
env_dump(redact).into_bytes(),
),
(
"all-smi-doctor/version.txt".to_string(),
version_dump(report).into_bytes(),
),
];
if let Some(bytes) = uname_bytes(redact) {
out.push(("all-smi-doctor/uname.txt".to_string(), bytes));
}
if let Some(bytes) = lspci_bytes(redact) {
out.push(("all-smi-doctor/lspci.txt".to_string(), bytes));
}
if let Some(bytes) = lsmod_bytes(redact) {
out.push(("all-smi-doctor/lsmod.txt".to_string(), bytes));
}
if let Some(bytes) = dmesg_gpu_bytes(redact) {
out.push(("all-smi-doctor/dmesg-gpu.txt".to_string(), bytes));
}
#[cfg(target_os = "macos")]
if opts.verbose
&& let Some(bytes) = macos_system_profiler_bytes(redact)
{
out.push((
"all-smi-doctor/system_profiler_display.txt".to_string(),
bytes,
));
}
let _ = opts;
Ok(out)
}
const SECRET_NAME_SUBSTRINGS: &[&str] = &[
"TOKEN",
"SECRET",
"PASSWORD",
"PASSWD",
"API_KEY",
"APIKEY",
"ACCESS_KEY",
"PRIVATE_KEY",
"CREDENTIAL",
"AUTH",
"SESSION",
"COOKIE",
"BEARER",
"SIGNATURE",
"ENCRYPTION_KEY",
"CLIENT_SECRET",
];
pub(crate) const REDACT_SECRET_VALUE: &str = "<redacted:secret>";
pub(crate) fn is_secret_env_name(name: &str) -> bool {
let upper = name.to_ascii_uppercase();
SECRET_NAME_SUBSTRINGS.iter().any(|p| upper.contains(p))
}
fn env_dump(redact: &RedactOptions) -> String {
let keep = [
"ALL_SMI_",
"CUDA_",
"NVIDIA_",
"ROCR_",
"HIP_",
"HSA_",
"TPU_",
"CLOUD_TPU_",
"HL_",
"HABANA_",
"NO_COLOR",
"USER",
"HOSTNAME",
"KUBERNETES_",
"BACKENDAI_",
"HOME",
];
let mut vars: Vec<(String, String)> = std::env::vars()
.filter(|(k, _)| keep.iter().any(|p| k.starts_with(*p) || k == p))
.map(|(k, v)| {
if is_secret_env_name(&k) {
(k, REDACT_SECRET_VALUE.to_string())
} else {
(k, v)
}
})
.collect();
vars.sort_by(|a, b| a.0.cmp(&b.0));
let mut text = String::new();
for (k, v) in vars {
text.push_str(&format!("{k}={v}\n"));
}
for var in ["PATH", "LD_LIBRARY_PATH"] {
match std::env::var(var) {
Ok(v) if !v.is_empty() => {
let sep = if cfg!(windows) { ';' } else { ':' };
let entries = v.split(sep).filter(|s| !s.is_empty()).count();
text.push_str(&format!("{var}=<redacted:path-list {entries} entries>\n"));
}
_ => {}
}
}
scrub(&text, redact)
}
fn version_dump(report: &Report) -> String {
let features = enabled_features().join(",");
let triple = crate::doctor::checks::platform::checks()
.iter()
.find(|c| c.id == "platform.runtime")
.map(|c| (c.run)(&Default::default()))
.map(|r| r.message().to_string())
.unwrap_or_else(|| "target unknown".to_string());
let version = &report.version;
let schema = report.schema;
let timestamp = &report.timestamp;
format!(
"all-smi {version}\nschema: {schema}\ntimestamp: {timestamp}\nfeatures: {features}\nruntime: {triple}\n"
)
}
fn enabled_features() -> Vec<&'static str> {
let mut v: Vec<&'static str> = Vec::new();
#[cfg(feature = "cli")]
v.push("cli");
#[cfg(feature = "mock")]
v.push("mock");
#[cfg(feature = "furiosa")]
v.push("furiosa");
if v.is_empty() {
v.push("none");
}
v
}
fn uname_bytes(redact: &RedactOptions) -> Option<Vec<u8>> {
#[cfg(unix)]
{
let out = try_exec("uname", &["-a"], Duration::from_millis(500))?;
if out.success() {
return Some(scrub(out.stdout.trim_end(), redact).into_bytes());
}
None
}
#[cfg(not(unix))]
{
let _ = redact;
None
}
}
fn lspci_bytes(redact: &RedactOptions) -> Option<Vec<u8>> {
#[cfg(target_os = "linux")]
{
let out = try_exec("lspci", &["-vv"], Duration::from_millis(2_500))?;
if !out.success() {
return None;
}
let mut keep: Vec<String> = Vec::new();
let mut in_match = false;
let keywords = [
"VGA",
"3D",
"Display",
"NVIDIA",
"AMD",
"Habana",
"Tenstorrent",
"Accel",
];
for line in out.stdout.lines() {
let trimmed = line.trim_start();
if trimmed == line && !line.is_empty() {
in_match = keywords.iter().any(|k| line.contains(k));
}
if in_match {
keep.push(line.to_string());
}
}
let text = keep.join("\n");
Some(scrub(&text, redact).into_bytes())
}
#[cfg(not(target_os = "linux"))]
{
let _ = redact;
None
}
}
fn lsmod_bytes(redact: &RedactOptions) -> Option<Vec<u8>> {
#[cfg(target_os = "linux")]
{
let out = try_exec("lsmod", &[], Duration::from_millis(1_000))?;
if !out.success() {
return None;
}
Some(scrub(&out.stdout, redact).into_bytes())
}
#[cfg(not(target_os = "linux"))]
{
let _ = redact;
None
}
}
fn dmesg_gpu_bytes(redact: &RedactOptions) -> Option<Vec<u8>> {
#[cfg(target_os = "linux")]
{
let out = try_exec("dmesg", &["-T"], Duration::from_millis(2_500))?;
if !out.success() {
return None;
}
let keywords = ["nvidia", "amdgpu", "i915", "habanalabs", "drm", "tt-kmd"];
let filtered: Vec<&str> = out
.stdout
.lines()
.filter(|l| keywords.iter().any(|k| l.to_lowercase().contains(k)))
.collect();
let start = filtered.len().saturating_sub(200);
let text = filtered[start..].join("\n");
Some(scrub(&text, redact).into_bytes())
}
#[cfg(not(target_os = "linux"))]
{
let _ = redact;
None
}
}
#[cfg(target_os = "macos")]
fn macos_system_profiler_bytes(redact: &RedactOptions) -> Option<Vec<u8>> {
let out = try_exec(
"system_profiler",
&["SPDisplaysDataType"],
Duration::from_millis(2_900),
)?;
if !out.success() {
return None;
}
Some(scrub(&out.stdout, redact).into_bytes())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::doctor::Summary;
#[test]
fn bundle_writes_expected_entries() {
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
let report = Report {
schema: 1,
version: "0.99.9".to_string(),
timestamp: "2026-04-20T00:00:00Z".to_string(),
summary: Summary {
pass: 1,
warn: 0,
fail: 0,
skip: 0,
},
checks: vec![],
};
let opts = DoctorOptions {
json: false,
verbose: false,
bundle_path: Some(tmp.path().to_path_buf()),
include_identifiers: true,
remote_checks: vec![],
skip: vec![],
only: vec![],
use_color: false,
};
write_bundle(tmp.path(), &report, &opts).expect("bundle ok");
let bytes = std::fs::read(tmp.path()).expect("read bundle");
assert!(bytes.len() > 2);
assert_eq!(bytes[0], 0x1f);
assert_eq!(bytes[1], 0x8b);
}
#[test]
fn is_secret_env_name_matches_common_patterns() {
assert!(is_secret_env_name("BACKENDAI_SECRET_KEY"));
assert!(is_secret_env_name("BACKENDAI_ACCESS_KEY"));
assert!(is_secret_env_name("AWS_SESSION_TOKEN"));
assert!(is_secret_env_name("HUGGINGFACE_HUB_TOKEN"));
assert!(is_secret_env_name("MY_API_KEY"));
assert!(is_secret_env_name("github_client_secret"));
assert!(is_secret_env_name("SERVICE_PASSWORD"));
assert!(is_secret_env_name("BEARER_TOKEN_PROD"));
assert!(!is_secret_env_name("NVIDIA_VISIBLE_DEVICES"));
assert!(!is_secret_env_name("CUDA_VISIBLE_DEVICES"));
assert!(!is_secret_env_name("HOME"));
assert!(!is_secret_env_name("USER"));
assert!(!is_secret_env_name("PATH"));
}
#[test]
fn env_dump_redacts_secret_values_and_summarises_path() {
unsafe {
std::env::set_var("ALL_SMI_DOCTOR_TEST_TOKEN", "hunter2");
std::env::set_var("ALL_SMI_DOCTOR_TEST_PLAIN", "public-value");
std::env::set_var("PATH", "/a:/b:/c");
}
let opts = RedactOptions {
hostname: None,
username: None,
scrub_kernel_pointers: false,
enabled: true,
};
let dump = env_dump(&opts);
unsafe {
std::env::remove_var("ALL_SMI_DOCTOR_TEST_TOKEN");
std::env::remove_var("ALL_SMI_DOCTOR_TEST_PLAIN");
}
assert!(
dump.contains("ALL_SMI_DOCTOR_TEST_TOKEN=<redacted:secret>"),
"secret value must be redacted: {dump}"
);
assert!(
!dump.contains("hunter2"),
"raw secret must not appear in bundle: {dump}"
);
assert!(
dump.contains("ALL_SMI_DOCTOR_TEST_PLAIN=public-value"),
"non-secret value must be preserved: {dump}"
);
assert!(
dump.contains("PATH=<redacted:path-list"),
"PATH must be summarised: {dump}"
);
assert!(
!dump.contains("/a:/b:/c"),
"raw PATH contents must not leak: {dump}"
);
}
#[test]
fn bundle_unix_mode_is_0o600() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::NamedTempFile::new().expect("tempfile");
let report = Report {
schema: 1,
version: "0.99.9".to_string(),
timestamp: "2026-04-20T00:00:00Z".to_string(),
summary: Summary::default(),
checks: vec![],
};
let opts = DoctorOptions {
json: false,
verbose: false,
bundle_path: Some(tmp.path().to_path_buf()),
include_identifiers: true,
remote_checks: vec![],
skip: vec![],
only: vec![],
use_color: false,
};
write_bundle(tmp.path(), &report, &opts).expect("bundle ok");
let meta = std::fs::metadata(tmp.path()).expect("metadata");
let mode = meta.permissions().mode() & 0o777;
assert_eq!(
mode, 0o600,
"bundle file must be owner-read/write only, got {mode:o}"
);
}
}
#[test]
fn bundle_refuses_preexisting_symlink() {
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let dir = tempfile::tempdir().expect("tempdir");
let decoy_target = dir.path().join("DECOY");
std::fs::write(&decoy_target, b"sensitive").expect("decoy write");
let link_path = dir.path().join("bundle.tar.gz");
symlink(&decoy_target, &link_path).expect("symlink");
let report = Report {
schema: 1,
version: "0.99.9".to_string(),
timestamp: "2026-04-20T00:00:00Z".to_string(),
summary: Summary::default(),
checks: vec![],
};
let opts = DoctorOptions {
json: false,
verbose: false,
bundle_path: Some(link_path.clone()),
include_identifiers: true,
remote_checks: vec![],
skip: vec![],
only: vec![],
use_color: false,
};
let result = write_bundle(&link_path, &report, &opts);
assert!(
result.is_err(),
"write_bundle must refuse to follow a pre-existing symlink"
);
let decoy_contents = std::fs::read(&decoy_target).expect("decoy read");
assert_eq!(
decoy_contents, b"sensitive",
"symlink target was overwritten despite O_NOFOLLOW"
);
}
}
}