use anyhow::Result;
use ktstr::assert::{AssertDetail, AssertResult, DetailKind};
use ktstr::ktstr_test;
use ktstr::live_host::{BpfMapAccessor, BpfSyscallAccessor, KallsymsTable, LiveHostKernelEnv};
use ktstr::scenario::Ctx;
use ktstr::scenario::ops::{CgroupDef, HoldSpec, Step, execute_steps};
const KTSTR_SCHED: ktstr::prelude::Scheduler = ktstr::prelude::Scheduler::new("ktstr_sched")
.binary(ktstr::prelude::SchedulerSpec::Discover("scx-ktstr"));
const KTSTR_SCHED_PAYLOAD: ktstr::prelude::Payload =
ktstr::prelude::Payload::from_scheduler(&KTSTR_SCHED);
#[ktstr_test(llcs = 1, cores = 2, threads = 1, duration_s = 10, watchdog_timeout_s = 60, scheduler = KTSTR_SCHED_PAYLOAD)]
fn live_host_pipeline_inside_guest_produces_expected_shape(ctx: &Ctx) -> Result<AssertResult> {
let steps = vec![Step {
setup: vec![CgroupDef::named("cg_0").workers(ctx.workers_per_cgroup)].into(),
ops: vec![],
hold: HoldSpec::Fixed(std::time::Duration::from_secs(2)),
}];
let _ = execute_steps(ctx, steps)?;
let env = match LiveHostKernelEnv::discover() {
Ok(env) => env,
Err(e) => {
return Ok(AssertResult::fail(AssertDetail::new(
DetailKind::Other,
format!(
"LiveHostKernelEnv::discover() failed inside the \
guest: {e}. The live-host pipeline cannot run \
without BTF (sched_ext-capable kernels mandate \
CONFIG_DEBUG_INFO_BTF, so /sys/kernel/btf/vmlinux \
should be present)."
),
)));
}
};
if env.release.is_empty() {
return Ok(AssertResult::fail(AssertDetail::new(
DetailKind::Other,
"LiveHostKernelEnv::discover() returned an empty kernel \
release string — uname(2) wrapper produced an empty \
utsname.release, indicating a libc / kernel ABI \
regression",
)));
}
if !env.btf_path.exists() {
return Ok(AssertResult::fail(AssertDetail::new(
DetailKind::Other,
format!(
"LiveHostKernelEnv resolved btf_path={} but the file \
does not exist — the discovery walk found a stale \
candidate path",
env.btf_path.display()
),
)));
}
let accessor = match BpfSyscallAccessor::from_running_kernel() {
Ok(a) => a,
Err(e) => {
let status = std::fs::read_to_string("/proc/self/status")
.unwrap_or_else(|e| format!("(read /proc/self/status: {e})"));
let pid = unsafe { libc::getpid() };
let euid = unsafe { libc::geteuid() };
let mut diagnostic =
format!("pid={pid} euid={euid}\n--- /proc/self/status ---\n{status}\n",);
if let Ok(v) = std::fs::read_to_string("/proc/sys/kernel/unprivileged_bpf_disabled") {
diagnostic.push_str(&format!("--- unprivileged_bpf_disabled ---\n{v}"));
}
if let Ok(v) = std::fs::read_to_string("/sys/kernel/security/lockdown") {
diagnostic.push_str(&format!("--- /sys/kernel/security/lockdown ---\n{v}"));
}
return Ok(AssertResult::fail(AssertDetail::new(
DetailKind::Other,
format!(
"BpfSyscallAccessor::from_running_kernel() failed \
inside the guest: {e}. ktstr runs as root and \
CAP_SYS_ADMIN should be available — a failure \
here means BPF_MAP_GET_NEXT_ID returned a kernel \
error other than ENOENT (the end-of-iteration \
sentinel), which would indicate a deeper BPF \
subsystem problem.\n\nDIAGNOSTIC:\n{diagnostic}"
),
)));
}
};
let maps = accessor.maps();
if maps.is_empty() {
return Ok(AssertResult::fail(AssertDetail::new(
DetailKind::Other,
"BpfSyscallAccessor::maps() returned an empty list — the \
scx-ktstr scheduler should have at least one BPF map \
loaded by the time this test runs. Either the scheduler \
didn't attach (would have shown earlier in the test \
pipeline) or BPF_MAP_GET_NEXT_ID enumerated nothing, \
which contradicts the kernel's id space",
)));
}
let any_scx = maps
.iter()
.any(|m| m.name().contains("scx") || m.name().contains("ktstr"));
if !any_scx {
let names: Vec<std::borrow::Cow<'_, str>> = maps.iter().map(|m| m.name()).collect();
return Ok(AssertResult::fail(AssertDetail::new(
DetailKind::Other,
format!(
"BpfSyscallAccessor enumerated {} maps but none \
contained 'scx' or 'ktstr' in their name. Map \
names observed: {:?}. Either the scheduler isn't \
attached or its map names diverged from the \
scx-ktstr / scx_* convention",
maps.len(),
names
),
)));
}
let kallsyms = match KallsymsTable::load_from(&env) {
Ok(t) => t,
Err(e) => {
return Ok(AssertResult::fail(AssertDetail::new(
DetailKind::Other,
format!(
"KallsymsTable::load_from(env) failed inside the \
guest: {e}. /proc/kallsyms is root-readable; \
ktstr runs as root, so a load failure here \
means /proc/kallsyms wasn't mounted or returned \
EIO"
),
)));
}
};
if kallsyms.is_empty() {
return Ok(AssertResult::fail(AssertDetail::new(
DetailKind::Other,
"KallsymsTable parsed /proc/kallsyms inside the guest \
but the resulting table is empty — every line had a \
zero address, which is the CAP_SYSLOG-redacted view. \
ktstr's guest runs with kptr_restrict=0 and the \
process has CAP_SYSLOG, so this indicates a sysctl / \
capability regression",
)));
}
let sched_class = kallsyms.resolve("ext_sched_class");
let disable_fn = kallsyms.resolve("scx_disable_workfn");
if sched_class.is_none() && disable_fn.is_none() {
return Ok(AssertResult::fail(AssertDetail::new(
DetailKind::Other,
format!(
"Neither ext_sched_class nor scx_disable_workfn \
resolved via the live-host kallsyms table. \
Total symbols parsed: {}. Sample names from the \
table won't help here without dumping the whole \
table; the failure indicates either a symbol \
rename in the running kernel or a parse \
regression",
kallsyms.len()
),
)));
}
Ok(AssertResult::pass())
}