use anyhow::{Context, Result};
use std::fs::File;
use std::path::{Path, PathBuf};
use crate::vmm;
#[cfg(test)]
pub(crate) const MSG_TYPE_PROFRAW: u32 = u32::from_be_bytes(*b"PRAW");
pub(crate) fn try_flush_profraw() {
if !vmm::guest_comms::is_guest() {
return;
}
let exe_file = match File::open("/proc/self/exe") {
Ok(f) => f,
Err(_) => return,
};
let mmap = match unsafe { memmap2::Mmap::map(&exe_file) } {
Ok(m) => m,
Err(_) => return,
};
let bytes: &[u8] = &mmap;
let elf = match goblin::elf::Elf::parse(bytes) {
Ok(e) => e,
Err(_) => return,
};
let slide = pie_load_bias(&elf);
let vaddrs = find_symbol_vaddrs(
&elf,
&[
"__llvm_profile_get_size_for_buffer",
"__llvm_profile_write_buffer",
],
);
let size_vaddr = match vaddrs[0] {
Some(v) if v != 0 => v,
_ => return,
};
let write_vaddr = match vaddrs[1] {
Some(v) if v != 0 => v,
_ => return,
};
let get_size: extern "C" fn() -> u64 =
unsafe { std::mem::transmute((size_vaddr as usize).wrapping_add(slide)) };
let write_buffer: extern "C" fn(*mut std::os::raw::c_char) -> i32 =
unsafe { std::mem::transmute((write_vaddr as usize).wrapping_add(slide)) };
let needed = get_size() as usize;
if needed == 0 {
return;
}
let mut buf: Vec<u8> = vec![0u8; needed];
if write_buffer(buf.as_mut_ptr().cast::<std::os::raw::c_char>()) != 0 {
return;
}
vmm::guest_comms::send_profraw(&buf);
}
pub(crate) fn find_symbol_vaddrs(elf: &goblin::elf::Elf<'_>, names: &[&str]) -> Vec<Option<u64>> {
let mut results = vec![None; names.len()];
let mut remaining = names.len();
for sym in elf.syms.iter() {
if remaining == 0 {
break;
}
if sym.st_size == 0 {
continue;
}
let sym_name = match elf.strtab.get_at(sym.st_name) {
Some(n) => n,
None => continue,
};
for (i, name) in names.iter().enumerate() {
if results[i].is_none() && sym_name == *name {
results[i] = Some(sym.st_value);
remaining -= 1;
break;
}
}
}
results
}
pub(crate) fn pie_load_bias(elf: &goblin::elf::Elf<'_>) -> usize {
if elf.header.e_type != goblin::elf::header::ET_DYN {
return 0;
}
let phdr_file_offset = elf.header.e_phoff as usize;
let phdr_runtime = unsafe { libc::getauxval(libc::AT_PHDR) } as usize;
if phdr_runtime == 0 {
return 0;
}
phdr_runtime.wrapping_sub(phdr_file_offset)
}
static PROFRAW_COUNTER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
pub(crate) fn write_profraw(data: &[u8]) -> Result<()> {
let target_dir = target_dir();
std::fs::create_dir_all(&target_dir)
.with_context(|| format!("create profraw dir: {}", target_dir.display()))?;
let id = PROFRAW_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let path = target_dir.join(format!("ktstr-test-{}-{}.profraw", std::process::id(), id));
std::fs::write(&path, data).with_context(|| format!("write profraw: {}", path.display()))?;
Ok(())
}
pub fn target_dir() -> PathBuf {
if let Ok(d) = std::env::var("LLVM_COV_TARGET_DIR") {
return PathBuf::from(d);
}
if let Some(parent) = std::env::var("LLVM_PROFILE_FILE")
.ok()
.as_ref()
.and_then(|p| Path::new(p).parent())
.filter(|p| !p.as_os_str().is_empty())
{
return parent.to_path_buf();
}
let mut p = crate::resolve_current_exe().unwrap_or_else(|_| std::env::temp_dir());
p.pop(); p.push("llvm-cov-target");
p
}
fn redirect_pattern_for(
pid: libc::pid_t,
existing: Option<std::ffi::OsString>,
is_coverage_instrumented: bool,
target_dir: impl FnOnce() -> PathBuf,
) -> Option<PathBuf> {
if pid == 1 || existing.is_some() || !is_coverage_instrumented {
return None;
}
Some(target_dir().join("default-%p-%m.profraw"))
}
fn is_coverage_instrumented_binary() -> bool {
let exe_file = match File::open("/proc/self/exe") {
Ok(f) => f,
Err(_) => return false,
};
let mmap = match unsafe { memmap2::Mmap::map(&exe_file) } {
Ok(m) => m,
Err(_) => return false,
};
let elf = match goblin::elf::Elf::parse(&mmap) {
Ok(e) => e,
Err(_) => return false,
};
let vaddrs = find_symbol_vaddrs(&elf, &["__llvm_profile_runtime"]);
matches!(vaddrs[0], Some(v) if v != 0)
}
#[ctor::ctor(priority = 0)]
fn redirect_default_profraw_path() {
let pid = unsafe { libc::getpid() };
let existing = std::env::var_os("LLVM_PROFILE_FILE");
if pid == 1 || existing.is_some() {
return;
}
let instrumented = is_coverage_instrumented_binary();
if let Some(pattern) = redirect_pattern_for(pid, existing, instrumented, target_dir) {
unsafe {
std::env::set_var("LLVM_PROFILE_FILE", &pattern);
}
}
}
#[cfg(test)]
mod tests {
use super::super::test_helpers::{EnvVarGuard, lock_env};
use super::*;
#[test]
fn target_dir_with_env_var() {
let _lock = lock_env();
let _env = EnvVarGuard::set("LLVM_COV_TARGET_DIR", "/tmp/my-cov-dir");
let dir = target_dir();
assert_eq!(dir, PathBuf::from("/tmp/my-cov-dir"));
}
#[test]
fn target_dir_from_llvm_profile_file() {
let _lock = lock_env();
let _env_cov = EnvVarGuard::remove("LLVM_COV_TARGET_DIR");
let _env_prof =
EnvVarGuard::set("LLVM_PROFILE_FILE", "/tmp/cov-target/ktstr-%p-%m.profraw");
let dir = target_dir();
assert_eq!(dir, PathBuf::from("/tmp/cov-target"));
}
#[test]
fn target_dir_without_env_var() {
let _lock = lock_env();
let _env_cov = EnvVarGuard::remove("LLVM_COV_TARGET_DIR");
let _env_prof = EnvVarGuard::remove("LLVM_PROFILE_FILE");
let dir = target_dir();
assert!(
dir.ends_with("llvm-cov-target"),
"expected path ending in llvm-cov-target, got: {}",
dir.display()
);
}
#[test]
fn target_dir_bare_filename_llvm_profile_file_falls_through() {
let _lock = lock_env();
let _g_cov = EnvVarGuard::remove("LLVM_COV_TARGET_DIR");
let _g_prof = EnvVarGuard::set("LLVM_PROFILE_FILE", "default.profraw");
let dir = target_dir();
assert!(
!dir.as_os_str().is_empty(),
"bare-filename LLVM_PROFILE_FILE must fall through to the \
current_exe fallback, not return an empty PathBuf",
);
assert!(
dir.ends_with("llvm-cov-target"),
"fallback must land at the current_exe-relative llvm-cov-target \
dir, got: {}",
dir.display(),
);
}
#[test]
fn redirect_pattern_for_pid_1_returns_none() {
let pattern =
redirect_pattern_for(1, None, true, || PathBuf::from("/should/not/be/called"));
assert!(
pattern.is_none(),
"pid=1 (guest init) must skip env redirect"
);
}
#[test]
fn redirect_pattern_for_existing_env_returns_none() {
let pattern = redirect_pattern_for(
42,
Some(std::ffi::OsString::from("/operator/picked/path.profraw")),
true,
|| PathBuf::from("/should/not/be/called"),
);
assert!(
pattern.is_none(),
"existing LLVM_PROFILE_FILE must take precedence"
);
}
#[test]
fn redirect_pattern_for_empty_env_short_circuits() {
let pattern = redirect_pattern_for(42, Some(std::ffi::OsString::new()), true, || {
PathBuf::from("/should/not/be/called")
});
assert!(
pattern.is_none(),
"Some(\"\") in LLVM_PROFILE_FILE counts as set; redirect must defer"
);
}
#[test]
fn redirect_pattern_for_non_instrumented_binary_returns_none() {
let pattern =
redirect_pattern_for(42, None, false, || PathBuf::from("/should/not/be/called"));
assert!(
pattern.is_none(),
"non-coverage-instrumented binary must not pollute the env passed \
to children"
);
}
#[test]
fn redirect_pattern_for_host_unset_returns_target_pattern() {
let target = PathBuf::from("/synthetic/llvm-cov-target");
let pattern = redirect_pattern_for(42, None, true, || target.clone())
.expect("host pid + unset env + instrumented must produce a redirect pattern");
assert_eq!(
pattern,
PathBuf::from("/synthetic/llvm-cov-target/default-%p-%m.profraw"),
);
}
#[test]
fn redirect_pattern_for_filename_matches_cargo_ktstr_wrapper() {
let target = PathBuf::from("/x");
let pattern = redirect_pattern_for(42, None, true, || target.clone()).unwrap();
assert_eq!(
pattern.file_name().and_then(|n| n.to_str()),
Some("default-%p-%m.profraw"),
"filename suffix must match cargo-ktstr's profraw_inject_for",
);
}
#[test]
fn msg_type_profraw_ascii() {
let bytes = MSG_TYPE_PROFRAW.to_be_bytes();
assert_eq!(&bytes, b"PRAW");
}
#[test]
fn find_symbol_vaddrs_resolves_known_symbol() {
let exe = crate::resolve_current_exe().unwrap();
let data = std::fs::read(&exe).unwrap();
let elf = goblin::elf::Elf::parse(&data).unwrap();
let results = find_symbol_vaddrs(&elf, &["main"]);
assert_eq!(results.len(), 1);
assert!(
results[0].is_some(),
"main symbol should be resolved in test binary"
);
assert_ne!(results[0].unwrap(), 0, "main address should be nonzero");
}
#[test]
fn find_symbol_vaddrs_missing_symbol_returns_none() {
let exe = crate::resolve_current_exe().unwrap();
let data = std::fs::read(&exe).unwrap();
let elf = goblin::elf::Elf::parse(&data).unwrap();
let results = find_symbol_vaddrs(&elf, &["__nonexistent_symbol_xyz__"]);
assert_eq!(results.len(), 1);
assert!(results[0].is_none());
}
#[test]
fn find_symbol_vaddrs_mixed_results() {
let exe = crate::resolve_current_exe().unwrap();
let data = std::fs::read(&exe).unwrap();
let elf = goblin::elf::Elf::parse(&data).unwrap();
let results = find_symbol_vaddrs(&elf, &["main", "__nonexistent_symbol_xyz__"]);
assert_eq!(results.len(), 2);
assert!(results[0].is_some(), "main should resolve");
assert!(results[1].is_none(), "nonexistent should not resolve");
}
#[test]
fn pie_load_bias_returns_nonzero_slide_on_pie_test_binary() {
let exe = crate::resolve_current_exe().unwrap();
let data = std::fs::read(&exe).unwrap();
let elf = goblin::elf::Elf::parse(&data).unwrap();
assert_eq!(
elf.header.e_type,
goblin::elf::header::ET_DYN,
"Rust test binaries default to PIE (ET_DYN); got e_type={}",
elf.header.e_type,
);
let slide = pie_load_bias(&elf);
assert_ne!(
slide, 0,
"ET_DYN binary under default ASLR must produce a non-zero \
load bias; if this assertion fails check that \
/proc/sys/kernel/randomize_va_space is non-zero",
);
}
#[test]
fn pie_load_bias_returns_zero_for_et_exec() {
let exe = crate::resolve_current_exe().unwrap();
let mut data = std::fs::read(&exe).unwrap();
let little_endian = match data[goblin::elf::header::EI_DATA] {
goblin::elf::header::ELFDATA2LSB => true,
goblin::elf::header::ELFDATA2MSB => false,
other => panic!("unexpected EI_DATA byte: 0x{other:02x}"),
};
let et_exec_bytes: [u8; 2] = if little_endian {
goblin::elf::header::ET_EXEC.to_le_bytes()
} else {
goblin::elf::header::ET_EXEC.to_be_bytes()
};
data[goblin::elf::header::SIZEOF_IDENT] = et_exec_bytes[0];
data[goblin::elf::header::SIZEOF_IDENT + 1] = et_exec_bytes[1];
let elf = goblin::elf::Elf::parse(&data).unwrap();
assert_eq!(
elf.header.e_type,
goblin::elf::header::ET_EXEC,
"byte mutation should have made the parsed header report ET_EXEC",
);
assert_eq!(
pie_load_bias(&elf),
0,
"ET_EXEC binary must short-circuit to 0; absolute st_value \
needs no slide",
);
}
}