#![cfg(all(target_os = "linux", feature = "symbolize-split"))]
use std::path::{Path, PathBuf};
use std::process::Command;
use kernal_api::symbolize::split::{
split_debug_symbols, DebugSplitError, DebugSplitRequest, SplitMechanism,
};
const RUST_FIXTURE: &str = r#"
#[no_mangle]
#[inline(never)]
pub extern "C" fn kernal_fixture_target(value: u64) -> u64 {
value.wrapping_mul(2654435761)
}
fn main() {
println!("{}", kernal_fixture_target(std::env::args().count() as u64));
}
"#;
const C_FIXTURE: &str = r#"
#include <stdio.h>
__attribute__((noinline)) unsigned long kernal_fixture_target(unsigned long value) {
return value * 2654435761UL;
}
int main(int argc, char **argv) {
(void)argv;
printf("%lu\n", kernal_fixture_target((unsigned long)argc));
return 0;
}
"#;
fn build_fixture(directory: &Path, name: &str, build_id: bool) -> Option<PathBuf> {
let binary = directory.join(name);
let rust_source = directory.join(format!("{name}.rs"));
std::fs::write(&rust_source, RUST_FIXTURE).expect("write the Rust fixture source");
let link_argument = if build_id {
"-Clink-arg=-Wl,--build-id"
} else {
"-Clink-arg=-Wl,--build-id=none"
};
let rustc = Command::new("rustc")
.args(["-g", "-Cdebuginfo=2", link_argument, "-o"])
.arg(&binary)
.arg(&rust_source)
.output();
if matches!(&rustc, Ok(output) if output.status.success()) {
return Some(binary);
}
let c_source = directory.join(format!("{name}.c"));
std::fs::write(&c_source, C_FIXTURE).expect("write the C fixture source");
let cc = Command::new("cc")
.args(["-g", "-O0"])
.arg(if build_id {
"-Wl,--build-id"
} else {
"-Wl,--build-id=none"
})
.arg("-o")
.arg(&binary)
.arg(&c_source)
.output();
matches!(&cc, Ok(output) if output.status.success()).then_some(binary)
}
fn run_binutils(program: &str, arguments: &[&std::ffi::OsStr]) -> bool {
Command::new(program)
.args(arguments)
.output()
.is_ok_and(|output| output.status.success())
}
fn skip(reason: &str) {
eprintln!("skipping debug-symbol-split test: {reason}");
}
fn split_fixture(directory: &Path, name: &str) -> Option<kernal_api::symbolize::split::DebugSplit> {
let binary = build_fixture(directory, name, true)?;
match split_debug_symbols(&DebugSplitRequest::new(&binary)) {
Ok(split) => Some(split),
Err(DebugSplitError::MechanismUnavailable { missing, .. }) => {
skip(&format!("this host has no {}", missing.join(" or ")));
None
}
Err(error) => panic!("splitting {} failed: {error}", binary.display()),
}
}
#[test]
fn the_split_moves_the_debug_info_out_of_the_shipped_binary() {
let directory = tempfile::tempdir().expect("temporary directory");
let Some(binary) = build_fixture(directory.path(), "fixture", true) else {
skip("neither rustc nor cc could build the fixture");
return;
};
let Some(split) = split_fixture(directory.path(), "fixture") else {
return;
};
assert_eq!(split.mechanism(), SplitMechanism::GnuDebugLink);
assert!(split.mechanism().covers_linked_image());
assert_eq!(split.binary(), binary.as_path());
assert_eq!(split.symbol_file(), directory.path().join("fixture.debug"));
assert!(
split.build_identity().starts_with("elf:"),
"expected an ELF build-id identity, got {}",
split.build_identity()
);
assert!(split.symbol_file().is_file());
let binary_bytes = std::fs::metadata(split.binary())
.expect("stripped binary")
.len();
let symbol_bytes = std::fs::metadata(split.symbol_file())
.expect("symbol file")
.len();
assert!(
symbol_bytes > 0 && binary_bytes > 0,
"both artifacts must exist: binary {binary_bytes}, symbols {symbol_bytes}"
);
}
#[test]
fn a_binary_without_a_build_id_is_refused_before_any_tool_runs() {
let directory = tempfile::tempdir().expect("temporary directory");
let Some(binary) = build_fixture(directory.path(), "no-build-id", false) else {
skip("neither rustc nor cc could build the fixture");
return;
};
let before = std::fs::metadata(&binary).expect("fixture").len();
match split_debug_symbols(&DebugSplitRequest::new(&binary)) {
Err(DebugSplitError::BuildIdentityMissing { binary: reported }) => {
assert_eq!(reported, binary);
}
Ok(split) => {
assert!(split.build_identity().starts_with("elf:"));
skip("this host's linker always emits a build-id");
return;
}
Err(DebugSplitError::MechanismUnavailable { missing, .. }) => {
skip(&format!("this host has no {}", missing.join(" or ")));
}
Err(error) => panic!("unexpected failure: {error}"),
}
assert_eq!(std::fs::metadata(&binary).expect("fixture").len(), before);
assert!(!directory.path().join("no-build-id.debug").exists());
}
#[test]
fn an_already_stripped_binary_is_refused_rather_than_given_an_empty_sidecar() {
let directory = tempfile::tempdir().expect("temporary directory");
let Some(binary) = build_fixture(directory.path(), "prestripped", true) else {
skip("neither rustc nor cc could build the fixture");
return;
};
if !run_binutils("strip", &["--strip-debug".as_ref(), binary.as_os_str()]) {
skip("this host has no strip");
return;
}
match split_debug_symbols(&DebugSplitRequest::new(&binary)) {
Err(DebugSplitError::DebugInfoMissing { binary: reported }) => {
assert_eq!(reported, binary);
}
Err(DebugSplitError::MechanismUnavailable { missing, .. }) => {
skip(&format!("this host has no {}", missing.join(" or ")));
}
other => panic!("a stripped binary must be refused, got {other:?}"),
}
}
#[cfg(feature = "symbolize-worker")]
mod resolution {
use super::*;
use kernal_api::symbolize::SymbolizerWorker;
const TARGET_FUNCTION: &str = "kernal_fixture_target";
fn worker() -> Option<SymbolizerWorker> {
if let Some(exported) = std::env::var_os("NEXTEST_BIN_EXE_kernal-symbolize") {
return Some(SymbolizerWorker::new(exported));
}
let Some(path) = option_env!("CARGO_BIN_EXE_kernal-symbolize") else {
skip("this lane did not build kernal-symbolize");
return None;
};
Some(SymbolizerWorker::new(path))
}
#[tokio::test]
async fn the_symbolizer_resolves_a_known_function_through_the_symbol_file() {
let directory = tempfile::tempdir().expect("temporary directory");
let Some(split) = split_fixture(directory.path(), "resolved") else {
return;
};
let Some(worker) = worker() else {
return;
};
let verified = split
.verify_resolves(&worker, TARGET_FUNCTION)
.await
.expect("the produced symbol file must resolve the fixture function");
assert_eq!(verified.function(), TARGET_FUNCTION);
assert!(verified.module_offset() > 0);
assert_eq!(verified.symbol_file(), split.symbol_file());
}
#[tokio::test]
async fn a_symbol_file_from_another_build_fails_verification() {
let directory = tempfile::tempdir().expect("temporary directory");
let Some(split) = split_fixture(directory.path(), "mine") else {
return;
};
let Some(other) = split_fixture(directory.path(), "theirs") else {
return;
};
let Some(worker) = worker() else {
return;
};
assert_ne!(split.build_identity(), other.build_identity());
std::fs::copy(other.symbol_file(), split.symbol_file()).expect("swap the symbol file");
match split.verify_resolves(&worker, TARGET_FUNCTION).await {
Err(DebugSplitError::Unresolved { module_status, .. }) => {
assert_ne!(
module_status,
kernal_api::symbolize::wire::ModuleSymbolStatus::Resolved
);
}
other => panic!("a mismatched symbol file must be refused, got {other:?}"),
}
}
#[tokio::test]
async fn an_unknown_function_is_reported_rather_than_guessed() {
let directory = tempfile::tempdir().expect("temporary directory");
let Some(split) = split_fixture(directory.path(), "unknown") else {
return;
};
let Some(worker) = worker() else {
return;
};
match split
.verify_resolves(&worker, "no_such_function_exists")
.await
{
Err(DebugSplitError::FunctionAddressUnknown { function, .. }) => {
assert_eq!(function, "no_such_function_exists");
}
other => panic!("an absent function must be reported, got {other:?}"),
}
}
}