mod offsets;
mod payload;
mod snapshot;
mod walker;
pub use offsets::SdtAllocOffsets;
pub use payload::discover_payload_btf_id;
pub use snapshot::{SdtAllocEntry, SdtAllocatorSnapshot};
pub use walker::walk_sdt_allocator;
pub(super) const SDT_TASK_LEVELS: usize = 3;
const SDT_TASK_ENTS_PER_PAGE_SHIFT: u32 = 9;
pub(super) const SDT_TASK_ENTS_PER_CHUNK: usize = 1 << SDT_TASK_ENTS_PER_PAGE_SHIFT; pub(super) const SDT_TASK_CHUNK_BITMAP_U64S: usize = SDT_TASK_ENTS_PER_CHUNK / 64;
pub const MAX_SDT_ALLOC_ENTRIES: usize = 4096;
pub(super) const SIZEOF_SDT_ID: usize = 8;
pub(super) const MIN_ELEM_SIZE: u64 = 16;
pub(super) const MAX_ELEM_SIZE: u64 = 4096;
pub(crate) const MAX_BTF_ID_PROBE: u32 = 100_000;
pub(super) 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::*;
use btf_rs::Btf;
use crate::monitor::btf_render::RenderedValue;
#[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"
);
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 continue-on-arm-ambiguity semantics \
changed — 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"
);
}
}