use super::*;
#[test]
fn cast_chase_arena_pointee_exceeds_cap_wraps_in_truncated() {
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: 5000,
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 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 mut target_bytes = vec![0u8; 4096];
target_bytes[0..8].copy_from_slice(&0x77u64.to_le_bytes());
let mut arena_bytes = std::collections::HashMap::new();
arena_bytes.insert(TARGET_ADDR, 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::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, 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!(
"cap-clamped chase must still surface as Ptr; got {:?}",
members[0].value
);
};
assert_eq!(value, TARGET_ADDR);
assert!(
deref_skipped_reason.is_none(),
"cap-clamped read is a SUCCESS; no skip reason expected; 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 > POINTER_CHASE_CAP must wrap deref in Truncated; got {inner:?}");
};
assert_eq!(needed, 5000, "Truncated.needed must be Q's BTF size");
assert_eq!(
had, 4096,
"Truncated.had must equal POINTER_CHASE_CAP (4096)"
);
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"));
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, 0x77,
"first 8 bytes of cap-clamped read must decode correctly"
);
}
#[test]
fn cast_intercept_u64_at_parent_bytes_boundary_falls_through() {
let (blob, t_id, q_id) = cast_btf_t_and_q();
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let outer_bytes = vec![0xCA, 0xFE, 0xBA, 0xBE];
let mut arena_bytes = std::collections::HashMap::new();
arena_bytes.insert(0xBEBA_FECAu64, vec![0u8; 8]);
let reader = CastStubReader {
hit: Some(CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
arena_window: Some((0, u64::MAX)),
arena_bytes_at: arena_bytes,
..Default::default()
};
let v = render_value_with_mem(&btf, t_id, &outer_bytes, &reader);
let outer_struct = match &v {
RenderedValue::Struct { .. } => &v,
RenderedValue::Truncated { partial, .. } => partial.as_ref(),
other => panic!("expected Struct or Truncated{{Struct}}; got {other:?}"),
};
let RenderedValue::Struct { ref members, .. } = *outer_struct else {
panic!("expected Struct under outer Truncated; got {outer_struct:?}");
};
match &members[0].value {
RenderedValue::Truncated { needed, had, .. } => {
assert_eq!(*needed, 8, "u64 needs 8 bytes");
assert_eq!(*had, 4, "supplied bytes for member is 4");
}
RenderedValue::Ptr { .. } => panic!(
"boundary fall-through must NOT produce Ptr — intercept's \
boundary guard (`field_bytes.get(..8)?` in \
try_cast_intercept) short-circuits; got {:?}",
members[0].value
),
other => panic!("boundary fall-through must produce Truncated; got {other:?}"),
}
}
#[test]
fn cast_intercept_bool_field_not_intercepted() {
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: 4,
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: 8,
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;
let outer_bytes = 1u64.to_le_bytes().to_vec();
let reader = CastStubReader {
hit: Some(CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
arena_window: Some((0, u64::MAX)),
..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:?}");
};
match &members[0].value {
RenderedValue::Bool { value } => {
assert!(*value, "bool value 0x01 must render as true");
}
RenderedValue::Ptr { .. } => panic!(
"_Bool field must NOT be intercepted (int.is_bool() gate at \
encoding gate in try_cast_intercept rejects); got {:?}",
members[0].value
),
other => panic!("_Bool field must render as Bool; got {other:?}"),
}
}
#[test]
fn cast_intercept_signed_8byte_int_not_intercepted() {
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: 1,
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: 8,
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;
let outer_bytes = (-1i64).to_le_bytes().to_vec();
let reader = CastStubReader {
hit: Some(CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
arena_window: Some((0, u64::MAX)),
..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:?}");
};
match &members[0].value {
RenderedValue::Int { bits, value } => {
assert_eq!(
*bits, 64,
"signed int must render at its declared 64-bit width"
);
assert_eq!(*value, -1, "signed -1 must round-trip as Int{{value: -1}}");
}
RenderedValue::Ptr { .. } => panic!(
"signed 8-byte int must NOT be intercepted (int.is_signed() \
encoding gate in try_cast_intercept rejects); got {:?}",
members[0].value
),
other => panic!("signed 8-byte int must render as Int; got {other:?}"),
}
}
#[test]
fn cast_intercept_parent_type_id_none_does_not_crash() {
let (blob, t_id, q_id) = cast_btf_t_and_q();
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let Type::Struct(t_struct) = btf.resolve_type_by_id(t_id).expect("T resolves") else {
panic!("T_id resolves to non-Struct type");
};
let m = t_struct.members.first().expect("T has one member");
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 mut arena_bytes = std::collections::HashMap::new();
arena_bytes.insert(TARGET_ADDR, 0xAAu64.to_le_bytes().to_vec());
let reader = CastStubReader {
hit: Some(CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
arena_window: Some((ARENA_LO, ARENA_HI)),
arena_bytes_at: arena_bytes,
..Default::default()
};
let mut visited: std::collections::HashSet<u64> = std::collections::HashSet::new();
let v = render_member(
&btf,
m,
None,
&outer_bytes,
0,
Some(&reader as &dyn MemReader),
&mut visited,
);
let RenderedValue::Uint { bits, value } = v else {
panic!(
"parent_type_id=None must short-circuit the intercept and \
render as Uint; got {v:?}. A failure here means \
render_member's `let parent = parent_type_id?` guard at \
`parent_type_id.and_then(...)` guard in render_member \
was bypassed."
);
};
assert_eq!(bits, 64);
assert_eq!(value, TARGET_ADDR);
}
#[test]
fn cast_chase_recursive_target_with_inner_cast_field() {
let (strings, n_int, n_t, n_q, n_f, n_x) = cast_strings_for_t_q();
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 mut strings = strings;
let n_r = push(&mut strings, "R");
let n_y = push(&mut strings, "y");
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: 8,
members: vec![CastSynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
CastSynType::Struct {
name_off: n_r,
size: 8,
members: vec![CastSynMember {
name_off: n_y,
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;
let r_id: u32 = 4;
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::Arena,
},
);
cast_map.insert(
(q_id, 0),
CastHit {
alloc_size: None,
target_type_id: r_id,
addr_space: AddrSpace::Arena,
},
);
const ARENA_LO: u64 = 0x10_0000_0000;
const ARENA_HI: u64 = 0x10_0001_0000;
const TARGET_ADDR: u64 = 0x10_0000_1000;
const TARGET_ADDR_2: u64 = 0x10_0000_2000;
let outer_bytes = TARGET_ADDR.to_le_bytes().to_vec();
let q_bytes: Vec<u8> = TARGET_ADDR_2.to_le_bytes().to_vec();
let r_bytes: Vec<u8> = 0xBBu64.to_le_bytes().to_vec();
let mut arena_bytes = std::collections::HashMap::new();
arena_bytes.insert(TARGET_ADDR, q_bytes);
arena_bytes.insert(TARGET_ADDR_2, r_bytes);
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, t_id, &outer_bytes, &reader);
let RenderedValue::Struct { ref members, .. } = v else {
panic!("expected outer Struct render, got {v:?}");
};
let RenderedValue::Ptr {
value: outer_value,
deref: ref outer_deref,
deref_skipped_reason: ref outer_reason,
..
} = members[0].value
else {
panic!(
"outer cast intercept must surface as Ptr; got {:?}",
members[0].value
);
};
assert_eq!(outer_value, TARGET_ADDR);
assert!(
outer_reason.is_none(),
"outer chase must succeed; got {outer_reason:?}"
);
let q_inner = outer_deref
.as_deref()
.expect("outer deref Some (chase succeeded)");
let RenderedValue::Struct {
type_name: ref q_name,
members: ref q_members,
} = *q_inner
else {
panic!("outer deref must be Q struct; got {q_inner:?}");
};
assert_eq!(q_name.as_deref(), Some("Q"));
assert_eq!(q_members.len(), 1);
assert_eq!(q_members[0].name, "x");
let RenderedValue::Ptr {
value: inner_value,
deref: ref inner_deref,
deref_skipped_reason: ref inner_reason,
..
} = q_members[0].value
else {
panic!(
"inner Q.x must surface as Ptr (recursive cast hit); got {:?}. \
A failure here means the renderer didn't pass Q_id as \
parent_type_id when recursing through the deref payload.",
q_members[0].value
);
};
assert_eq!(inner_value, TARGET_ADDR_2);
assert!(
inner_reason.is_none(),
"inner chase must succeed; got {inner_reason:?}"
);
let r_inner = inner_deref
.as_deref()
.expect("inner deref Some (chase succeeded)");
let RenderedValue::Struct {
type_name: ref r_name,
members: ref r_members,
} = *r_inner
else {
panic!("inner deref must be R struct; got {r_inner:?}");
};
assert_eq!(r_name.as_deref(), Some("R"));
assert_eq!(r_members.len(), 1);
assert_eq!(r_members[0].name, "y");
let RenderedValue::Uint { bits, value } = r_members[0].value else {
panic!(
"R.y must terminate as Uint (no recursive cast entry at (R,0)); \
got {:?}",
r_members[0].value
);
};
assert_eq!(bits, 64);
assert_eq!(value, 0xBB);
}
#[test]
fn cast_intercept_modifier_chain_parent_uses_post_peel_id() {
let (strings, n_int, n_t, n_q, n_f, n_x) = cast_strings_for_t_q();
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 mut strings = strings;
let n_typedef = push(&mut strings, "T_alias");
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: 8,
members: vec![CastSynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
CastSynType::Const { type_id: 2 },
CastSynType::Typedef {
name_off: n_typedef,
type_id: 4,
},
];
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;
let typedef_id: u32 = 5;
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::Arena,
},
);
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 mut arena_bytes = std::collections::HashMap::new();
arena_bytes.insert(TARGET_ADDR, 0x55u64.to_le_bytes().to_vec());
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, typedef_id, &outer_bytes, &reader);
let RenderedValue::Struct {
type_name,
ref members,
} = v
else {
panic!("expected Struct render, got {v:?}");
};
assert_eq!(
type_name.as_deref(),
Some("T"),
"renderer must collapse modifier wrappers to underlying T name"
);
let RenderedValue::Ptr {
value,
ref deref,
ref deref_skipped_reason,
..
} = members[0].value
else {
panic!(
"modifier-chain rendering must reach the cast intercept (peel \
must produce T_id as parent_type_id); got {:?}. A failure here \
means peel_modifiers_with_id forwarded the typedef wrapper id \
instead of the post-peel struct id when calling \
render_struct.",
members[0].value
);
};
assert_eq!(value, TARGET_ADDR);
assert!(deref_skipped_reason.is_none());
let inner = deref.as_deref().expect("chase deref Some");
let RenderedValue::Struct {
type_name: ref inner_name,
members: ref inner_members,
} = *inner
else {
panic!("deref payload must be Q struct, got {inner:?}");
};
assert_eq!(inner_name.as_deref(), Some("Q"));
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, 0x55);
}