use serde::{Deserialize, Serialize};
use anyhow::{Context, Result};
use btf_rs::{Btf, Type};
use super::Kva;
use super::btf_offsets::{StructOrFwd, find_struct_or_fwd, member_byte_offset};
use super::btf_render::{MemReader, RenderedValue, render_value_with_mem};
use super::dump::hex_dump;
use super::guest::GuestKernel;
const SDT_TASK_LEVELS: usize = 3;
const SDT_TASK_ENTS_PER_PAGE_SHIFT: u32 = 9;
const SDT_TASK_ENTS_PER_CHUNK: usize = 1 << SDT_TASK_ENTS_PER_PAGE_SHIFT; const SDT_TASK_CHUNK_BITMAP_U64S: usize = SDT_TASK_ENTS_PER_CHUNK / 64;
pub const MAX_SDT_ALLOC_ENTRIES: usize = 4096;
const SIZEOF_SDT_ID: usize = 8;
const MIN_ELEM_SIZE: u64 = 16;
const MAX_ELEM_SIZE: u64 = 4096;
pub(crate) const MAX_BTF_ID_PROBE: u32 = 100_000;
#[derive(Debug, Clone)]
pub struct SdtAllocOffsets {
pub allocator_pool: usize,
pub allocator_root: usize,
pub allocator_size: usize,
pub pool_elem_size: usize,
pub desc_allocated: usize,
pub desc_nr_free: usize,
pub desc_chunk: usize,
pub chunk_union: usize,
pub data_header_size: usize,
}
impl SdtAllocOffsets {
pub fn from_btf(btf: &Btf) -> Result<Self> {
let allocator = require_full_struct(btf, "scx_allocator").context(
"btf: struct scx_allocator unavailable (scheduler doesn't link sdt_alloc, or BTF only carries a forward declaration)"
)?;
let allocator_pool = member_byte_offset(btf, &allocator, "pool")?;
let allocator_root = member_byte_offset(btf, &allocator, "root")?;
let allocator_size = allocator.size();
let pool = require_full_struct(btf, "sdt_pool")
.context("btf: struct sdt_pool unavailable for member offsets")?;
let pool_elem_size = member_byte_offset(btf, &pool, "elem_size")?;
let desc = require_full_struct(btf, "sdt_desc")
.context("btf: struct sdt_desc unavailable for member offsets")?;
let desc_allocated = member_byte_offset(btf, &desc, "allocated")?;
let desc_nr_free = member_byte_offset(btf, &desc, "nr_free")?;
let desc_chunk = member_byte_offset(btf, &desc, "chunk")?;
let chunk_union = match find_struct_or_fwd(btf, "sdt_chunk")
.context("btf: struct sdt_chunk not found")?
{
StructOrFwd::Full(chunk) => chunk_union_offset(btf, &chunk)?,
StructOrFwd::Fwd => 0,
};
let data_header_size =
match find_struct_or_fwd(btf, "sdt_data").context("btf: struct sdt_data not found")? {
StructOrFwd::Full(data) => data.size(),
StructOrFwd::Fwd => SIZEOF_SDT_ID,
};
Ok(Self {
allocator_pool,
allocator_root,
allocator_size,
pool_elem_size,
desc_allocated,
desc_nr_free,
desc_chunk,
chunk_union,
data_header_size,
})
}
}
fn require_full_struct(btf: &Btf, name: &str) -> Result<btf_rs::Struct> {
match find_struct_or_fwd(btf, name)? {
StructOrFwd::Full(s) => Ok(s),
StructOrFwd::Fwd => anyhow::bail!(
"btf: struct {name} present only as BTF_KIND_FWD forward declaration; member offsets unavailable"
),
}
}
fn chunk_union_offset(btf: &Btf, chunk: &btf_rs::Struct) -> Result<usize> {
if let Ok(off) = member_byte_offset(btf, chunk, "descs") {
return Ok(off);
}
if let Ok(off) = member_byte_offset(btf, chunk, "data") {
return Ok(off);
}
anyhow::bail!("btf: struct sdt_chunk has neither `descs` nor `data` member")
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SdtAllocEntry {
pub idx: i32,
pub genn: i32,
pub user_addr: u64,
pub payload: RenderedValue,
}
impl std::fmt::Display for SdtAllocEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"idx={} genn={} user_addr={:#x} payload=",
self.idx, self.genn, self.user_addr
)?;
std::fmt::Display::fmt(&self.payload, f)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SdtAllocatorSnapshot {
pub allocator_name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub entries: Vec<SdtAllocEntry>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub truncated: bool,
pub skipped_subtrees: u32,
pub elem_size: u64,
pub target_type_id: u32,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub payload_type_reason: String,
#[serde(skip)]
pub all_slot_addrs: Vec<u64>,
}
impl std::fmt::Display for SdtAllocatorSnapshot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"sdt_alloc {} (elem_size={}, target_type_id={}",
self.allocator_name, self.elem_size, self.target_type_id
)?;
if !self.payload_type_reason.is_empty() {
write!(f, ", reason={}", self.payload_type_reason)?;
}
write!(f, "): {} live", self.entries.len())?;
if self.truncated {
f.write_str(" (truncated)")?;
}
if self.skipped_subtrees > 0 {
write!(f, " ({} subtrees skipped)", self.skipped_subtrees)?;
}
for entry in &self.entries {
f.write_str("\n")?;
super::btf_render::write_value_at_depth(f, &entry.payload, 2)?;
}
Ok(())
}
}
#[allow(clippy::too_many_arguments)]
pub fn walk_sdt_allocator(
kernel: &GuestKernel,
kern_vm_start: u64,
allocator_bytes: &[u8],
offsets: &SdtAllocOffsets,
btf: &Btf,
target_type_id: u32,
payload_type_reason: impl Into<String>,
allocator_name: impl Into<String>,
mem: &dyn MemReader,
) -> SdtAllocatorSnapshot {
let mut snap = SdtAllocatorSnapshot {
allocator_name: allocator_name.into(),
entries: Vec::new(),
truncated: false,
skipped_subtrees: 0,
elem_size: 0,
target_type_id,
payload_type_reason: payload_type_reason.into(),
all_slot_addrs: Vec::new(),
};
let pool_off = offsets.allocator_pool + offsets.pool_elem_size;
let Some(elem_size) = read_u64_at(allocator_bytes, pool_off) else {
return snap;
};
if !(MIN_ELEM_SIZE..=MAX_ELEM_SIZE).contains(&elem_size) {
snap.elem_size = elem_size;
return snap;
}
snap.elem_size = elem_size;
let header = offsets.data_header_size;
if elem_size < header as u64 {
return snap;
}
let payload_size = (elem_size - header as u64) as usize;
let Some(root_ptr) = read_u64_at(allocator_bytes, offsets.allocator_root) else {
return snap;
};
if root_ptr == 0 {
return snap;
}
let mut walker = TreeWalker {
kernel,
kern_vm_start,
offsets,
btf,
target_type_id,
payload_size,
mem,
out: &mut snap,
};
walker.descend(root_ptr, 0);
snap
}
#[derive(Debug, Clone)]
pub struct PayloadTypeChoice {
pub target_type_id: u32,
pub reason: String,
}
pub fn discover_payload_btf_id(
btf: &Btf,
payload_size: usize,
allocator_name: &str,
) -> PayloadTypeChoice {
if payload_size == 0 {
return PayloadTypeChoice {
target_type_id: 0,
reason: "payload_size == 0".into(),
};
}
let mut size_matches: Vec<(u32, String)> = Vec::new();
const CONSECUTIVE_FAIL_CAP: u32 = 64;
let mut tid: u32 = 1;
let mut consecutive_fail: u32 = 0;
while tid < MAX_BTF_ID_PROBE {
match btf.resolve_type_by_id(tid) {
Ok(ty) => {
consecutive_fail = 0;
if let Type::Struct(s) = ty
&& s.size() == payload_size
&& let Ok(name) = btf.resolve_name(&s)
&& !name.is_empty()
{
size_matches.push((tid, name));
}
}
Err(_) => {
consecutive_fail += 1;
if consecutive_fail >= CONSECUTIVE_FAIL_CAP {
break;
}
}
}
tid += 1;
}
match size_matches.len() {
0 => PayloadTypeChoice {
target_type_id: 0,
reason: format!("no candidate of size {payload_size}"),
},
1 => PayloadTypeChoice {
target_type_id: size_matches[0].0,
reason: String::new(),
},
n => {
let name_stem = allocator_name
.strip_suffix("_allocator")
.unwrap_or(allocator_name);
if !name_stem.is_empty() {
let stems: &[&str] = &[name_stem, name_stem.strip_prefix("scx_").unwrap_or("")];
for stem in stems {
if stem.is_empty() {
continue;
}
let hits: Vec<u32> = size_matches
.iter()
.filter(|(_, sn)| sn == stem)
.map(|(id, _)| *id)
.collect();
if hits.len() == 1 {
return PayloadTypeChoice {
target_type_id: hits[0],
reason: String::new(),
};
}
}
}
type Pat = fn(&str) -> bool;
let patterns: &[Pat] = &[
|n: &str| n == "task_ctx",
|n: &str| n.ends_with("_arena_ctx"),
|n: &str| n.ends_with("_task_ctx"),
|n: &str| n.ends_with("_ctx"),
];
for pat in patterns {
let hits: Vec<u32> = size_matches
.iter()
.filter(|(_, n)| pat(n))
.map(|(id, _)| *id)
.collect();
match hits.len() {
0 => continue,
1 => {
return PayloadTypeChoice {
target_type_id: hits[0],
reason: String::new(),
};
}
_ => {
continue;
}
}
}
PayloadTypeChoice {
target_type_id: 0,
reason: format!("ambiguous: {n} candidates"),
}
}
}
}
struct TreeWalker<'a> {
kernel: &'a GuestKernel,
kern_vm_start: u64,
offsets: &'a SdtAllocOffsets,
btf: &'a Btf,
target_type_id: u32,
payload_size: usize,
mem: &'a dyn MemReader,
out: &'a mut SdtAllocatorSnapshot,
}
impl<'a> TreeWalker<'a> {
fn descend(&mut self, desc_ptr: u64, level: usize) {
if level >= SDT_TASK_LEVELS {
return;
}
if self.out.truncated {
return;
}
let Some(desc_pa) = self.translate_arena_ptr(desc_ptr) else {
self.out.skipped_subtrees = self.out.skipped_subtrees.saturating_add(1);
return;
};
let mut allocated = [0u64; SDT_TASK_CHUNK_BITMAP_U64S];
let mem = self.kernel.mem();
for (i, slot) in allocated.iter_mut().enumerate() {
*slot = mem.read_u64(desc_pa, self.offsets.desc_allocated + i * 8);
}
let nr_free = mem.read_u64(desc_pa, self.offsets.desc_nr_free);
if nr_free > SDT_TASK_ENTS_PER_CHUNK as u64 {
self.out.skipped_subtrees = self.out.skipped_subtrees.saturating_add(1);
return;
}
let chunk_ptr = mem.read_u64(desc_pa, self.offsets.desc_chunk);
if chunk_ptr == 0 {
self.out.skipped_subtrees = self.out.skipped_subtrees.saturating_add(1);
return;
}
let Some(chunk_pa) = self.translate_arena_ptr(chunk_ptr) else {
self.out.skipped_subtrees = self.out.skipped_subtrees.saturating_add(1);
return;
};
if level == SDT_TASK_LEVELS - 1 {
for (word_idx, &word_value) in allocated.iter().enumerate() {
let mut word = word_value;
while word != 0 {
let bit = word.trailing_zeros() as usize;
word &= word - 1;
let pos = word_idx * 64 + bit;
if pos >= SDT_TASK_ENTS_PER_CHUNK {
continue;
}
let entry_ptr_off = self.offsets.chunk_union + pos * 8;
let entry_ptr = mem.read_u64(chunk_pa, entry_ptr_off);
if entry_ptr == 0 {
tracing::debug!(
allocator = %self.out.allocator_name,
pos,
"sdt_alloc walker: leaf data[pos] == 0 (bit set, \
pointer store not yet committed — scx_alloc_internal \
populates the pointer after the bitmap bit)",
);
continue;
}
self.emit_leaf(entry_ptr);
}
}
} else {
for pos in 0..SDT_TASK_ENTS_PER_CHUNK {
let entry_ptr_off = self.offsets.chunk_union + pos * 8;
let entry_ptr = mem.read_u64(chunk_pa, entry_ptr_off);
if entry_ptr == 0 {
tracing::trace!(
allocator = %self.out.allocator_name,
level,
pos,
"sdt_alloc walker: internal desc[pos] == 0 \
(never-created subtree)",
);
continue;
}
self.descend(entry_ptr, level + 1);
}
}
}
fn emit_leaf(&mut self, data_ptr: u64) {
self.out.all_slot_addrs.push(data_ptr & 0xFFFF_FFFF);
if self.out.entries.len() >= MAX_SDT_ALLOC_ENTRIES {
self.out.truncated = true;
return;
}
let Some(data_pa) = self.translate_arena_ptr(data_ptr) else {
self.out.skipped_subtrees = self.out.skipped_subtrees.saturating_add(1);
return;
};
let mem = self.kernel.mem();
let idx = mem.read_u32(data_pa, 0) as i32;
let genn = mem.read_u32(data_pa, 4) as i32;
let mut payload_bytes = vec![0u8; self.payload_size];
let n = mem.read_bytes(
data_pa + self.offsets.data_header_size as u64,
&mut payload_bytes,
);
payload_bytes.truncate(n);
if payload_bytes.is_empty() {
self.out.entries.push(SdtAllocEntry {
idx,
genn,
user_addr: data_ptr & 0xFFFF_FFFF,
payload: RenderedValue::Unsupported {
reason: "payload read failed: end-of-DRAM or unmapped page".into(),
},
});
return;
}
if payload_bytes.iter().all(|&b| b == 0) {
tracing::debug!(
allocator = %self.out.allocator_name,
idx,
genn,
user_addr = format_args!("{:#x}", data_ptr & 0xFFFF_FFFF),
payload_len = payload_bytes.len(),
"sdt_alloc walker: all-zero payload (mid-free race? scx_alloc_free_idx \
zeros payload before clearing the bitmap)",
);
}
let payload = if self.target_type_id != 0 {
render_value_with_mem(self.btf, self.target_type_id, &payload_bytes, self.mem)
} else {
RenderedValue::Bytes {
hex: hex_dump(&payload_bytes),
}
};
self.out.entries.push(SdtAllocEntry {
idx,
genn,
user_addr: data_ptr & 0xFFFF_FFFF,
payload,
});
}
fn translate_arena_ptr(&self, ptr: u64) -> Option<u64> {
if ptr == 0 {
return None;
}
let kva = self.kern_vm_start.wrapping_add(ptr & 0xFFFF_FFFF);
let pa = self.kernel.mem().translate_kva(
self.kernel.cr3_pa(),
Kva(kva),
self.kernel.l5(),
self.kernel.tcr_el1(),
)?;
if pa >= self.kernel.mem().size() {
return None;
}
Some(pa)
}
}
fn read_u64_at(bytes: &[u8], offset: usize) -> Option<u64> {
let end = offset.checked_add(8)?;
let slice = bytes.get(offset..end)?;
let mut buf = [0u8; 8];
buf.copy_from_slice(slice);
Some(u64::from_le_bytes(buf))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_u64_at_basic() {
let bytes = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0xff];
assert_eq!(read_u64_at(&bytes, 0), Some(0x0807060504030201));
assert_eq!(read_u64_at(&bytes, 2), None);
assert_eq!(read_u64_at(&bytes, 100), None);
}
#[test]
fn read_u64_at_handles_offset_overflow() {
let bytes = [0u8; 16];
assert_eq!(read_u64_at(&bytes, usize::MAX), None);
}
#[test]
fn empty_snapshot_serde() {
let snap = SdtAllocatorSnapshot::default();
let json = serde_json::to_string(&snap).unwrap();
assert!(!json.contains("\"entries\""));
assert!(!json.contains("\"truncated\""));
assert!(!json.contains("\"payload_type_reason\""));
assert!(json.contains("\"elem_size\":0"));
assert!(json.contains("\"allocator_name\":\"\""));
assert!(json.contains("\"skipped_subtrees\":0"));
}
#[test]
fn populated_snapshot_roundtrip() {
let snap = SdtAllocatorSnapshot {
allocator_name: "scx_task_allocator".into(),
entries: vec![SdtAllocEntry {
idx: 7,
genn: 1,
user_addr: 0x1000,
payload: RenderedValue::Bytes {
hex: "de ad be ef".into(),
},
}],
truncated: false,
skipped_subtrees: 2,
elem_size: 24,
target_type_id: 42,
payload_type_reason: String::new(),
all_slot_addrs: Vec::new(),
};
let json = serde_json::to_string(&snap).expect("serialize");
let parsed: SdtAllocatorSnapshot = serde_json::from_str(&json).expect("deserialize");
assert_eq!(parsed.entries.len(), 1);
assert_eq!(parsed.entries[0].idx, 7);
assert_eq!(parsed.entries[0].genn, 1);
assert_eq!(parsed.elem_size, 24);
assert_eq!(parsed.target_type_id, 42);
assert_eq!(parsed.skipped_subtrees, 2);
assert_eq!(parsed.allocator_name, "scx_task_allocator");
}
#[test]
fn truncated_flag_serialises() {
let snap = SdtAllocatorSnapshot {
allocator_name: "x".into(),
entries: vec![],
truncated: true,
skipped_subtrees: 0,
elem_size: 24,
target_type_id: 0,
payload_type_reason: String::new(),
all_slot_addrs: Vec::new(),
};
let json = serde_json::to_string(&snap).unwrap();
assert!(json.contains("\"truncated\":true"));
}
#[test]
fn payload_type_reason_serialises_when_nonempty() {
let snap = SdtAllocatorSnapshot {
allocator_name: "x".into(),
entries: vec![],
truncated: false,
skipped_subtrees: 0,
elem_size: 24,
target_type_id: 0,
payload_type_reason: "no candidate of size 16".into(),
all_slot_addrs: Vec::new(),
};
let json = serde_json::to_string(&snap).unwrap();
assert!(json.contains("\"payload_type_reason\":\"no candidate of size 16\""));
}
#[test]
fn constants_match_upstream_layout() {
assert_eq!(SDT_TASK_LEVELS, 3);
assert_eq!(SDT_TASK_ENTS_PER_PAGE_SHIFT, 9);
assert_eq!(SDT_TASK_ENTS_PER_CHUNK, 512);
assert_eq!(SDT_TASK_CHUNK_BITMAP_U64S, 8);
assert_eq!(SIZEOF_SDT_ID, 8);
}
#[test]
fn elem_size_bounds_match_kernel() {
const {
assert!(MIN_ELEM_SIZE >= 16);
}
const {
assert!(MAX_ELEM_SIZE <= 4096);
}
const {
assert!(MIN_ELEM_SIZE.is_multiple_of(8));
}
}
#[test]
fn entry_display_shows_idx_genn_user_addr() {
let entry = SdtAllocEntry {
idx: 7,
genn: 1,
user_addr: 0x1000,
payload: RenderedValue::Uint {
bits: 32,
value: 42,
},
};
let out = format!("{entry}");
assert!(out.contains("idx=7"), "missing idx: {out}");
assert!(out.contains("genn=1"), "missing genn: {out}");
assert!(out.contains("user_addr=0x1000"), "missing user_addr: {out}");
assert!(out.contains("payload=42"), "missing payload: {out}");
}
#[test]
fn snapshot_display_shows_header_and_entries() {
let snap = SdtAllocatorSnapshot {
allocator_name: "scx_task_allocator".into(),
entries: vec![SdtAllocEntry {
idx: 7,
genn: 1,
user_addr: 0x1000,
payload: RenderedValue::Uint {
bits: 32,
value: 42,
},
}],
truncated: false,
skipped_subtrees: 0,
elem_size: 24,
target_type_id: 42,
payload_type_reason: String::new(),
all_slot_addrs: Vec::new(),
};
let out = format!("{snap}");
assert!(
out.contains("sdt_alloc scx_task_allocator"),
"missing header: {out}"
);
assert!(out.contains("elem_size=24"), "missing elem_size: {out}");
assert!(
out.contains("target_type_id=42"),
"missing target_type_id: {out}"
);
assert!(out.contains("1 live"), "missing entry count: {out}");
assert!(out.contains("42"), "missing entry payload: {out}");
}
#[test]
fn snapshot_display_marks_truncated_and_skipped() {
let snap = SdtAllocatorSnapshot {
allocator_name: "x".into(),
entries: vec![],
truncated: true,
skipped_subtrees: 5,
elem_size: 24,
target_type_id: 0,
payload_type_reason: "no candidate of size 16".into(),
all_slot_addrs: Vec::new(),
};
let out = format!("{snap}");
assert!(out.contains("(truncated)"), "missing truncated: {out}");
assert!(
out.contains("(5 subtrees skipped)"),
"missing skipped: {out}"
);
assert!(
out.contains("reason=no candidate of size 16"),
"missing reason: {out}"
);
}
#[test]
fn discover_payload_btf_id_zero_size_short_circuits() {
let path = match crate::monitor::find_test_vmlinux() {
Some(p) => p,
None => {
crate::report::test_skip("no vmlinux for BTF load");
return;
}
};
let btf = match crate::monitor::btf_offsets::load_btf_from_path(&path) {
Ok(b) => b,
Err(_) => {
crate::report::test_skip("BTF load failed");
return;
}
};
let choice = discover_payload_btf_id(&btf, 0, "");
assert_eq!(
choice.target_type_id, 0,
"zero-size must yield target_type_id=0"
);
assert_eq!(
choice.reason, "payload_size == 0",
"zero-size reason must be the early-return marker, got: {}",
choice.reason
);
}
#[test]
fn discover_payload_btf_id_no_candidate_path() {
let path = match crate::monitor::find_test_vmlinux() {
Some(p) => p,
None => {
crate::report::test_skip("no vmlinux for BTF load");
return;
}
};
let btf = match crate::monitor::btf_offsets::load_btf_from_path(&path) {
Ok(b) => b,
Err(_) => {
crate::report::test_skip("BTF load failed");
return;
}
};
let impossible_size = usize::MAX / 2;
let choice = discover_payload_btf_id(&btf, impossible_size, "");
assert_eq!(choice.target_type_id, 0);
let expected = format!("no candidate of size {impossible_size}");
assert_eq!(
choice.reason, expected,
"reason must exactly match documented format: got '{}'",
choice.reason
);
}
#[test]
fn sdt_alloc_offsets_from_vmlinux_btf_returns_err() {
let path = match crate::monitor::find_test_vmlinux() {
Some(p) => p,
None => {
crate::report::test_skip("no vmlinux for BTF load");
return;
}
};
let btf = match crate::monitor::btf_offsets::load_btf_from_path(&path) {
Ok(b) => b,
Err(_) => {
crate::report::test_skip("BTF load failed");
return;
}
};
let err = SdtAllocOffsets::from_btf(&btf)
.expect_err("vmlinux BTF must NOT contain scx_allocator — from_btf must Err");
let msg = format!("{err:#}");
assert!(
msg.contains("scx_allocator"),
"error must name the missing struct so the dump pipeline can log a useful diagnostic: '{msg}'"
);
}
const SDTA_BTF_MAGIC: u16 = 0xEB9F;
const SDTA_BTF_VERSION: u8 = 1;
const SDTA_BTF_HEADER_LEN: u32 = 24;
const SDTA_BTF_KIND_INT: u32 = 1;
const SDTA_BTF_KIND_STRUCT: u32 = 4;
const SDTA_BTF_KIND_FWD: u32 = 7;
#[derive(Clone, Copy)]
struct SdtaSynMember {
name_off: u32,
type_id: u32,
byte_offset: u32,
}
enum SdtaSynType {
Int {
name_off: u32,
size: u32,
encoding: u32,
offset: u32,
bits: u32,
},
Struct {
name_off: u32,
size: u32,
members: Vec<SdtaSynMember>,
},
Fwd { name_off: u32 },
}
fn sdta_push_name(s: &mut Vec<u8>, name: &str) -> u32 {
let off = s.len() as u32;
s.extend_from_slice(name.as_bytes());
s.push(0);
off
}
fn sdta_build_btf(types: &[SdtaSynType], strings: &[u8]) -> Vec<u8> {
let mut type_section: Vec<u8> = Vec::new();
for ty in types {
match ty {
SdtaSynType::Int {
name_off,
size,
encoding,
offset,
bits,
} => {
type_section.extend_from_slice(&name_off.to_le_bytes());
let info = (SDTA_BTF_KIND_INT << 24) & 0x1f00_0000;
type_section.extend_from_slice(&info.to_le_bytes());
type_section.extend_from_slice(&size.to_le_bytes());
let int_data = (*encoding << 24) | ((*offset & 0xff) << 16) | (*bits & 0xff);
type_section.extend_from_slice(&int_data.to_le_bytes());
}
SdtaSynType::Struct {
name_off,
size,
members,
} => {
type_section.extend_from_slice(&name_off.to_le_bytes());
let vlen = members.len() as u32;
let info = ((SDTA_BTF_KIND_STRUCT << 24) & 0x1f00_0000) | (vlen & 0xffff);
type_section.extend_from_slice(&info.to_le_bytes());
type_section.extend_from_slice(&size.to_le_bytes());
for m in members {
type_section.extend_from_slice(&m.name_off.to_le_bytes());
type_section.extend_from_slice(&m.type_id.to_le_bytes());
let bit_off = m.byte_offset * 8;
type_section.extend_from_slice(&bit_off.to_le_bytes());
}
}
SdtaSynType::Fwd { name_off } => {
type_section.extend_from_slice(&name_off.to_le_bytes());
let info = (SDTA_BTF_KIND_FWD << 24) & 0x1f00_0000;
type_section.extend_from_slice(&info.to_le_bytes());
type_section.extend_from_slice(&0u32.to_le_bytes());
}
}
}
let type_len = type_section.len() as u32;
let str_len = strings.len() as u32;
let mut blob: Vec<u8> = Vec::new();
blob.extend_from_slice(&SDTA_BTF_MAGIC.to_le_bytes());
blob.push(SDTA_BTF_VERSION);
blob.push(0); blob.extend_from_slice(&SDTA_BTF_HEADER_LEN.to_le_bytes());
blob.extend_from_slice(&0u32.to_le_bytes()); blob.extend_from_slice(&type_len.to_le_bytes());
blob.extend_from_slice(&type_len.to_le_bytes()); blob.extend_from_slice(&str_len.to_le_bytes());
blob.extend_from_slice(&type_section);
blob.extend_from_slice(strings);
blob
}
fn sdta_strings_for_from_btf() -> (Vec<u8>, SdtaNames) {
let mut strings: Vec<u8> = vec![0];
let n_u64 = sdta_push_name(&mut strings, "u64");
let n_scx_allocator = sdta_push_name(&mut strings, "scx_allocator");
let n_sdt_pool = sdta_push_name(&mut strings, "sdt_pool");
let n_sdt_desc = sdta_push_name(&mut strings, "sdt_desc");
let n_sdt_chunk = sdta_push_name(&mut strings, "sdt_chunk");
let n_sdt_data = sdta_push_name(&mut strings, "sdt_data");
let n_pool = sdta_push_name(&mut strings, "pool");
let n_root = sdta_push_name(&mut strings, "root");
let n_elem_size = sdta_push_name(&mut strings, "elem_size");
let n_allocated = sdta_push_name(&mut strings, "allocated");
let n_nr_free = sdta_push_name(&mut strings, "nr_free");
let n_chunk = sdta_push_name(&mut strings, "chunk");
let n_descs = sdta_push_name(&mut strings, "descs");
(
strings,
SdtaNames {
n_u64,
n_scx_allocator,
n_sdt_pool,
n_sdt_desc,
n_sdt_chunk,
n_sdt_data,
n_pool,
n_root,
n_elem_size,
n_allocated,
n_nr_free,
n_chunk,
n_descs,
},
)
}
struct SdtaNames {
n_u64: u32,
n_scx_allocator: u32,
n_sdt_pool: u32,
n_sdt_desc: u32,
n_sdt_chunk: u32,
n_sdt_data: u32,
n_pool: u32,
n_root: u32,
n_elem_size: u32,
n_allocated: u32,
n_nr_free: u32,
n_chunk: u32,
n_descs: u32,
}
fn sdta_allocator_struct(names: &SdtaNames) -> SdtaSynType {
SdtaSynType::Struct {
name_off: names.n_scx_allocator,
size: 16,
members: vec![
SdtaSynMember {
name_off: names.n_pool,
type_id: 1,
byte_offset: 0,
},
SdtaSynMember {
name_off: names.n_root,
type_id: 1,
byte_offset: 8,
},
],
}
}
#[test]
fn sdt_alloc_offsets_missing_sdt_pool_distinct_error() {
let (strings, names) = sdta_strings_for_from_btf();
let types = vec![
SdtaSynType::Int {
name_off: names.n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
sdta_allocator_struct(&names),
SdtaSynType::Struct {
name_off: names.n_sdt_desc,
size: 24,
members: vec![
SdtaSynMember {
name_off: names.n_allocated,
type_id: 1,
byte_offset: 0,
},
SdtaSynMember {
name_off: names.n_nr_free,
type_id: 1,
byte_offset: 8,
},
SdtaSynMember {
name_off: names.n_chunk,
type_id: 1,
byte_offset: 16,
},
],
},
SdtaSynType::Struct {
name_off: names.n_sdt_chunk,
size: 8,
members: vec![SdtaSynMember {
name_off: names.n_descs,
type_id: 1,
byte_offset: 0,
}],
},
SdtaSynType::Fwd {
name_off: names.n_sdt_data,
},
];
let blob = sdta_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let err =
SdtAllocOffsets::from_btf(&btf).expect_err("missing sdt_pool must surface as Err");
let msg = format!("{err:#}");
assert!(
msg.contains("sdt_pool"),
"error must name the missing struct: '{msg}'"
);
assert!(
msg.contains("unavailable for member offsets"),
"error must carry the sdt_pool-specific context distinguishing this from sdt_chunk's 'not found' wording: '{msg}'"
);
assert!(
!msg.contains("sdt_desc"),
"missing-sdt_pool error must not reference sdt_desc: '{msg}'"
);
assert!(
!msg.contains("sdt_chunk"),
"missing-sdt_pool error must not reference sdt_chunk: '{msg}'"
);
}
#[test]
fn sdt_alloc_offsets_missing_sdt_desc_distinct_error() {
let (strings, names) = sdta_strings_for_from_btf();
let types = vec![
SdtaSynType::Int {
name_off: names.n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
sdta_allocator_struct(&names),
SdtaSynType::Struct {
name_off: names.n_sdt_pool,
size: 32,
members: vec![SdtaSynMember {
name_off: names.n_elem_size,
type_id: 1,
byte_offset: 16,
}],
},
SdtaSynType::Struct {
name_off: names.n_sdt_chunk,
size: 8,
members: vec![SdtaSynMember {
name_off: names.n_descs,
type_id: 1,
byte_offset: 0,
}],
},
SdtaSynType::Fwd {
name_off: names.n_sdt_data,
},
];
let blob = sdta_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let err =
SdtAllocOffsets::from_btf(&btf).expect_err("missing sdt_desc must surface as Err");
let msg = format!("{err:#}");
assert!(
msg.contains("sdt_desc"),
"error must name the missing struct: '{msg}'"
);
assert!(
msg.contains("unavailable for member offsets"),
"error must carry the sdt_desc-specific context distinguishing this from sdt_chunk's 'not found' wording: '{msg}'"
);
assert!(
!msg.contains("sdt_pool"),
"missing-sdt_desc error must not reference sdt_pool: '{msg}'"
);
assert!(
!msg.contains("sdt_chunk"),
"missing-sdt_desc error must not reference sdt_chunk: '{msg}'"
);
}
#[test]
fn sdt_alloc_offsets_missing_sdt_chunk_distinct_error() {
let (strings, names) = sdta_strings_for_from_btf();
let types = vec![
SdtaSynType::Int {
name_off: names.n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
sdta_allocator_struct(&names),
SdtaSynType::Struct {
name_off: names.n_sdt_pool,
size: 32,
members: vec![SdtaSynMember {
name_off: names.n_elem_size,
type_id: 1,
byte_offset: 16,
}],
},
SdtaSynType::Struct {
name_off: names.n_sdt_desc,
size: 24,
members: vec![
SdtaSynMember {
name_off: names.n_allocated,
type_id: 1,
byte_offset: 0,
},
SdtaSynMember {
name_off: names.n_nr_free,
type_id: 1,
byte_offset: 8,
},
SdtaSynMember {
name_off: names.n_chunk,
type_id: 1,
byte_offset: 16,
},
],
},
SdtaSynType::Fwd {
name_off: names.n_sdt_data,
},
];
let blob = sdta_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let err =
SdtAllocOffsets::from_btf(&btf).expect_err("missing sdt_chunk must surface as Err");
let msg = format!("{err:#}");
assert!(
msg.contains("sdt_chunk"),
"error must name the missing struct: '{msg}'"
);
assert!(
msg.contains("not found"),
"sdt_chunk error must carry the find_struct_or_fwd 'not found' wording: '{msg}'"
);
assert!(
!msg.contains("unavailable for member offsets"),
"sdt_chunk uses find_struct_or_fwd, NOT require_full_struct — the 'unavailable for member offsets' phrase is sdt_pool / sdt_desc-specific and must not appear: '{msg}'"
);
assert!(
!msg.contains("sdt_pool"),
"missing-sdt_chunk error must not reference sdt_pool: '{msg}'"
);
assert!(
!msg.contains("sdt_desc"),
"missing-sdt_chunk error must not reference sdt_desc: '{msg}'"
);
}
#[test]
fn sdt_alloc_offsets_sdt_data_fwd_uses_sizeof_sdt_id() {
let (strings, names) = sdta_strings_for_from_btf();
let types = vec![
SdtaSynType::Int {
name_off: names.n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
sdta_allocator_struct(&names),
SdtaSynType::Struct {
name_off: names.n_sdt_pool,
size: 32,
members: vec![SdtaSynMember {
name_off: names.n_elem_size,
type_id: 1,
byte_offset: 16,
}],
},
SdtaSynType::Struct {
name_off: names.n_sdt_desc,
size: 24,
members: vec![
SdtaSynMember {
name_off: names.n_allocated,
type_id: 1,
byte_offset: 0,
},
SdtaSynMember {
name_off: names.n_nr_free,
type_id: 1,
byte_offset: 8,
},
SdtaSynMember {
name_off: names.n_chunk,
type_id: 1,
byte_offset: 16,
},
],
},
SdtaSynType::Struct {
name_off: names.n_sdt_chunk,
size: 8,
members: vec![SdtaSynMember {
name_off: names.n_descs,
type_id: 1,
byte_offset: 0,
}],
},
SdtaSynType::Fwd {
name_off: names.n_sdt_data,
},
];
let blob = sdta_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let offsets = SdtAllocOffsets::from_btf(&btf)
.expect("sdt_data Fwd must NOT cause from_btf to fail — Fwd is the lavd-style path");
assert_eq!(
offsets.data_header_size, SIZEOF_SDT_ID,
"data_header_size for a Fwd sdt_data must fall back to SIZEOF_SDT_ID (=8, the union sdt_id header size that lib/sdt_task_defs.h fixes)"
);
assert_eq!(
offsets.data_header_size, 8,
"literal-8 cross-check: the Fwd fallback must equal exactly 8 bytes (kernel-header-fixed)"
);
}
#[test]
fn discover_payload_btf_id_single_size_match_returns_id() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = sdta_push_name(&mut strings, "u64");
let n_cgrp_ctx = sdta_push_name(&mut strings, "cgrp_ctx");
let n_a = sdta_push_name(&mut strings, "a");
let n_b = sdta_push_name(&mut strings, "b");
let types = vec![
SdtaSynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SdtaSynType::Struct {
name_off: n_cgrp_ctx,
size: 16,
members: vec![
SdtaSynMember {
name_off: n_a,
type_id: 1,
byte_offset: 0,
},
SdtaSynMember {
name_off: n_b,
type_id: 1,
byte_offset: 8,
},
],
},
];
let blob = sdta_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let choice = discover_payload_btf_id(&btf, 16, "");
assert_eq!(
choice.target_type_id, 2,
"single 16-byte struct cgrp_ctx must be picked unambiguously"
);
assert_eq!(
choice.reason, "",
"single-match path must return empty reason; got {:?}",
choice.reason
);
}
#[test]
fn discover_payload_btf_id_per_cgroup_ctx_resolves_via_ctx_suffix() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = sdta_push_name(&mut strings, "u64");
let n_cgrp = sdta_push_name(&mut strings, "scx_cgroup_ctx");
let n_a = sdta_push_name(&mut strings, "a");
let n_b = sdta_push_name(&mut strings, "b");
let types = vec![
SdtaSynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SdtaSynType::Struct {
name_off: n_cgrp,
size: 16,
members: vec![
SdtaSynMember {
name_off: n_a,
type_id: 1,
byte_offset: 0,
},
SdtaSynMember {
name_off: n_b,
type_id: 1,
byte_offset: 8,
},
],
},
];
let blob = sdta_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let choice = discover_payload_btf_id(&btf, 16, "");
assert_eq!(
choice.target_type_id, 2,
"scx_cgroup_ctx (single 16-byte size-match) must resolve via the \
single-match arm; the per-cgroup struct name is the bug surface for #89"
);
assert_eq!(choice.reason, "");
}
#[test]
fn discover_payload_btf_id_task_ctx_exact_wins_over_ctx_suffix() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = sdta_push_name(&mut strings, "u64");
let n_task = sdta_push_name(&mut strings, "task_ctx");
let n_cgrp = sdta_push_name(&mut strings, "cgrp_ctx");
let n_a = sdta_push_name(&mut strings, "a");
let types = vec![
SdtaSynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SdtaSynType::Struct {
name_off: n_task,
size: 16,
members: vec![
SdtaSynMember {
name_off: n_a,
type_id: 1,
byte_offset: 0,
},
SdtaSynMember {
name_off: n_a,
type_id: 1,
byte_offset: 8,
},
],
},
SdtaSynType::Struct {
name_off: n_cgrp,
size: 16,
members: vec![
SdtaSynMember {
name_off: n_a,
type_id: 1,
byte_offset: 0,
},
SdtaSynMember {
name_off: n_a,
type_id: 1,
byte_offset: 8,
},
],
},
];
let blob = sdta_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let choice = discover_payload_btf_id(&btf, 16, "");
assert_eq!(
choice.target_type_id, 2,
"exact `task_ctx` arm (priority 1) must win over `*_ctx` suffix \
arm (priority 4); cgrp_ctx must NOT be picked: {:?}",
choice
);
assert_eq!(choice.reason, "");
}
#[test]
fn discover_payload_btf_id_ambiguous_at_ctx_arm_falls_through() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = sdta_push_name(&mut strings, "u64");
let n_a = sdta_push_name(&mut strings, "a");
let n_cgrp = sdta_push_name(&mut strings, "cgrp_ctx");
let n_task_data = sdta_push_name(&mut strings, "task_data_ctx");
let types = vec![
SdtaSynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SdtaSynType::Struct {
name_off: n_cgrp,
size: 16,
members: vec![
SdtaSynMember {
name_off: n_a,
type_id: 1,
byte_offset: 0,
},
SdtaSynMember {
name_off: n_a,
type_id: 1,
byte_offset: 8,
},
],
},
SdtaSynType::Struct {
name_off: n_task_data,
size: 16,
members: vec![
SdtaSynMember {
name_off: n_a,
type_id: 1,
byte_offset: 0,
},
SdtaSynMember {
name_off: n_a,
type_id: 1,
byte_offset: 8,
},
],
},
];
let blob = sdta_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let choice = discover_payload_btf_id(&btf, 16, "");
assert_eq!(
choice.target_type_id, 0,
"ambiguous `*_ctx` matches must fall through every arm and \
return target_type_id=0; got {:?}",
choice
);
assert_eq!(
choice.reason, "ambiguous: 2 candidates",
"ambiguous-fallback reason format is wire-stable (operator reads \
SdtAllocatorSnapshot::payload_type_reason). Pin the format string \
byte-for-byte; a refactor that changes 'ambiguous' to 'multi' or \
'candidates' to 'matches' would silently break log scrapers."
);
}
#[test]
fn discover_payload_btf_id_per_arm_ambiguity_resolves_at_lower_arm() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = sdta_push_name(&mut strings, "u64");
let n_a = sdta_push_name(&mut strings, "a");
let n_cgrp_arena = sdta_push_name(&mut strings, "cgrp_arena_ctx");
let n_other_arena = sdta_push_name(&mut strings, "other_arena_ctx");
let n_my_task = sdta_push_name(&mut strings, "my_task_ctx");
let types = vec![
SdtaSynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SdtaSynType::Struct {
name_off: n_cgrp_arena,
size: 16,
members: vec![
SdtaSynMember {
name_off: n_a,
type_id: 1,
byte_offset: 0,
},
SdtaSynMember {
name_off: n_a,
type_id: 1,
byte_offset: 8,
},
],
},
SdtaSynType::Struct {
name_off: n_other_arena,
size: 16,
members: vec![
SdtaSynMember {
name_off: n_a,
type_id: 1,
byte_offset: 0,
},
SdtaSynMember {
name_off: n_a,
type_id: 1,
byte_offset: 8,
},
],
},
SdtaSynType::Struct {
name_off: n_my_task,
size: 16,
members: vec![
SdtaSynMember {
name_off: n_a,
type_id: 1,
byte_offset: 0,
},
SdtaSynMember {
name_off: n_a,
type_id: 1,
byte_offset: 8,
},
],
},
];
let blob = sdta_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let choice = discover_payload_btf_id(&btf, 16, "");
assert_eq!(
choice.target_type_id, 4,
"arm 2 ambiguous → continue; arm 3 unique my_task_ctx → return id 4. \
Got {:?}. If this fails, the implementer's fix changed the \
continue-on-arm-ambiguity semantics — verify against the \
docstring at sdt_alloc.rs:565-571 (which currently contradicts \
the code) and update both sides together.",
choice
);
assert_eq!(
choice.reason, "",
"successful pattern-arm resolution must return empty reason"
);
}
}