use super::*;
#[test]
fn render_bitfield_unsigned_multibit_at_nonzero_offset_decodes_exact() {
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_b = push(&mut strings, "B");
let n_f = push(&mut strings, "f");
let types = vec![
CastSynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
CastSynType::BitfieldStruct {
name_off: n_b,
size: 8,
members: vec![CastSynBitMember {
name_off: n_f,
type_id: 1,
bit_offset: 4,
bitfield_size: 12,
}],
},
];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let bytes = 0xABC0u64.to_le_bytes();
let v = render_value(&btf, 2, &bytes);
assert_eq!(
sole_member_value(&v),
&RenderedValue::Uint {
bits: 12,
value: 0xABC,
}
);
}
#[test]
fn render_bitfield_signed_int_base_decodes_negative() {
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, "int");
let n_b = push(&mut strings, "B");
let n_f = push(&mut strings, "f");
let types = vec![
CastSynType::Int {
name_off: n_int,
size: 4,
encoding: 1,
offset: 0,
bits: 32,
},
CastSynType::BitfieldStruct {
name_off: n_b,
size: 4,
members: vec![CastSynBitMember {
name_off: n_f,
type_id: 1,
bit_offset: 0,
bitfield_size: 4,
}],
},
];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let v = render_value(&btf, 2, &[0x0F, 0x00, 0x00, 0x00]);
assert_eq!(
sole_member_value(&v),
&RenderedValue::Int { bits: 4, value: -1 }
);
}
#[test]
fn render_bitfield_signed_enum_base_decodes_negative_int() {
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_e = push(&mut strings, "E");
let n_neg = push(&mut strings, "NEG");
let n_b = push(&mut strings, "B");
let n_f = push(&mut strings, "f");
let types = vec![
CastSynType::Enum {
name_off: n_e,
size: 4,
signed: true,
members: vec![(n_neg, 0xFFFF_FFFFu32)],
},
CastSynType::BitfieldStruct {
name_off: n_b,
size: 4,
members: vec![CastSynBitMember {
name_off: n_f,
type_id: 1,
bit_offset: 0,
bitfield_size: 8,
}],
},
];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let v = render_value(&btf, 2, &[0xFF, 0x00, 0x00, 0x00]);
assert_eq!(
sole_member_value(&v),
&RenderedValue::Int { bits: 8, value: -1 }
);
}
#[test]
fn render_sub_byte_signed_enum_resolves_negative_variant_name() {
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_e = push(&mut strings, "E");
let n_neg = push(&mut strings, "NEG");
let types = vec![CastSynType::Enum {
name_off: n_e,
size: 1,
signed: true,
members: vec![(n_neg, 0xFFFF_FFFFu32)],
}];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let v = render_value(&btf, 1, &[0xFF]);
assert_eq!(v, enum_v(8, -1, Some("NEG"), true));
}
#[test]
fn render_sub_byte_signed_enum64_resolves_negative_variant_name() {
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_e = push(&mut strings, "E64");
let n_neg = push(&mut strings, "NEG");
let types = vec![CastSynType::Enum64 {
name_off: n_e,
size: 4,
signed: true,
members: vec![(n_neg, u64::MAX)],
}];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let v = render_value(&btf, 1, &[0xFF, 0xFF, 0xFF, 0xFF]);
assert_eq!(v, enum_v(32, -1, Some("NEG"), true));
}
#[test]
fn render_full_width_signed_enum64_resolves_negative_variant_name() {
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_e = push(&mut strings, "E64");
let n_neg = push(&mut strings, "NEG");
let types = vec![CastSynType::Enum64 {
name_off: n_e,
size: 8,
signed: true,
members: vec![(n_neg, u64::MAX)],
}];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let v = render_value(&btf, 1, &[0xFF; 8]);
assert_eq!(v, enum_v(64, -1, Some("NEG"), true));
}
#[test]
fn render_bitfield_signed_enum64_base_decodes_negative_int() {
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_e = push(&mut strings, "E64");
let n_neg = push(&mut strings, "NEG");
let n_b = push(&mut strings, "B");
let n_f = push(&mut strings, "f");
let types = vec![
CastSynType::Enum64 {
name_off: n_e,
size: 8,
signed: true,
members: vec![(n_neg, u64::MAX)],
},
CastSynType::BitfieldStruct {
name_off: n_b,
size: 8,
members: vec![CastSynBitMember {
name_off: n_f,
type_id: 1,
bit_offset: 0,
bitfield_size: 8,
}],
},
];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let v = render_value(&btf, 2, &[0xFF, 0, 0, 0, 0, 0, 0, 0]);
assert_eq!(
sole_member_value(&v),
&RenderedValue::Int { bits: 8, value: -1 }
);
}
#[test]
fn render_bitfield_width_over_64_is_unsupported() {
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_b = push(&mut strings, "B");
let n_f = push(&mut strings, "f");
let types = vec![
CastSynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
CastSynType::BitfieldStruct {
name_off: n_b,
size: 16,
members: vec![CastSynBitMember {
name_off: n_f,
type_id: 1,
bit_offset: 0,
bitfield_size: 65,
}],
},
];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let v = render_value(&btf, 2, &[0u8; 16]);
match sole_member_value(&v) {
RenderedValue::Unsupported { reason } => {
assert!(
reason.contains("out of range"),
"unexpected reason: {reason}"
);
}
other => panic!("expected Unsupported, got {other:?}"),
}
}
#[test]
fn render_bitfield_width_zero_is_unsupported_direct() {
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 types = vec![CastSynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
}];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let v = super::render_bitfield(&btf, 1, &[0u8; 8], 0, 0);
match v {
RenderedValue::Unsupported { reason } => {
assert!(
reason.contains("out of range"),
"unexpected reason: {reason}"
);
}
other => panic!("expected Unsupported, got {other:?}"),
}
}
#[test]
fn render_bitfield_straddling_end_of_bytes_is_truncated() {
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_b = push(&mut strings, "B");
let n_f = push(&mut strings, "f");
let types = vec![
CastSynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
CastSynType::BitfieldStruct {
name_off: n_b,
size: 8,
members: vec![CastSynBitMember {
name_off: n_f,
type_id: 1,
bit_offset: 0,
bitfield_size: 64,
}],
},
];
let blob = cast_build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let v = render_value(&btf, 2, &[0xAB]);
match sole_member_value(&v) {
RenderedValue::Truncated { needed, had, .. } => {
assert_eq!((*needed, *had), (8, 1));
}
other => panic!("expected Truncated, got {other:?}"),
}
}
fn cast_btf_t_with_u32() -> (Vec<u8>, u32, u32) {
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_u32 = push(&mut strings, "u32");
let n_u64 = push(&mut strings, "u64");
let n_t = push(&mut strings, "T");
let n_q = push(&mut strings, "Q");
let n_f = push(&mut strings, "f");
let n_x = push(&mut strings, "x");
let types = vec![
CastSynType::Int {
name_off: n_u32,
size: 4,
encoding: 0,
offset: 0,
bits: 32,
},
CastSynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
CastSynType::Struct {
name_off: n_t,
size: 4,
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: 2,
byte_offset: 0,
}],
},
];
(cast_build_btf(&types, &strings), 3, 4)
}
#[test]
fn cast_intercept_u64_renders_as_ptr_with_chase() {
let (blob, t_id, q_id) = cast_btf_t_and_q();
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
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 = 0x42u64.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(
(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 {
type_name,
ref members,
} = v
else {
panic!("expected Struct render, got {v:?}");
};
assert_eq!(type_name.as_deref(), Some("T"));
assert_eq!(members.len(), 1);
assert_eq!(members[0].name, "f");
let RenderedValue::Ptr {
value,
ref deref,
ref deref_skipped_reason,
..
} = members[0].value
else {
panic!(
"intercept must produce Ptr (not Uint); got {:?}",
members[0].value
);
};
assert_eq!(
value, TARGET_ADDR,
"Ptr value must be the loaded u64 (arena address)"
);
assert!(
deref_skipped_reason.is_none(),
"successful chase: no skip reason; got {deref_skipped_reason:?}"
);
let inner = deref
.as_deref()
.expect("chase succeeded → 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"),
"inner deref Struct must carry the target'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);
}
#[test]
fn cast_intercept_null_value_no_crash() {
let (blob, t_id, q_id) = cast_btf_t_and_q();
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let outer_bytes = 0u64.to_le_bytes().to_vec();
let reader = CastStubReader {
hit: Some(CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
..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!(
"null intercept must still surface as Ptr (matches Type::Ptr arm); got {:?}",
members[0].value
);
};
assert_eq!(value, 0);
assert!(deref.is_none(), "null Ptr has no deref");
assert!(
deref_skipped_reason.is_none(),
"null Ptr must NOT carry a skip reason: a chase was never attempted"
);
}
#[test]
fn cast_intercept_non_u64_field_not_intercepted() {
let (blob, t_id, q_id) = cast_btf_t_with_u32();
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let outer_bytes = 0xCAFEu32.to_le_bytes().to_vec();
let reader = CastStubReader {
hit: Some(CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
..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::Uint { bits, value } = members[0].value else {
panic!(
"u32 field with size==4 must render as Uint, NOT Ptr; got {:?}",
members[0].value
);
};
assert_eq!(bits, 32, "u32 surfaces as 32-bit Uint");
assert_eq!(value, 0xCAFE);
}
#[test]
fn cast_intercept_no_hit_renders_uint() {
let (blob, t_id, _q_id) = cast_btf_t_and_q();
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
let outer_bytes = 0x12345678u64.to_le_bytes().to_vec();
let reader = CastStubReader::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::Uint { bits, value } = members[0].value else {
panic!(
"no cast_lookup hit must yield plain Uint, got {:?}",
members[0].value
);
};
assert_eq!(bits, 64);
assert_eq!(value, 0x12345678);
}
#[test]
fn cast_chase_cycle_detection() {
let (blob, t_id, _q_id) = cast_btf_t_and_q();
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
const ARENA_LO: u64 = 0x10_0000_0000;
const ARENA_HI: u64 = 0x10_0001_0000;
const SELF_ADDR: u64 = 0x10_0000_1000;
let outer_bytes = SELF_ADDR.to_le_bytes().to_vec();
let self_bytes = SELF_ADDR.to_le_bytes().to_vec();
let mut arena_bytes = std::collections::HashMap::new();
arena_bytes.insert(SELF_ADDR, self_bytes);
let reader = CastStubReader {
hit: Some(CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Arena,
}),
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 chase must surface as Ptr, got {:?}",
members[0].value
);
};
assert_eq!(outer_value, SELF_ADDR);
assert!(
outer_reason.is_none(),
"outer chase succeeded; no skip reason expected, got {outer_reason:?}"
);
let inner = outer_deref.as_deref().expect("outer chase deref Some");
let RenderedValue::Struct {
members: ref inner_members,
..
} = *inner
else {
panic!("inner deref must be a Struct, got {inner:?}");
};
let RenderedValue::Ptr {
value: inner_value,
deref: ref inner_deref,
deref_skipped_reason: ref inner_reason,
..
} = inner_members[0].value
else {
panic!(
"inner u64 cast intercept must surface as Ptr, got {:?}",
inner_members[0].value
);
};
assert_eq!(inner_value, SELF_ADDR);
assert!(
inner_deref.is_none(),
"cycle detection must NOT recurse into the deref payload"
);
let reason = inner_reason
.as_deref()
.expect("cycle detection must populate deref_skipped_reason");
assert!(
reason.contains("cycle"),
"skip reason must mention cycle, got: {reason}"
);
}
#[test]
fn cast_chase_kernel_plausibility_rejects_freed_slab() {
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_beef;
let outer_bytes = KVA.to_le_bytes().to_vec();
let stale_bytes: Vec<u8> = 0xff00_0000_0000_0001u64.to_le_bytes().to_vec();
let mut kva_bytes = std::collections::HashMap::new();
kva_bytes.insert(KVA, stale_bytes);
let reader = CastStubReader {
hit: Some(CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Kernel,
}),
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 cast intercept must surface as Ptr, got {:?}",
members[0].value
);
};
assert_eq!(value, KVA);
assert!(
deref.is_none(),
"plausibility-rejected chase must NOT carry a deref payload"
);
let reason = deref_skipped_reason
.as_deref()
.expect("plausibility rejection must populate skip reason");
assert!(
reason.contains("plausibility"),
"skip reason must mention plausibility, got: {reason}"
);
}
#[test]
fn cast_intercept_kernel_hint_arena_value_dispatches_to_arena_reader() {
let (blob, t_id, q_id) = cast_btf_t_and_q();
let btf = Btf::from_bytes(&blob).expect("synthetic BTF parses");
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 = 0x42u64.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(
(t_id, 0),
CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Kernel,
},
);
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,
ref deref,
ref deref_skipped_reason,
ref cast_annotation,
} = members[0].value
else {
panic!(
"Kernel-hint + arena-value cast must surface as Ptr, got {:?}",
members[0].value
);
};
assert_eq!(value, TARGET_ADDR, "Ptr value is the loaded u64");
assert!(
deref_skipped_reason.is_none(),
"arena dispatch chose the arena reader → no skip reason; got {deref_skipped_reason:?}",
);
let inner = deref
.as_deref()
.expect("arena reader returned Some bytes → deref payload populated");
let RenderedValue::Struct {
type_name: ref inner_name,
members: ref inner_members,
} = *inner
else {
panic!(
"deref payload must be the rendered Q struct (proves arena chase, \
not kernel chase, did the read), got {inner:?}",
);
};
assert_eq!(
inner_name.as_deref(),
Some("Q"),
"inner deref carries Q's name → render_value_inner(target_type_id) succeeded",
);
assert_eq!(inner_members.len(), 1, "Q has one u64 member");
let RenderedValue::Uint { bits, value } = inner_members[0].value else {
panic!(
"Q.x must render as Uint (was rendered through arena reader bytes), got {:?}",
inner_members[0].value
);
};
assert_eq!(bits, 64);
assert_eq!(value, 0x42, "arena reader returned 0x42 at TARGET_ADDR");
assert_eq!(
cast_annotation.as_deref(),
Some("cast→arena"),
"runtime dispatch chose arena → annotation is cast→arena, NOT cast→kernel; \
got {cast_annotation:?}",
);
}