extern crate self as ktstr;
#[allow(
clippy::all,
dead_code,
non_camel_case_types,
non_snake_case,
non_upper_case_globals
)]
mod bpf_skel;
#[cfg(test)]
#[macro_use]
mod test_macros;
#[doc(hidden)]
pub mod non_exhaustive {}
pub mod cache;
pub mod cgroup;
pub mod flock;
pub(crate) fn errno_name(errno: i32) -> Option<&'static str> {
let e = nix::errno::Errno::from_raw(errno);
if matches!(e, nix::errno::Errno::UnknownErrno) {
return None;
}
Some(match e {
nix::errno::Errno::EPERM => "EPERM",
nix::errno::Errno::ENOENT => "ENOENT",
nix::errno::Errno::ESRCH => "ESRCH",
nix::errno::Errno::EINTR => "EINTR",
nix::errno::Errno::EIO => "EIO",
nix::errno::Errno::ENXIO => "ENXIO",
nix::errno::Errno::E2BIG => "E2BIG",
nix::errno::Errno::ENOEXEC => "ENOEXEC",
nix::errno::Errno::EBADF => "EBADF",
nix::errno::Errno::ECHILD => "ECHILD",
nix::errno::Errno::EAGAIN => "EAGAIN",
nix::errno::Errno::ENOMEM => "ENOMEM",
nix::errno::Errno::EACCES => "EACCES",
nix::errno::Errno::EFAULT => "EFAULT",
nix::errno::Errno::EBUSY => "EBUSY",
nix::errno::Errno::EEXIST => "EEXIST",
nix::errno::Errno::ENODEV => "ENODEV",
nix::errno::Errno::ENOTDIR => "ENOTDIR",
nix::errno::Errno::EISDIR => "EISDIR",
nix::errno::Errno::EINVAL => "EINVAL",
nix::errno::Errno::ENFILE => "ENFILE",
nix::errno::Errno::EMFILE => "EMFILE",
nix::errno::Errno::ENOSPC => "ENOSPC",
nix::errno::Errno::ESPIPE => "ESPIPE",
nix::errno::Errno::EROFS => "EROFS",
nix::errno::Errno::EPIPE => "EPIPE",
nix::errno::Errno::EDOM => "EDOM",
nix::errno::Errno::ERANGE => "ERANGE",
nix::errno::Errno::EDEADLK => "EDEADLK",
nix::errno::Errno::ENAMETOOLONG => "ENAMETOOLONG",
nix::errno::Errno::ENOSYS => "ENOSYS",
nix::errno::Errno::ENOTEMPTY => "ENOTEMPTY",
nix::errno::Errno::ELOOP => "ELOOP",
nix::errno::Errno::ENOTSUP => "ENOTSUP",
nix::errno::Errno::EADDRINUSE => "EADDRINUSE",
nix::errno::Errno::ECONNREFUSED => "ECONNREFUSED",
nix::errno::Errno::ETIMEDOUT => "ETIMEDOUT",
_ => return None,
})
}
pub fn read_kmsg() -> String {
match rmesg::log_entries(rmesg::Backend::Default, false) {
Ok(entries) => entries
.iter()
.map(|e| e.message.as_str())
.collect::<Vec<_>>()
.join("\n"),
Err(_) => String::new(),
}
}
pub mod assert;
pub(crate) mod budget;
pub mod cli;
pub(crate) mod elf_strip;
pub mod fetch;
pub mod host_context;
pub mod host_heap;
pub mod host_state;
pub mod host_state_compare;
pub mod kernel_path;
pub(crate) mod monitor;
pub(crate) mod probe;
pub(crate) mod report;
pub mod runner;
pub mod scenario;
pub(crate) mod stats;
pub mod test_support;
pub(crate) mod timeline;
pub mod topology;
pub mod remote_cache;
pub(crate) mod sync;
pub mod verifier;
pub(crate) mod vm;
pub(crate) mod vmm;
pub mod worker_ready;
pub mod worker_ready_wait;
pub mod workload;
pub(crate) const BUSYBOX: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/busybox"));
pub const EMBEDDED_KCONFIG: &str = include_str!("../ktstr.kconfig");
pub fn kconfig_hash() -> String {
format!("{:08x}", crc32fast::hash(EMBEDDED_KCONFIG.as_bytes()))
}
pub fn cache_key_suffix() -> String {
kconfig_hash()
}
pub use ktstr_macros::Payload;
pub use ktstr_macros::Scheduler;
pub use ktstr_macros::ktstr_test;
#[doc(hidden)]
pub mod __private {
pub use ctor;
pub use linkme;
pub use serde_json;
}
#[cfg(feature = "integration")]
pub use crate::probe::process::resolve_func_ip;
pub mod prelude {
pub use anyhow::Result;
pub use crate::assert::{Assert, AssertResult};
pub use crate::cgroup::CgroupManager;
pub use crate::host_context::HostContext;
pub use crate::host_heap::HostHeapState;
pub use crate::ktstr_test;
pub use crate::scenario::backdrop::Backdrop;
pub use crate::scenario::flags::FlagDecl;
pub use crate::scenario::ops::{
CgroupDef, CpusetSpec, HoldSpec, Op, Setup, Step, execute_defs, execute_scenario,
execute_scenario_with, execute_steps, execute_steps_with,
};
pub use crate::scenario::payload_run::{PayloadHandle, PayloadRun};
pub use crate::scenario::scenarios;
pub use crate::scenario::{CgroupGroup, Ctx, collect_all, spawn_diverse};
pub use crate::test_support::{
BpfMapWrite, CgroupPath, Check, MemSideCache, Metric, MetricBounds, MetricHint,
MetricSource, NumaDistance, NumaNode, OutputFormat, Payload, PayloadKind, PayloadMetrics,
Polarity, Scheduler, SchedulerSpec, SidecarResult, Sysctl, Topology, extract_metrics,
};
pub use crate::{Payload, Scheduler};
pub use crate::topology::{LlcInfo, NodeMemInfo, TestTopology};
pub use crate::workload::{
AffinityKind, AffinityMode, MemPolicy, MpolFlags, Phase, SchedPolicy, Work, WorkType,
WorkerReport, WorkloadConfig, WorkloadHandle, build_nodemask,
};
}
pub const KTSTR_KERNEL_ENV: &str = "KTSTR_KERNEL";
pub const KTSTR_KERNEL_LIST_ENV: &str = "KTSTR_KERNEL_LIST";
pub const KTSTR_KERNEL_PARALLELISM_ENV: &str = "KTSTR_KERNEL_PARALLELISM";
pub const KTSTR_KERNEL_HINT: &str = "set KTSTR_KERNEL to a kernel source directory, \
a version (e.g. `6.14.2`), or a cache key (see `cargo ktstr kernel list`), or run \
`cargo ktstr kernel build` to populate the cache";
pub fn ktstr_kernel_env() -> Option<String> {
std::env::var(KTSTR_KERNEL_ENV)
.ok()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty())
}
pub fn find_kernel() -> anyhow::Result<Option<std::path::PathBuf>> {
use kernel_path::KernelId;
let release = rustix::system::uname()
.release()
.to_str()
.ok()
.map(str::to_owned);
let release_ref = release.as_deref();
let mut skip_cache_scan = false;
if let Some(val) = ktstr_kernel_env() {
match KernelId::parse(&val) {
KernelId::Path(_) => match kernel_path::find_image(Some(&val), release_ref) {
Some(p) => return Ok(Some(p)),
None => anyhow::bail!(
"KTSTR_KERNEL={val} does not contain a kernel image. {KTSTR_KERNEL_HINT}"
),
},
KernelId::Version(ref ver) => {
let cache = cache::CacheDir::new().map_err(|e| {
anyhow::anyhow!(
"KTSTR_KERNEL={val} requires cache access, \
but cache directory could not be opened: {e}"
)
})?;
let arch = std::env::consts::ARCH;
let key = format!("{ver}-tarball-{arch}-kc{}", cache_key_suffix());
if let Some(entry) = cache.lookup(&key) {
return Ok(Some(entry.image_path()));
}
skip_cache_scan = true;
}
KernelId::CacheKey(ref key) => {
let cache = cache::CacheDir::new().map_err(|e| {
anyhow::anyhow!(
"KTSTR_KERNEL={val} requires cache access, \
but cache directory could not be opened: {e}"
)
})?;
if let Some(entry) = cache.lookup(key) {
return Ok(Some(entry.image_path()));
}
skip_cache_scan = true;
}
id @ (KernelId::Range { .. } | KernelId::Git { .. }) => {
if let Err(e) = id.validate() {
anyhow::bail!("KTSTR_KERNEL={val}: {e}");
}
anyhow::bail!(
"KTSTR_KERNEL={val}: multi-kernel specs (ranges, \
git sources) are not supported in env-var form. \
Use --kernel on the test/coverage/verifier \
subcommands, or set KTSTR_KERNEL to a single \
version, cache key, or path."
);
}
}
}
if !skip_cache_scan
&& let Ok(cache) = cache::CacheDir::new()
&& let Ok(entries) = cache.list()
{
let kc_hash = kconfig_hash();
for listed in &entries {
let cache::ListedEntry::Valid(entry) = listed else {
continue;
};
if entry.kconfig_status(&kc_hash).is_stale() {
continue;
}
let image = entry.image_path();
if !image.exists() {
continue;
}
if let Some(vmlinux) = entry.vmlinux_path()
&& let Err(e) = monitor::symbols::KernelSymbols::from_vmlinux(&vmlinux)
{
tracing::warn!(
entry = %entry.path.display(),
error = %e,
"skipping cached kernel with unusable vmlinux"
);
continue;
}
return Ok(Some(image));
}
}
Ok(kernel_path::find_image(None, release_ref))
}
pub fn build_and_find_binary(package: &str) -> anyhow::Result<std::path::PathBuf> {
let output = std::process::Command::new("cargo")
.args(["build", "-p", package, "--message-format=json"])
.current_dir(env!("CARGO_MANIFEST_DIR"))
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.map_err(|e| anyhow::anyhow!("cargo build: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("cargo build -p {package} failed:\n{stderr}");
}
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if let Ok(msg) = serde_json::from_str::<serde_json::Value>(line)
&& msg.get("reason").and_then(|r| r.as_str()) == Some("compiler-artifact")
&& msg
.get("profile")
.and_then(|p| p.get("test"))
.and_then(|t| t.as_bool())
== Some(false)
&& msg
.get("target")
.and_then(|t| t.get("kind"))
.and_then(|k| k.as_array())
.is_some_and(|kinds| kinds.iter().any(|k| k.as_str() == Some("bin")))
&& let Some(filenames) = msg.get("filenames").and_then(|f| f.as_array())
&& let Some(path) = filenames.first().and_then(|f| f.as_str())
{
return Ok(std::path::PathBuf::from(path));
}
}
anyhow::bail!("no binary artifact found for package '{package}'")
}
pub(crate) fn resolve_current_exe() -> anyhow::Result<std::path::PathBuf> {
use anyhow::Context;
let exe = std::env::current_exe().context("resolve current exe")?;
if exe.exists() {
return Ok(exe);
}
let proc_exe = std::path::PathBuf::from("/proc/self/exe");
anyhow::ensure!(
proc_exe.exists(),
"current exe not found: {}",
exe.display()
);
Ok(proc_exe)
}
#[allow(clippy::too_many_arguments)]
pub fn run_shell(
kernel: std::path::PathBuf,
numa_nodes: u32,
llcs: u32,
cores: u32,
threads: u32,
include_files: &[(&str, &std::path::Path)],
memory_mb: Option<u32>,
dmesg: bool,
exec: Option<&str>,
) -> anyhow::Result<()> {
let payload = resolve_current_exe()?;
let owned_includes: Vec<(String, std::path::PathBuf)> = include_files
.iter()
.map(|(a, p)| (a.to_string(), p.to_path_buf()))
.collect();
let mut cmdline = format!("KTSTR_MODE=shell KTSTR_TOPO={numa_nodes},{llcs},{cores},{threads}");
if dmesg {
cmdline.push_str(" loglevel=7");
}
if let Ok(val) = std::env::var("RUST_LOG") {
cmdline.push_str(&format!(" RUST_LOG={val}"));
}
if let Ok(term) = std::env::var("TERM") {
cmdline.push_str(&format!(" KTSTR_TERM={term}"));
}
if let Ok(ct) = std::env::var("COLORTERM") {
cmdline.push_str(&format!(" KTSTR_COLORTERM={ct}"));
}
unsafe {
let mut ws: libc::winsize = std::mem::zeroed();
if libc::ioctl(libc::STDIN_FILENO, libc::TIOCGWINSZ, &mut ws) == 0
&& ws.ws_col > 0
&& ws.ws_row > 0
{
cmdline.push_str(&format!(
" KTSTR_COLS={} KTSTR_ROWS={}",
ws.ws_col, ws.ws_row
));
}
}
let no_perf_mode = std::env::var("KTSTR_NO_PERF_MODE").is_ok();
let mut builder = vmm::KtstrVm::builder()
.kernel(&kernel)
.init_binary(&payload)
.topology(numa_nodes, llcs, cores, threads)
.cmdline(&cmdline)
.include_files(owned_includes)
.busybox(true)
.dmesg(dmesg)
.no_perf_mode(no_perf_mode);
if let Some(cmd) = exec {
builder = builder.exec_cmd(cmd.to_string());
}
builder = match memory_mb {
Some(mb) => builder.memory_mb(mb),
None => builder.memory_deferred(),
};
let vm = builder.build()?;
vm.run_interactive()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_current_exe_happy_path() {
let exe = resolve_current_exe().unwrap();
let std_exe = std::env::current_exe().unwrap();
if std_exe.exists() {
assert_eq!(exe, std_exe);
} else {
assert_eq!(exe, std::path::PathBuf::from("/proc/self/exe"));
}
}
#[test]
fn errno_name_known_values() {
assert_eq!(errno_name(libc::EPERM), Some("EPERM"));
assert_eq!(errno_name(libc::ENOENT), Some("ENOENT"));
assert_eq!(errno_name(libc::EINVAL), Some("EINVAL"));
assert_eq!(errno_name(libc::ENOMEM), Some("ENOMEM"));
assert_eq!(errno_name(libc::EBUSY), Some("EBUSY"));
assert_eq!(errno_name(libc::EACCES), Some("EACCES"));
assert_eq!(errno_name(libc::EAGAIN), Some("EAGAIN"));
assert_eq!(errno_name(libc::ENOSYS), Some("ENOSYS"));
assert_eq!(errno_name(libc::ETIMEDOUT), Some("ETIMEDOUT"));
}
#[test]
fn errno_name_unknown() {
assert_eq!(errno_name(9999), None);
assert_eq!(errno_name(0), None);
assert_eq!(errno_name(-1), None);
}
#[test]
fn find_kernel_preserves_untracked_cache_entries() {
use crate::cache::{CacheArtifacts, CacheDir, KernelMetadata, KernelSource};
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _kernel_guard = EnvVarGuard::remove("KTSTR_KERNEL");
let tmp = tempfile::TempDir::new().unwrap();
let cache_root = tmp.path().join("cache");
let _cache_guard = EnvVarGuard::set("KTSTR_CACHE_DIR", &cache_root);
let cache = CacheDir::with_root(cache_root.clone());
let src_dir = tempfile::TempDir::new().unwrap();
let image = src_dir.path().join("bzImage");
std::fs::write(&image, b"fake kernel image").unwrap();
let meta = KernelMetadata::new(
KernelSource::Tarball,
"x86_64".to_string(),
"bzImage".to_string(),
"2026-04-12T10:00:00Z".to_string(),
)
.with_version(Some("6.14.2".to_string()));
assert!(
meta.ktstr_kconfig_hash.is_none(),
"test fixture must have no recorded kconfig hash to exercise the \
Untracked branch of kconfig_status"
);
let entry = cache
.store("untracked-entry", &CacheArtifacts::new(&image), &meta)
.unwrap();
let expected_image = entry.image_path();
assert!(
expected_image.exists(),
"fixture image must exist on disk so find_kernel's image.exists() \
check passes — got {expected_image:?}"
);
let resolved = find_kernel().unwrap();
assert_eq!(
resolved,
Some(expected_image),
"find_kernel dropped an Untracked cache entry — the kconfig-hash \
filter at lib.rs must treat `Untracked` as keep, not stale"
);
}
#[test]
fn find_kernel_skips_stale_cache_entry() {
use crate::cache::{CacheArtifacts, CacheDir, KernelMetadata, KernelSource};
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let _kernel_guard = EnvVarGuard::remove("KTSTR_KERNEL");
let tmp = tempfile::TempDir::new().unwrap();
let cache_root = tmp.path().join("cache");
let _cache_guard = EnvVarGuard::set("KTSTR_CACHE_DIR", &cache_root);
let current_hash = crate::kconfig_hash();
let stale_hash = format!("{current_hash}-stale");
let cache = CacheDir::with_root(cache_root.clone());
let src_dir = tempfile::TempDir::new().unwrap();
let current_image = src_dir.path().join("current.bzImage");
std::fs::write(¤t_image, b"current kernel image").unwrap();
let current_meta = KernelMetadata::new(
KernelSource::Tarball,
"x86_64".to_string(),
"current.bzImage".to_string(),
"2026-04-01T00:00:00Z".to_string(),
)
.with_version(Some("6.14.2".to_string()))
.with_ktstr_kconfig_hash(Some(current_hash.clone()));
let current_entry = cache
.store(
"current-entry",
&CacheArtifacts::new(¤t_image),
¤t_meta,
)
.unwrap();
let stale_image = src_dir.path().join("stale.bzImage");
std::fs::write(&stale_image, b"stale kernel image").unwrap();
let stale_meta = KernelMetadata::new(
KernelSource::Tarball,
"x86_64".to_string(),
"stale.bzImage".to_string(),
"2026-04-20T00:00:00Z".to_string(),
)
.with_version(Some("6.14.3".to_string()))
.with_ktstr_kconfig_hash(Some(stale_hash));
cache
.store(
"stale-entry",
&CacheArtifacts::new(&stale_image),
&stale_meta,
)
.unwrap();
let resolved = find_kernel().unwrap();
assert_eq!(
resolved,
Some(current_entry.image_path()),
"find_kernel must skip the newer stale entry and return the \
current-hash entry — regression of the KconfigStatus::Stale \
skip branch in find_kernel's cache-scan loop",
);
}
#[test]
fn worker_ready_marker_path_format_is_stable() {
use crate::worker_ready::{WORKER_READY_MARKER_PREFIX, worker_ready_marker_path};
assert_eq!(WORKER_READY_MARKER_PREFIX, "/tmp/ktstr-worker-ready-");
assert_eq!(worker_ready_marker_path(0), "/tmp/ktstr-worker-ready-0");
assert_eq!(
worker_ready_marker_path(12345),
"/tmp/ktstr-worker-ready-12345"
);
assert_eq!(
worker_ready_marker_path(u32::MAX),
"/tmp/ktstr-worker-ready-4294967295"
);
}
#[test]
fn ktstr_kernel_env_round_trips_absolute_path() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _lock = lock_env();
let tmp = tempfile::TempDir::new().unwrap();
let canonical = std::fs::canonicalize(tmp.path()).unwrap();
let _guard = EnvVarGuard::set(KTSTR_KERNEL_ENV, &canonical);
let read_back = ktstr_kernel_env().expect("env is set");
assert_eq!(
std::path::PathBuf::from(&read_back),
canonical,
"writer-reader round-trip must preserve the exact path; \
drift between parent's canonicalize output and child's \
ktstr_kernel_env read breaks every downstream resolver",
);
}
#[test]
fn ktstr_kernel_env_unset_is_none() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _lock = lock_env();
let _guard = EnvVarGuard::remove(KTSTR_KERNEL_ENV);
assert!(
ktstr_kernel_env().is_none(),
"unset KTSTR_KERNEL must read as None so fallback resolvers activate",
);
}
#[test]
fn ktstr_kernel_env_empty_is_none() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _lock = lock_env();
let _guard = EnvVarGuard::set(KTSTR_KERNEL_ENV, "");
assert!(
ktstr_kernel_env().is_none(),
"empty KTSTR_KERNEL must collapse to None; CI flows routinely \
pass empty strings for unused variables",
);
}
#[test]
fn ktstr_kernel_env_whitespace_is_none() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _lock = lock_env();
let _guard = EnvVarGuard::set(KTSTR_KERNEL_ENV, " \t\n ");
assert!(
ktstr_kernel_env().is_none(),
"whitespace-only KTSTR_KERNEL must collapse to None via trim \
+ empty-filter; no caller parses a whitespace-only value \
meaningfully",
);
}
#[test]
fn ktstr_kernel_env_trims_surrounding_whitespace() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _lock = lock_env();
let _guard = EnvVarGuard::set(KTSTR_KERNEL_ENV, " ../linux ");
let read_back = ktstr_kernel_env().expect("env is set");
assert_eq!(
read_back, "../linux",
"surrounding whitespace must be trimmed but the interior \
preserved verbatim",
);
}
#[test]
fn find_kernel_inverted_range_env_surfaces_swap_diagnostic() {
use crate::test_support::test_helpers::{EnvVarGuard, lock_env};
let _env_lock = lock_env();
let tmp = tempfile::TempDir::new().unwrap();
let cache_root = tmp.path().join("cache");
let _cache_guard = EnvVarGuard::set("KTSTR_CACHE_DIR", &cache_root);
let _kernel_guard = EnvVarGuard::set(KTSTR_KERNEL_ENV, "6.16..6.12");
let err = find_kernel().expect_err("inverted range must error");
let msg = format!("{err:#}");
assert!(
msg.contains("inverted kernel range"),
"validate() diagnostic must surface ahead of the generic \
env-form bail; got: {msg}",
);
assert!(
msg.contains("6.12..6.16"),
"swap suggestion must appear in the error; got: {msg}",
);
assert!(
!msg.contains("not supported in env-var form"),
"validate() must short-circuit before the generic bail; got: {msg}",
);
}
#[test]
fn ktstr_kernel_env_constant_is_literal() {
assert_eq!(KTSTR_KERNEL_ENV, "KTSTR_KERNEL");
}
}