use crate::monitor::bpf_map::GuestMemMapAccessorOwned;
use crate::monitor::btf_offsets::ScxWalkerOffsets;
use crate::monitor::symbols::KernelSymbols;
pub(crate) struct ScxWalkerOwned {
pub(crate) rq_kvas: Vec<u64>,
pub(crate) rq_pas: Vec<u64>,
pub(crate) scx_root_kva: u64,
pub(crate) scx_tasks_kva: u64,
pub(crate) kaslr_offset: u64,
}
#[allow(dead_code)]
pub(crate) fn build(
owned_accessor: &GuestMemMapAccessorOwned,
offsets: Option<&ScxWalkerOffsets>,
symbols: Option<&KernelSymbols>,
per_cpu_offsets: Option<&[u64]>,
kaslr_offset: u64,
) -> Option<ScxWalkerOwned> {
let _offs = offsets?;
let syms = symbols?;
let page_offset = owned_accessor.guest_kernel().page_offset();
let scx_root_kva = syms.scx_root.unwrap_or(0);
let scx_tasks_kva = syms.scx_tasks.unwrap_or(0);
let pco = match per_cpu_offsets {
Some(pco) => pco,
None => {
tracing::debug!(
"capture_scx::build: per_cpu_offsets absent — degraded \
capture with no rq arrays; global scx_tasks walk and \
*scx_root read still active",
);
return Some(ScxWalkerOwned {
rq_kvas: Vec::new(),
rq_pas: Vec::new(),
scx_root_kva,
scx_tasks_kva,
kaslr_offset,
});
}
};
Some(compute_owned(
page_offset,
syms.runqueues,
scx_root_kva,
scx_tasks_kva,
pco,
kaslr_offset,
))
}
fn compute_owned(
page_offset: u64,
runqueues_kva: u64,
scx_root_kva: u64,
scx_tasks_kva: u64,
per_cpu_offsets: &[u64],
kaslr_offset: u64,
) -> ScxWalkerOwned {
let n = per_cpu_offsets.len();
let mut rq_pas: Vec<u64> = Vec::with_capacity(n);
let mut rq_kvas: Vec<u64> = Vec::with_capacity(n);
for &offset in per_cpu_offsets {
let kva = crate::monitor::symbols::per_cpu_kva(runqueues_kva, kaslr_offset, offset);
let pa = crate::monitor::symbols::kva_to_pa(kva, page_offset);
rq_pas.push(pa);
rq_kvas.push(pa.wrapping_add(page_offset));
}
ScxWalkerOwned {
rq_kvas,
rq_pas,
scx_root_kva,
scx_tasks_kva,
kaslr_offset,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::monitor::guest::GuestKernel;
use crate::monitor::reader::GuestMem;
use crate::monitor::symbols::DEFAULT_PAGE_OFFSET;
use std::collections::HashMap;
use std::sync::Arc;
fn empty_scx_walker_offsets() -> ScxWalkerOffsets {
ScxWalkerOffsets {
rq: None,
scx_rq: None,
task: None,
see: None,
dsq_lnode: None,
dsq: None,
sched: None,
sched_pnode: None,
sched_pcpu: None,
rht: None,
}
}
fn symbols_with_scx(scx_root: Option<u64>, scx_tasks: Option<u64>) -> KernelSymbols {
KernelSymbols {
runqueues: 0,
per_cpu_offset: 0,
page_offset_base_kva: None,
phys_base_kva: None,
scx_root,
scx_tasks,
init_top_pgt: None,
pgtable_l5_enabled: None,
prog_idr: None,
scx_watchdog_timeout: None,
scx_watchdog_timestamp: None,
scx_watchdog_interval: None,
jiffies_64: None,
kernel_cpustat: None,
kstat: None,
tick_cpu_sched: None,
node_data: None,
entry_syscall_64_kva: None,
kernel_text_kva: None,
}
}
fn test_accessor() -> GuestMemMapAccessorOwned {
let buf = Box::leak(Box::new([0u8; 64]));
let mem = unsafe { GuestMem::new(buf.as_mut_ptr(), buf.len() as u64) };
let kernel =
GuestKernel::new_for_test(Arc::new(mem), HashMap::new(), DEFAULT_PAGE_OFFSET, 0, false);
GuestMemMapAccessorOwned::new_for_test(kernel)
}
#[test]
fn compute_owned_happy_path() {
let page_offset = DEFAULT_PAGE_OFFSET;
let runqueues_kva: u64 = 0x20_0000;
let per_cpu = [0x10_0000u64, 0x14_0000u64, 0x18_0000u64];
let scx_root_kva = 0xffff_ffff_8230_0000;
let scx_tasks_kva = 0xffff_ffff_8240_0000;
let owned = compute_owned(
page_offset,
runqueues_kva,
scx_root_kva,
scx_tasks_kva,
&per_cpu,
0,
);
assert_eq!(owned.scx_root_kva, scx_root_kva);
assert_eq!(owned.scx_tasks_kva, scx_tasks_kva);
assert_eq!(owned.rq_kvas.len(), 3);
assert_eq!(owned.rq_pas.len(), 3);
let expected_pas =
crate::monitor::symbols::compute_rq_pas(runqueues_kva, &per_cpu, page_offset, 0);
assert_eq!(owned.rq_pas, expected_pas);
for (cpu, expected_pa) in expected_pas.iter().enumerate() {
assert_eq!(owned.rq_kvas[cpu], expected_pa.wrapping_add(page_offset),);
}
}
#[test]
fn compute_owned_partial_scx_root_zero() {
let page_offset = DEFAULT_PAGE_OFFSET;
let runqueues_kva: u64 = 0x20_0000;
let per_cpu = [0x10_0000u64, 0x14_0000u64];
let owned = compute_owned(page_offset, runqueues_kva, 0, 0, &per_cpu, 0);
assert_eq!(owned.scx_root_kva, 0);
assert_eq!(owned.scx_tasks_kva, 0);
assert_eq!(owned.rq_kvas.len(), 2);
assert_eq!(owned.rq_pas.len(), 2);
let expected_pas =
crate::monitor::symbols::compute_rq_pas(runqueues_kva, &per_cpu, page_offset, 0);
assert_eq!(owned.rq_pas, expected_pas);
}
#[test]
fn compute_owned_partial_scx_tasks_zero() {
let page_offset = DEFAULT_PAGE_OFFSET;
let runqueues_kva: u64 = 0x20_0000;
let per_cpu = [0x10_0000u64];
let scx_root_kva = 0xffff_ffff_8230_0000;
let owned = compute_owned(page_offset, runqueues_kva, scx_root_kva, 0, &per_cpu, 0);
assert_eq!(owned.scx_root_kva, scx_root_kva);
assert_eq!(owned.scx_tasks_kva, 0);
assert_eq!(owned.rq_kvas.len(), 1);
assert_eq!(owned.rq_pas.len(), 1);
}
#[test]
fn degraded_build_shape_empty_rq_with_symbol_kvas() {
let scx_root_kva = 0xffff_ffff_8230_0000;
let scx_tasks_kva = 0xffff_ffff_8240_0000;
let accessor = test_accessor();
let offsets = empty_scx_walker_offsets();
let symbols = symbols_with_scx(Some(scx_root_kva), Some(scx_tasks_kva));
let owned = build(&accessor, Some(&offsets), Some(&symbols), None, 0)
.expect("offsets + symbols present → degraded build returns Some");
assert!(owned.rq_kvas.is_empty());
assert!(owned.rq_pas.is_empty());
assert_eq!(owned.scx_root_kva, scx_root_kva);
assert_eq!(owned.scx_tasks_kva, scx_tasks_kva);
assert_eq!(owned.kaslr_offset, 0);
}
#[test]
fn build_returns_none_when_hard_prereq_absent() {
let accessor = test_accessor();
let offsets = empty_scx_walker_offsets();
let symbols = symbols_with_scx(Some(0xffff_ffff_8230_0000), Some(0xffff_ffff_8240_0000));
assert!(build(&accessor, None, Some(&symbols), None, 0).is_none());
assert!(build(&accessor, Some(&offsets), None, None, 0).is_none());
}
#[test]
fn degraded_build_scx_tasks_kva_independent_of_rq_arrays() {
let scx_tasks_kva = 0xffff_ffff_82e5_e840;
let accessor = test_accessor();
let offsets = empty_scx_walker_offsets();
let symbols = symbols_with_scx(None, Some(scx_tasks_kva));
let owned = build(&accessor, Some(&offsets), Some(&symbols), None, 0)
.expect("offsets + symbols present → degraded build returns Some");
assert!(owned.rq_kvas.is_empty());
assert!(owned.rq_pas.is_empty());
assert_eq!(owned.scx_tasks_kva, scx_tasks_kva);
assert_eq!(owned.scx_root_kva, 0);
}
#[test]
fn compute_owned_empty_per_cpu_offsets() {
let page_offset = DEFAULT_PAGE_OFFSET;
let runqueues_kva: u64 = 0x20_0000;
let scx_root_kva = 0xffff_ffff_8230_0000;
let scx_tasks_kva = 0xffff_ffff_8240_0000;
let owned = compute_owned(
page_offset,
runqueues_kva,
scx_root_kva,
scx_tasks_kva,
&[],
0,
);
assert!(owned.rq_kvas.is_empty());
assert!(owned.rq_pas.is_empty());
assert_eq!(owned.scx_root_kva, scx_root_kva);
assert_eq!(owned.scx_tasks_kva, scx_tasks_kva);
}
#[test]
fn compute_owned_wrapping_arithmetic() {
let page_offset = DEFAULT_PAGE_OFFSET;
let runqueues_kva: u64 = 0x1000;
let per_cpu = [page_offset.wrapping_sub(runqueues_kva)];
let owned = compute_owned(page_offset, runqueues_kva, 0, 0, &per_cpu, 0);
assert_eq!(owned.rq_pas, vec![0u64]);
assert_eq!(owned.rq_kvas, vec![page_offset]);
}
#[test]
fn compute_owned_kva_pa_pairwise_consistent() {
let page_offset = DEFAULT_PAGE_OFFSET;
let runqueues_kva: u64 = 0x4_0000;
let per_cpu = [
0x10_0000u64,
0x14_0000u64,
0x18_0000u64,
0x1c_0000u64,
0x20_0000u64,
];
let owned = compute_owned(
page_offset,
runqueues_kva,
0xffff_ffff_8000_0000,
0xffff_ffff_8001_0000,
&per_cpu,
0,
);
assert_eq!(owned.rq_kvas.len(), per_cpu.len());
assert_eq!(owned.rq_pas.len(), per_cpu.len());
for cpu in 0..per_cpu.len() {
assert_eq!(
owned.rq_kvas[cpu],
owned.rq_pas[cpu].wrapping_add(page_offset),
"kva/pa pair mismatch on cpu {cpu}",
);
}
}
#[test]
fn compute_owned_matches_compute_rq_pas_under_kaslr() {
let page_offset = DEFAULT_PAGE_OFFSET;
let runqueues_kva: u64 = 0x20_0000;
let per_cpu = [0x10_0000u64, 0x14_0000u64, 0x18_0000u64];
let kaslr = 0x1_0000_0000u64; let owned = compute_owned(
page_offset,
runqueues_kva,
0xffff_ffff_8230_0000,
0xffff_ffff_8240_0000,
&per_cpu,
kaslr,
);
let expected_pas =
crate::monitor::symbols::compute_rq_pas(runqueues_kva, &per_cpu, page_offset, kaslr);
assert_eq!(
owned.rq_pas, expected_pas,
"compute_owned and compute_rq_pas must agree on non-zero \
kaslr — asymmetric kaslr threading would surface here",
);
}
}