use super::*;
#[test]
fn kptr_from_function_param_stored_to_u64_field() {
let slot_off: u32 = 16;
let (blob, t_id, p_id, _t_ptr_id) = btf_kptr_base(slot_off);
let btf = Btf::from_bytes(&blob).unwrap();
let insns = vec![mov_x(6, 1), stx(BPF_SIZE_DW, 2, 6, slot_off as i16), exit()];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: t_id,
},
InitialReg {
reg: 2,
struct_type_id: p_id,
},
],
&[],
&[],
&[],
);
assert_eq!(
map.get(&(p_id, slot_off)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"kptr STX must record kernel-space cast: {map:?}"
);
}
#[test]
fn kptr_through_stack_spill() {
let slot_off: u32 = 24;
let (blob, t_id, p_id, _t_ptr_id) = btf_kptr_base(slot_off);
let btf = Btf::from_bytes(&blob).unwrap();
let insns = vec![
stx(BPF_SIZE_DW, 10, 1, -8), ldx(BPF_SIZE_DW, 3, 10, -8), stx(BPF_SIZE_DW, 4, 3, slot_off as i16),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: t_id,
},
InitialReg {
reg: 4,
struct_type_id: p_id,
},
],
&[],
&[],
&[],
);
assert_eq!(
map.get(&(p_id, slot_off)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"stack spill must preserve typed pointer: {map:?}"
);
}
#[test]
fn kptr_from_kfunc_return() {
let slot_off: u32 = 16;
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_t = push_name(&mut strings, "T");
let n_p = push_name(&mut strings, "P");
let n_x = push_name(&mut strings, "x");
let n_slot = push_name(&mut strings, "slot");
let n_kfunc = push_name(&mut strings, "bpf_task_acquire");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_t,
size: 8,
members: vec![SynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
SynType::Ptr { type_id: 2 },
SynType::Struct {
name_off: n_p,
size: slot_off + 8,
members: vec![SynMember {
name_off: n_slot,
type_id: 1,
byte_offset: slot_off,
}],
},
SynType::FuncProto {
return_type_id: 3,
params: vec![],
},
SynType::Func {
name_off: n_kfunc,
type_id: 5,
linkage: 1,
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_id = 2;
let p_id = 4;
let kfunc_id = 6;
let insns = vec![
kfunc_call(kfunc_id),
stx(BPF_SIZE_DW, 6, 0, slot_off as i16),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 6,
struct_type_id: p_id,
}],
&[],
&[],
&[],
);
assert_eq!(
map.get(&(p_id, slot_off)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"kfunc-returned T* stored to P.slot must record: {map:?}"
);
}
#[test]
fn kptr_clobbered_by_call() {
let slot_off: u32 = 16;
let (blob, t_id, p_id, _t_ptr_id) = btf_kptr_base(slot_off);
let btf = Btf::from_bytes(&blob).unwrap();
let insns = vec![call(), stx(BPF_SIZE_DW, 6, 1, slot_off as i16), exit()];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: t_id,
},
InitialReg {
reg: 6,
struct_type_id: p_id,
},
],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"post-call clobbered R1 must not record kptr: {map:?}"
);
}
#[test]
fn mixed_arena_and_kptr_in_one_program() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_t = push_name(&mut strings, "T");
let n_a = push_name(&mut strings, "A");
let n_m = push_name(&mut strings, "M");
let n_x = push_name(&mut strings, "x");
let n_a0 = push_name(&mut strings, "a0");
let n_a1 = push_name(&mut strings, "a1");
let n_arena_ptr = push_name(&mut strings, "arena_ptr");
let n_kptr = push_name(&mut strings, "kptr");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_t,
size: 8,
members: vec![SynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
SynType::Ptr { type_id: 2 },
SynType::Struct {
name_off: n_a,
size: 16,
members: vec![
SynMember {
name_off: n_a0,
type_id: 1,
byte_offset: 0,
},
SynMember {
name_off: n_a1,
type_id: 1,
byte_offset: 8,
},
],
},
SynType::Struct {
name_off: n_m,
size: 24,
members: vec![
SynMember {
name_off: n_arena_ptr,
type_id: 1,
byte_offset: 0,
},
SynMember {
name_off: n_kptr,
type_id: 1,
byte_offset: 16,
},
],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_id = 2;
let a_id = 4;
let m_id = 5;
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 0),
addr_space_cast(2, 2, 1),
ldx(BPF_SIZE_DW, 3, 2, 0),
ldx(BPF_SIZE_DW, 4, 2, 8),
stx(BPF_SIZE_DW, 1, 6, 16),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: m_id,
},
InitialReg {
reg: 6,
struct_type_id: t_id,
},
],
&[],
&[],
&[],
);
assert_eq!(
map.get(&(m_id, 0)),
Some(&CastHit {
alloc_size: None,
target_type_id: a_id,
addr_space: AddrSpace::Arena,
}),
"arena cast missing: {map:?}"
);
assert_eq!(
map.get(&(m_id, 16)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"kernel kptr missing: {map:?}"
);
}
#[test]
fn func_entry_seeding_from_btf() {
let slot1: u32 = 16; let slot3: u32 = 24; let slot4: u32 = 32; let slot5: u32 = 40; let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_t = push_name(&mut strings, "T");
let n_p = push_name(&mut strings, "P");
let n_x = push_name(&mut strings, "x");
let n_s1 = push_name(&mut strings, "s1");
let n_s3 = push_name(&mut strings, "s3");
let n_s4 = push_name(&mut strings, "s4");
let n_s5 = push_name(&mut strings, "s5");
let n_arg_t = push_name(&mut strings, "task");
let n_arg_p = push_name(&mut strings, "parent");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_t,
size: 8,
members: vec![SynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
SynType::Ptr { type_id: 2 }, SynType::Struct {
name_off: n_p,
size: slot5 + 8,
members: vec![
SynMember {
name_off: n_s1,
type_id: 1,
byte_offset: slot1,
},
SynMember {
name_off: n_s3,
type_id: 1,
byte_offset: slot3,
},
SynMember {
name_off: n_s4,
type_id: 1,
byte_offset: slot4,
},
SynMember {
name_off: n_s5,
type_id: 1,
byte_offset: slot5,
},
],
},
SynType::Ptr { type_id: 4 }, SynType::FuncProto {
return_type_id: 0,
params: vec![
SynParam {
name_off: n_arg_t,
type_id: 3,
},
SynParam {
name_off: n_arg_p,
type_id: 5,
},
],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_id = 2;
let p_id = 4;
let proto_id = 6;
let insns = vec![
stx(BPF_SIZE_DW, 2, 1, slot1 as i16),
stx(BPF_SIZE_DW, 2, 3, slot3 as i16),
stx(BPF_SIZE_DW, 2, 4, slot4 as i16),
stx(BPF_SIZE_DW, 2, 5, slot5 as i16),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[],
&[FuncEntry {
insn_offset: 0,
func_proto_id: proto_id,
}],
&[],
&[],
);
assert_eq!(
map.len(),
1,
"FuncEntry must seed only R1 and R2; R3..R5 stay Unknown so \
only the R1->slot1 STX records: {map:?}"
);
assert_eq!(
map.get(&(p_id, slot1)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"FuncEntry param seeding must populate R1 and R2: {map:?}"
);
assert!(
!map.contains_key(&(p_id, slot3)),
"R3 must remain Unknown post-FuncEntry: {map:?}"
);
assert!(
!map.contains_key(&(p_id, slot4)),
"R4 must remain Unknown post-FuncEntry: {map:?}"
);
assert!(
!map.contains_key(&(p_id, slot5)),
"R5 must remain Unknown post-FuncEntry: {map:?}"
);
}
#[test]
fn addr_space_cast_arena_alone_does_not_emit() {
let (blob, t_id, q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let cast = mk_insn(BPF_CLASS_ALU64 | BPF_OP_MOV | BPF_SRC_X, 4, 3, 1, 1);
let insns_cast_only = vec![ldx(BPF_SIZE_DW, 3, 1, 8), cast, exit()];
let map_cast_only = analyze_casts(
&insns_cast_only,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(
map_cast_only.is_empty(),
"arena_confirmed alone (no deref pattern) must not emit: {map_cast_only:?}"
);
let insns_cast_plus_kptr = vec![
ldx(BPF_SIZE_DW, 3, 1, 8),
cast,
stx(BPF_SIZE_DW, 1, 5, 8),
exit(),
];
let map_cast_plus_kptr = analyze_casts(
&insns_cast_plus_kptr,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: t_id,
},
InitialReg {
reg: 5,
struct_type_id: q_id,
},
],
&[],
&[],
&[],
);
assert!(
map_cast_plus_kptr.is_empty(),
"cast + same-slot STX must conflict-drop both observations \
(proves arena_confirmed was populated): {map_cast_plus_kptr:?}"
);
let insns_kptr_only = vec![stx(BPF_SIZE_DW, 1, 5, 8), exit()];
let map_kptr_only = analyze_casts(
&insns_kptr_only,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: t_id,
},
InitialReg {
reg: 5,
struct_type_id: q_id,
},
],
&[],
&[],
&[],
);
assert_eq!(
map_kptr_only.len(),
1,
"STX-only baseline must record exactly one kptr finding: {map_kptr_only:?}"
);
assert_eq!(
map_kptr_only.get(&(t_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Kernel,
}),
"STX-only baseline records (T, 8) -> (Q, Kernel): {map_kptr_only:?}"
);
}
#[test]
fn addr_space_cast_arena_survives_unknown_stx() {
let (blob, t_id, q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let insns = vec![
ldx(BPF_SIZE_DW, 3, 1, 8),
addr_space_cast(4, 3, 1),
ldx(BPF_SIZE_DW, 6, 3, 0),
stx(BPF_SIZE_DW, 1, 9, 8),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert_eq!(
map.len(),
1,
"exactly the (T, 8) chase finding expected: {map:?}"
);
assert_eq!(
map.get(&(t_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
"arena_confirmed must survive the untyped STX so shape \
inference still emits (T,8)->(Q, Arena): {map:?}"
);
}
#[test]
fn addr_space_cast_ambiguous_shape_emits_deferred_resolve() {
let (blob, t_id) = btf_source_and_two_targets(8);
let btf = Btf::from_bytes(&blob).unwrap();
let insns = vec![
ldx(BPF_SIZE_DW, 3, 1, 8),
addr_space_cast(4, 3, 1),
ldx(BPF_SIZE_DW, 6, 3, 0),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert_eq!(
map.get(&(t_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: 0,
addr_space: AddrSpace::Arena,
}),
"ambiguous shape + arena_confirmed must emit a deferred-resolve \
(target_type_id=0) arena CastHit: {map:?}"
);
}
#[test]
fn addr_space_cast_kernel_to_arena_propagates_loaded_field() {
let (blob, t_id, q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let cast = mk_insn(BPF_CLASS_ALU64 | BPF_OP_MOV | BPF_SRC_X, 4, 3, 1, 0x10000);
let insns = vec![
ldx(BPF_SIZE_DW, 3, 1, 8),
cast,
ldx(BPF_SIZE_DW, 5, 4, 0),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert_eq!(
map.len(),
1,
"exactly one finding (via the propagated cast result r4) expected: {map:?}"
);
assert_eq!(
map.get(&(t_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
"kernel->arena cast propagates src LoadedU64Field into dst, so the \
deref through the cast result records (T, 8) -> (Q, Arena): {map:?}"
);
}
#[test]
fn sign_extend_mov_drops_state() {
let (blob, t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let sxt = mk_insn(BPF_CLASS_ALU64 | BPF_OP_MOV | BPF_SRC_X, 4, 3, 8, 0);
let insns = vec![
ldx(BPF_SIZE_DW, 3, 1, 8),
sxt,
ldx(BPF_SIZE_DW, 5, 4, 0),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"sign-extend MOV must drop typed state: {map:?}"
);
}