use super::*;
#[test]
fn arena_chase_pointee_fwd_resolves_to_complete_struct_sibling() {
let mut strings: Vec<u8> = vec![0];
let push = |s: &mut Vec<u8>, name: &str| -> u32 {
let off = s.len() as u32;
s.extend_from_slice(name.as_bytes());
s.push(0);
off
};
let n_int = push(&mut strings, "u64");
let n_t = push(&mut strings, "scx_task_map_val");
let n_data = push(&mut strings, "data");
let n_task_ctx = push(&mut strings, "task_ctx");
let n_field = push(&mut strings, "field");
let types = vec![
CastSynType::Int {
name_off: n_int,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
CastSynType::Fwd {
name_off: n_task_ctx,
is_union: false,
},
CastSynType::Ptr { type_id: 2 },
CastSynType::Struct {
name_off: n_t,
size: 8,
members: vec![CastSynMember {
name_off: n_data,
type_id: 3,
byte_offset: 0,
}],
},
CastSynType::Struct {
name_off: n_task_ctx,
size: 8,
members: vec![CastSynMember {
name_off: n_field,
type_id: 1,
byte_offset: 0,
}],
},
];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let outer_id: u32 = 4;
const ARENA_LO: u64 = 0x10_0000_0000;
const ARENA_HI: u64 = 0x10_0001_0000;
const TARGET_ADDR: u64 = 0x10_0000_1000;
let outer_bytes = TARGET_ADDR.to_le_bytes().to_vec();
let inner_bytes = 0x77u64.to_le_bytes().to_vec();
let mut arena_bytes = std::collections::HashMap::new();
arena_bytes.insert(TARGET_ADDR, inner_bytes);
let reader = CastStubReader {
arena_window: Some((ARENA_LO, ARENA_HI)),
arena_bytes_at: arena_bytes,
..Default::default()
};
let v = render_value_with_mem(&btf, outer_id, &outer_bytes, &reader);
let RenderedValue::Struct { ref members, .. } = v else {
panic!("expected Struct render, got {v:?}");
};
let RenderedValue::Ptr {
value,
ref deref,
ref deref_skipped_reason,
..
} = members[0].value
else {
panic!(
"data field must render as Ptr (BTF Type::Ptr arm); got {:?}",
members[0].value
);
};
assert_eq!(value, TARGET_ADDR);
assert!(
deref_skipped_reason.is_none(),
"Fwd-resolved chase must succeed; got skip reason: {deref_skipped_reason:?}"
);
let payload = deref
.as_deref()
.expect("Fwd-resolved chase must produce a deref payload");
let RenderedValue::Struct {
ref type_name,
members: ref inner_members,
} = *payload
else {
panic!("deref must be Struct render; got {payload:?}");
};
assert_eq!(
type_name.as_deref(),
Some("task_ctx"),
"deref must carry the resolved Struct name"
);
assert_eq!(inner_members.len(), 1);
assert_eq!(inner_members[0].name, "field");
let RenderedValue::Uint { bits, value } = inner_members[0].value else {
panic!(
"inner field must decode as Uint; got {:?}",
inner_members[0].value
);
};
assert_eq!(bits, 64);
assert_eq!(value, 0x77);
}
#[test]
fn cast_chase_arena_target_fwd_resolves_to_complete_struct_sibling() {
let mut strings: Vec<u8> = vec![0];
let push = |s: &mut Vec<u8>, name: &str| -> u32 {
let off = s.len() as u32;
s.extend_from_slice(name.as_bytes());
s.push(0);
off
};
let n_u64 = push(&mut strings, "u64");
let n_t = push(&mut strings, "scx_task_map_val");
let n_data = push(&mut strings, "data");
let n_target = push(&mut strings, "task_ctx");
let n_field = push(&mut strings, "field");
let types = vec![
CastSynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
CastSynType::Struct {
name_off: n_t,
size: 8,
members: vec![CastSynMember {
name_off: n_data,
type_id: 1,
byte_offset: 0,
}],
},
CastSynType::Fwd {
name_off: n_target,
is_union: false,
},
CastSynType::Struct {
name_off: n_target,
size: 8,
members: vec![CastSynMember {
name_off: n_field,
type_id: 1,
byte_offset: 0,
}],
},
];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let parent_id: u32 = 2;
let fwd_target_id: u32 = 3;
const ARENA_LO: u64 = 0x10_0000_0000;
const ARENA_HI: u64 = 0x10_0001_0000;
const TARGET_ADDR: u64 = 0x10_0000_1000;
let outer_bytes = TARGET_ADDR.to_le_bytes().to_vec();
let inner_bytes = 0xABCDu64.to_le_bytes().to_vec();
let mut arena_bytes = std::collections::HashMap::new();
arena_bytes.insert(TARGET_ADDR, inner_bytes);
let mut cast_map = crate::monitor::cast_analysis::CastMap::new();
cast_map.insert(
(parent_id, 0),
CastHit {
alloc_size: None,
target_type_id: fwd_target_id,
addr_space: AddrSpace::Arena,
},
);
let reader = CastStubReader {
cast_map: Some(cast_map),
arena_window: Some((ARENA_LO, ARENA_HI)),
arena_bytes_at: arena_bytes,
..Default::default()
};
let v = render_value_with_mem(&btf, parent_id, &outer_bytes, &reader);
let RenderedValue::Struct { ref members, .. } = v else {
panic!("expected Struct render, got {v:?}");
};
let RenderedValue::Ptr {
value,
ref deref,
ref deref_skipped_reason,
ref cast_annotation,
} = members[0].value
else {
panic!(
"data field must render as cast-recovered Ptr; got {:?}",
members[0].value
);
};
assert_eq!(value, TARGET_ADDR);
assert_eq!(
cast_annotation.as_deref(),
Some("cast→arena"),
"cast intercept must annotate the arena chase"
);
assert!(
deref_skipped_reason.is_none(),
"Fwd-resolved cast chase must not skip; got: {deref_skipped_reason:?}"
);
let payload = deref
.as_deref()
.expect("Fwd-resolved cast chase must produce deref payload");
let RenderedValue::Struct {
ref type_name,
members: ref inner_members,
} = *payload
else {
panic!("deref must be Struct render; got {payload:?}");
};
assert_eq!(
type_name.as_deref(),
Some("task_ctx"),
"deref must carry the resolved Struct name"
);
assert_eq!(inner_members[0].name, "field");
let RenderedValue::Uint { value, .. } = inner_members[0].value else {
panic!(
"inner field must decode as Uint; got {:?}",
inner_members[0].value
);
};
assert_eq!(value, 0xABCD);
}
#[test]
fn cast_chase_kernel_target_fwd_resolves_to_complete_struct_sibling() {
let mut strings: Vec<u8> = vec![0];
let push = |s: &mut Vec<u8>, name: &str| -> u32 {
let off = s.len() as u32;
s.extend_from_slice(name.as_bytes());
s.push(0);
off
};
let n_u64 = push(&mut strings, "u64");
let n_t = push(&mut strings, "parent");
let n_data = push(&mut strings, "data");
let n_target = push(&mut strings, "kernel_target");
let n_field = push(&mut strings, "field");
let types = vec![
CastSynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
CastSynType::Struct {
name_off: n_t,
size: 8,
members: vec![CastSynMember {
name_off: n_data,
type_id: 1,
byte_offset: 0,
}],
},
CastSynType::Fwd {
name_off: n_target,
is_union: false,
},
CastSynType::Struct {
name_off: n_target,
size: 8,
members: vec![CastSynMember {
name_off: n_field,
type_id: 1,
byte_offset: 0,
}],
},
];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let parent_id: u32 = 2;
let fwd_target_id: u32 = 3;
const KVA: u64 = 0xffff_8000_0000_3000;
let outer_bytes = KVA.to_le_bytes().to_vec();
let inner_bytes = 0xDEADBEEFu64.to_le_bytes().to_vec();
let mut kva_bytes = std::collections::HashMap::new();
kva_bytes.insert(KVA, inner_bytes);
let mut cast_map = crate::monitor::cast_analysis::CastMap::new();
cast_map.insert(
(parent_id, 0),
CastHit {
alloc_size: None,
target_type_id: fwd_target_id,
addr_space: AddrSpace::Kernel,
},
);
let reader = CastStubReader {
cast_map: Some(cast_map),
kva_bytes_at: kva_bytes,
..Default::default()
};
let v = render_value_with_mem(&btf, parent_id, &outer_bytes, &reader);
let RenderedValue::Struct { ref members, .. } = v else {
panic!("expected Struct render, got {v:?}");
};
let RenderedValue::Ptr {
value,
ref deref,
ref deref_skipped_reason,
ref cast_annotation,
} = members[0].value
else {
panic!(
"data field must render as cast-recovered Ptr; got {:?}",
members[0].value
);
};
assert_eq!(value, KVA);
assert_eq!(cast_annotation.as_deref(), Some("cast→kernel"));
assert!(
deref_skipped_reason.is_none(),
"Fwd-resolved kernel cast chase must not skip; got: {deref_skipped_reason:?}"
);
let payload = deref
.as_deref()
.expect("Fwd-resolved kernel cast chase must produce deref payload");
let RenderedValue::Struct {
ref type_name,
members: ref inner_members,
} = *payload
else {
panic!("deref must be Struct render; got {payload:?}");
};
assert_eq!(
type_name.as_deref(),
Some("kernel_target"),
"deref must carry the resolved Struct name"
);
let RenderedValue::Uint { value, .. } = inner_members[0].value else {
panic!(
"inner field must decode as Uint; got {:?}",
inner_members[0].value
);
};
assert_eq!(value, 0xDEADBEEF);
}
#[test]
fn fwd_shortcut_rejects_aggregate_kind_mismatch() {
let mut strings: Vec<u8> = vec![0];
let push = |s: &mut Vec<u8>, name: &str| -> u32 {
let off = s.len() as u32;
s.extend_from_slice(name.as_bytes());
s.push(0);
off
};
let n_u64 = push(&mut strings, "u64");
let n_wrap = push(&mut strings, "wrap");
let n_data = push(&mut strings, "data");
let n_foo = push(&mut strings, "foo");
let n_x = push(&mut strings, "x");
let types = vec![
CastSynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
CastSynType::Fwd {
name_off: n_foo,
is_union: false,
},
CastSynType::Ptr { type_id: 2 },
CastSynType::Struct {
name_off: n_wrap,
size: 8,
members: vec![CastSynMember {
name_off: n_data,
type_id: 3,
byte_offset: 0,
}],
},
CastSynType::Struct {
name_off: n_foo,
size: 8,
members: vec![CastSynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
];
let mut blob = cast_build_btf(&types, &strings);
let id5_info_off: usize = 24 + 16 + 12 + 12 + 24 + 4;
let info = u32::from_le_bytes(blob[id5_info_off..id5_info_off + 4].try_into().unwrap());
let new_info = (info & !(0x1f << 24)) | (5u32 << 24);
blob[id5_info_off..id5_info_off + 4].copy_from_slice(&new_info.to_le_bytes());
let btf = Btf::from_bytes(&blob).expect("synthetic BTF with union parses");
let wrap_id: u32 = 4;
let fwd_id: u32 = 2;
const ARENA_LO: u64 = 0x10_0000_0000;
const ARENA_HI: u64 = 0x10_0001_0000;
const TARGET_ADDR: u64 = 0x10_0000_1000;
let outer_bytes = TARGET_ADDR.to_le_bytes().to_vec();
let reader = CastStubReader {
arena_window: Some((ARENA_LO, ARENA_HI)),
..Default::default()
};
let v = render_value_with_mem(&btf, wrap_id, &outer_bytes, &reader);
let RenderedValue::Struct { ref members, .. } = v else {
panic!("expected Struct render, got {v:?}");
};
let RenderedValue::Ptr {
ref deref,
ref deref_skipped_reason,
..
} = members[0].value
else {
panic!("data field must render as Ptr; got {:?}", members[0].value);
};
assert!(
deref.is_none(),
"aggregate-kind mismatch must NOT resolve the Fwd; chase must skip"
);
let reason = deref_skipped_reason
.as_deref()
.expect("aggregate-kind mismatch must populate skip reason (Fwd unresolved)");
assert!(
reason.contains("forward declaration"),
"skip reason must report the Fwd; got: {reason}"
);
assert!(
reason.contains("foo"),
"skip reason must include the Fwd's name; got: {reason}"
);
assert!(
reason.contains(&fwd_id.to_string()),
"skip reason must include the Fwd's id; got: {reason}"
);
}
#[test]
fn peel_modifiers_resolving_fwd_no_sibling_returns_fwd() {
let mut strings: Vec<u8> = vec![0];
let push = |s: &mut Vec<u8>, name: &str| -> u32 {
let off = s.len() as u32;
s.extend_from_slice(name.as_bytes());
s.push(0);
off
};
let n_int = push(&mut strings, "u64");
let n_fwd = push(&mut strings, "lonely_fwd");
let types = vec![
CastSynType::Int {
name_off: n_int,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
CastSynType::Fwd {
name_off: n_fwd,
is_union: false,
},
];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let (peeled, peeled_id) =
peel_modifiers_resolving_fwd(&btf, 2).expect("Fwd resolves through helper");
assert!(
matches!(peeled, Type::Fwd(_)),
"no-sibling lookup must return the original Fwd; got {peeled:?}"
);
assert_eq!(peeled_id, 2);
}
#[test]
fn peel_modifiers_resolving_fwd_anonymous_fwd_returns_fwd() {
let strings: Vec<u8> = vec![0];
let types = vec![
CastSynType::Int {
name_off: 0,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
CastSynType::Fwd {
name_off: 0,
is_union: false,
},
];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let (peeled, peeled_id) =
peel_modifiers_resolving_fwd(&btf, 2).expect("anonymous Fwd resolves through helper");
assert!(
matches!(peeled, Type::Fwd(_)),
"anonymous Fwd must remain Fwd; got {peeled:?}"
);
assert_eq!(peeled_id, 2);
}
#[test]
fn peel_modifiers_resolving_fwd_through_typedef_chain() {
let mut strings: Vec<u8> = vec![0];
let push = |s: &mut Vec<u8>, name: &str| -> u32 {
let off = s.len() as u32;
s.extend_from_slice(name.as_bytes());
s.push(0);
off
};
let n_u64 = push(&mut strings, "u64");
let n_alias = push(&mut strings, "alias");
let n_target = push(&mut strings, "target");
let n_field = push(&mut strings, "field");
let types = vec![
CastSynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
CastSynType::Typedef {
name_off: n_alias,
type_id: 3,
},
CastSynType::Fwd {
name_off: n_target,
is_union: false,
},
CastSynType::Struct {
name_off: n_target,
size: 8,
members: vec![CastSynMember {
name_off: n_field,
type_id: 1,
byte_offset: 0,
}],
},
];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let (peeled, peeled_id) =
peel_modifiers_resolving_fwd(&btf, 2).expect("Typedef→Fwd→Struct chain resolves");
assert!(
matches!(peeled, Type::Struct(_)),
"Typedef→Fwd chain must land on the complete Struct; got {peeled:?}"
);
assert_eq!(
peeled_id, 4,
"resolved id must be the complete Struct's id, not the Typedef or Fwd id"
);
}
#[test]
fn cast_chase_kernel_read_kva_failure() {
let (blob, t_id, q_id) = cast_btf_t_and_q();
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
const KVA: u64 = 0xffff_8000_0000_2000;
let outer_bytes = KVA.to_le_bytes().to_vec();
let reader = CastStubReader {
hit: Some(CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Kernel,
}),
..Default::default()
};
let v = render_value_with_mem(&btf, t_id, &outer_bytes, &reader);
let RenderedValue::Struct { ref members, .. } = v else {
panic!("expected Struct render, got {v:?}");
};
let RenderedValue::Ptr {
value,
ref deref,
ref deref_skipped_reason,
..
} = members[0].value
else {
panic!(
"read_kva failure must still surface as Ptr; got {:?}",
members[0].value
);
};
assert_eq!(value, KVA);
assert!(
deref.is_none(),
"read_kva failure must not produce a deref payload"
);
let reason = deref_skipped_reason
.as_deref()
.expect("read_kva failure must populate skip reason");
assert!(
reason.contains("read_kva failed"),
"skip reason must say 'read_kva failed'; got: {reason}"
);
assert!(
reason.contains(&format!("0x{KVA:x}")),
"skip reason must include the failing address in hex; got: {reason}"
);
assert!(
reason.contains("needed"),
"skip reason must include the requested byte count; got: {reason}"
);
}
#[test]
fn cast_chase_kernel_page_edge_truncation() {
let (strings, n_int, n_t, n_q, n_f, n_x) = cast_strings_for_t_q();
let types = vec![
CastSynType::Int {
name_off: n_int,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
CastSynType::Struct {
name_off: n_t,
size: 8,
members: vec![CastSynMember {
name_off: n_f,
type_id: 1,
byte_offset: 0,
}],
},
CastSynType::Struct {
name_off: n_q,
size: 100,
members: vec![CastSynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let t_id: u32 = 2;
let q_id: u32 = 3;
const KVA: u64 = 0xffff_8000_0000_0ff0;
let outer_bytes = KVA.to_le_bytes().to_vec();
let mut target_bytes = vec![0u8; 16];
target_bytes[0..8].copy_from_slice(&0xCAFEu64.to_le_bytes());
let mut kva_bytes = std::collections::HashMap::new();
kva_bytes.insert(KVA, target_bytes);
let mut cast_map: crate::monitor::cast_analysis::CastMap = std::collections::BTreeMap::new();
cast_map.insert(
(t_id, 0),
CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Kernel,
},
);
let reader = CastStubReader {
cast_map: Some(cast_map),
kva_bytes_at: kva_bytes,
..Default::default()
};
let v = render_value_with_mem(&btf, t_id, &outer_bytes, &reader);
let RenderedValue::Struct { ref members, .. } = v else {
panic!("expected Struct render, got {v:?}");
};
let RenderedValue::Ptr {
value,
ref deref,
ref deref_skipped_reason,
..
} = members[0].value
else {
panic!(
"page-edge clipped chase must still surface as Ptr; got {:?}",
members[0].value
);
};
assert_eq!(value, KVA);
assert!(
deref_skipped_reason.is_none(),
"successful (clipped) read must carry no skip reason; got {deref_skipped_reason:?}"
);
let inner = deref
.as_deref()
.expect("read succeeded → deref must be Some");
let RenderedValue::Truncated {
needed,
had,
ref partial,
} = *inner
else {
panic!("btf_size > read_size must wrap deref payload in Truncated; got {inner:?}");
};
assert_eq!(needed, 100, "Truncated.needed must be the BTF size");
assert_eq!(
had, 16,
"Truncated.had must be the page-edge-clipped read size"
);
let inner_struct = match &**partial {
RenderedValue::Struct { .. } => partial.as_ref(),
RenderedValue::Truncated {
partial: deeper, ..
} => deeper.as_ref(),
other => panic!(
"partial render must reach a Q struct (possibly via inner Truncated); got {other:?}"
),
};
let RenderedValue::Struct {
type_name: ref inner_name,
members: ref inner_members,
} = *inner_struct
else {
panic!("expected inner Struct render, got {inner_struct:?}");
};
assert_eq!(
inner_name.as_deref(),
Some("Q"),
"inner struct must carry Q's name"
);
assert_eq!(inner_members.len(), 1);
assert_eq!(inner_members[0].name, "x");
let RenderedValue::Uint { bits, value } = inner_members[0].value else {
panic!("Q.x must render as Uint, got {:?}", inner_members[0].value);
};
assert_eq!(bits, 64);
assert_eq!(value, 0xCAFE, "first 8 bytes of clipped read must decode");
}
#[test]
fn cast_chase_kernel_successful_chase_top_byte_non_ff() {
let (blob, t_id, q_id) = cast_btf_t_and_q();
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
const KVA: u64 = 0xffff_8000_dead_b000;
let outer_bytes = KVA.to_le_bytes().to_vec();
let inner_bytes: Vec<u8> = 0x42u64.to_le_bytes().to_vec();
let mut kva_bytes = std::collections::HashMap::new();
kva_bytes.insert(KVA, inner_bytes);
let mut cast_map: crate::monitor::cast_analysis::CastMap = std::collections::BTreeMap::new();
cast_map.insert(
(t_id, 0),
CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Kernel,
},
);
let reader = CastStubReader {
cast_map: Some(cast_map),
kva_bytes_at: kva_bytes,
..Default::default()
};
let v = render_value_with_mem(&btf, t_id, &outer_bytes, &reader);
let RenderedValue::Struct { ref members, .. } = v else {
panic!("expected Struct render, got {v:?}");
};
let RenderedValue::Ptr {
value,
ref deref,
ref deref_skipped_reason,
..
} = members[0].value
else {
panic!(
"kernel chase must surface as Ptr; got {:?}",
members[0].value
);
};
assert_eq!(value, KVA);
assert!(
deref_skipped_reason.is_none(),
"successful chase carries no skip reason; got {deref_skipped_reason:?}"
);
let inner = deref
.as_deref()
.expect("plausibility-allowed chase → deref must be Some");
let RenderedValue::Struct {
type_name: ref inner_name,
members: ref inner_members,
} = *inner
else {
panic!("deref payload must be the rendered Q struct; got {inner:?}");
};
assert_eq!(
inner_name.as_deref(),
Some("Q"),
"deref payload must carry Q's name"
);
assert_eq!(inner_members.len(), 1);
assert_eq!(inner_members[0].name, "x");
let RenderedValue::Uint { bits, value } = inner_members[0].value else {
panic!("Q.x must render as Uint, got {:?}", inner_members[0].value);
};
assert_eq!(bits, 64);
assert_eq!(value, 0x42, "Q.x must reflect the bytes read_kva returned");
}