use super::*;
#[test]
fn arena_and_kptr_same_field_drops_both() {
let slot_off: u32 = 8;
let (blob, t_id, p_id, _t_ptr_id) = btf_kptr_base(slot_off);
let btf = Btf::from_bytes(&blob).unwrap();
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, slot_off as i16),
ldx(BPF_SIZE_DW, 3, 2, 0),
stx(BPF_SIZE_DW, 1, 6, slot_off as i16),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: p_id,
},
InitialReg {
reg: 6,
struct_type_id: t_id,
},
],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"arena+kptr conflict on same slot must drop both: {map:?}"
);
}
#[test]
fn kptr_conflict_two_targets_drops() {
let slot_off: u32 = 16;
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_t1 = push_name(&mut strings, "T1");
let n_t2 = push_name(&mut strings, "T2");
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 types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_t1,
size: 8,
members: vec![SynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
SynType::Struct {
name_off: n_t2,
size: 8,
members: vec![SynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
SynType::Struct {
name_off: n_p,
size: slot_off + 8,
members: vec![SynMember {
name_off: n_slot,
type_id: 1,
byte_offset: slot_off,
}],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t1_id = 2;
let t2_id = 3;
let p_id = 4;
let seeds = [
InitialReg {
reg: 1,
struct_type_id: t1_id,
},
InitialReg {
reg: 2,
struct_type_id: t2_id,
},
InitialReg {
reg: 6,
struct_type_id: p_id,
},
];
let insns_single = vec![stx(BPF_SIZE_DW, 6, 1, slot_off as i16), exit()];
let map_single = analyze_casts(&insns_single, &btf, &seeds, &[], &[], &[]);
assert_eq!(
map_single.len(),
1,
"(a) single STX must record exactly one finding: {map_single:?}"
);
assert_eq!(
map_single.get(&(p_id, slot_off)),
Some(&CastHit {
alloc_size: None,
target_type_id: t1_id,
addr_space: AddrSpace::Kernel,
}),
"(a) baseline records (P, slot) -> (T1, Kernel): {map_single:?}"
);
let insns_conflict = vec![
stx(BPF_SIZE_DW, 6, 1, slot_off as i16),
stx(BPF_SIZE_DW, 6, 2, slot_off as i16),
exit(),
];
let map_conflict = analyze_casts(&insns_conflict, &btf, &seeds, &[], &[], &[]);
assert!(
map_conflict.is_empty(),
"(b) two distinct kptr targets on same slot must collapse to \
Conflicting and drop: {map_conflict:?}"
);
let insns_three = vec![
stx(BPF_SIZE_DW, 6, 1, slot_off as i16),
stx(BPF_SIZE_DW, 6, 2, slot_off as i16),
stx(BPF_SIZE_DW, 6, 1, slot_off as i16),
exit(),
];
let map_three = analyze_casts(&insns_three, &btf, &seeds, &[], &[], &[]);
assert!(
map_three.is_empty(),
"(c) Conflicting state must be sticky across same-target \
restore — third STX of T1 must not resurrect: {map_three:?}"
);
}
#[test]
fn oob_dst_reg_does_not_panic() {
let (blob, t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let bad = BpfInsn::new(BPF_CLASS_LDX | BPF_SIZE_DW | BPF_MODE_MEM, 11, 1, 8, 0);
let insns = vec![bad, exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(map.is_empty(), "OOB dst must not panic, map empty: {map:?}");
}
#[test]
fn oob_src_reg_does_not_panic() {
let (blob, t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let bad = BpfInsn::new(BPF_CLASS_LDX | BPF_SIZE_DW | BPF_MODE_MEM, 2, 15, 8, 0);
let insns = vec![bad, exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(map.is_empty(), "OOB src must not panic, map empty: {map:?}");
}
#[test]
fn self_store_rejected() {
let slot_off: u32 = 8;
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_p = push_name(&mut strings, "P");
let n_slot = push_name(&mut strings, "slot");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_p,
size: slot_off + 8,
members: vec![SynMember {
name_off: n_slot,
type_id: 1,
byte_offset: slot_off,
}],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let p_id = 2;
let insns = vec![stx(BPF_SIZE_DW, 1, 1, slot_off as i16), exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: p_id,
}],
&[],
&[],
&[],
);
assert!(map.is_empty(), "self-store must be rejected: {map:?}");
}
#[test]
fn variadic_param_breaks_seeding() {
let slot_off1: u32 = 16;
let slot_off2: u32 = 24;
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_slot1 = push_name(&mut strings, "slot1");
let n_slot2 = push_name(&mut strings, "slot2");
let n_arg_t = push_name(&mut strings, "task");
let n_arg_p = push_name(&mut strings, "parent");
let n_arg_after = push_name(&mut strings, "after_variadic");
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_off2 + 8,
members: vec![
SynMember {
name_off: n_slot1,
type_id: 1,
byte_offset: slot_off1,
},
SynMember {
name_off: n_slot2,
type_id: 1,
byte_offset: slot_off2,
},
],
},
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,
},
SynParam {
name_off: 0,
type_id: 0,
},
SynParam {
name_off: n_arg_after,
type_id: 3,
},
],
},
];
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, slot_off1 as i16),
stx(BPF_SIZE_DW, 2, 3, slot_off2 as i16),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[],
&[FuncEntry {
insn_offset: 0,
func_proto_id: proto_id,
}],
&[],
&[],
);
assert_eq!(
map.get(&(p_id, slot_off1)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"non-variadic params must seed R1 and R2: {map:?}"
);
assert!(
!map.contains_key(&(p_id, slot_off2)),
"variadic sentinel must terminate scan, R3 must stay Unknown: {map:?}"
);
}
#[test]
fn func_entry_clears_all_regs() {
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_arg = push_name(&mut strings, "arg");
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: 0,
params: vec![SynParam {
name_off: n_arg,
type_id: 3,
}],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_id = 2;
let p_id = 4;
let proto_id = 5;
let insns = vec![stx(BPF_SIZE_DW, 6, 3, slot_off as i16), exit()];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 3,
struct_type_id: t_id,
},
InitialReg {
reg: 6,
struct_type_id: p_id,
},
],
&[FuncEntry {
insn_offset: 0,
func_proto_id: proto_id,
}],
&[],
&[],
);
assert!(
map.is_empty(),
"FuncEntry pre-clear must drop R3 typed state: {map:?}"
);
}
#[test]
fn probe_mem_load_treated_as_unknown() {
let (blob, t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
const BPF_MODE_PROBE_MEM: u8 = 0x20;
let probe_load = mk_insn(BPF_CLASS_LDX | BPF_SIZE_DW | BPF_MODE_PROBE_MEM, 2, 1, 8, 0);
let insns = vec![probe_load, ldx(BPF_SIZE_DW, 3, 2, 0), exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"BPF_PROBE_MEM load must mark dst Unknown: {map:?}"
);
}
#[test]
fn finalize_arena_confirmed_conflicts_with_kptr() {
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_q = push_name(&mut strings, "Q");
let n_f = push_name(&mut strings, "f");
let n_x = push_name(&mut strings, "x");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_t,
size: 16,
members: vec![SynMember {
name_off: n_f,
type_id: 1,
byte_offset: 8,
}],
},
SynType::Ptr { type_id: 2 },
SynType::Struct {
name_off: n_q,
size: 8,
members: vec![SynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_id = 2;
let q_id = 4;
let cast = mk_insn(BPF_CLASS_ALU64 | BPF_OP_MOV | BPF_SRC_X, 4, 2, 1, 1);
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 8),
cast,
stx(BPF_SIZE_DW, 1, 3, 8),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: t_id,
},
InitialReg {
reg: 3,
struct_type_id: q_id,
},
],
&[],
&[],
&[],
);
assert!(
!map.contains_key(&(t_id, 8)),
"arena_confirmed + kptr conflict on (T, 8) must drop both: {map:?}"
);
assert!(map.is_empty(), "no other entries expected: {map:?}");
}
#[test]
fn finalize_empty_access_set_does_not_emit() {
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, 2, 1, 8), exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(
!map.contains_key(&(t_id, 8)),
"empty access set must not emit: {map:?}"
);
assert!(map.is_empty(), "no other entries expected: {map:?}");
}
#[test]
fn finalize_source_in_candidates_with_others_emits_other() {
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_q = push_name(&mut strings, "Q");
let n_a = push_name(&mut strings, "a");
let n_b = push_name(&mut strings, "b");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_t,
size: 16,
members: vec![
SynMember {
name_off: n_a,
type_id: 1,
byte_offset: 0,
},
SynMember {
name_off: n_b,
type_id: 1,
byte_offset: 8,
},
],
},
SynType::Struct {
name_off: n_q,
size: 16,
members: vec![
SynMember {
name_off: n_a,
type_id: 1,
byte_offset: 0,
},
SynMember {
name_off: n_b,
type_id: 1,
byte_offset: 8,
},
],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_id = 2;
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),
exit(),
];
let q_id = 3;
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert_eq!(
map.get(&(t_id, 0)),
Some(&CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
"source removed, sole non-source candidate Q emitted: {map:?}"
);
}
#[test]
fn finalize_only_source_candidate_drops() {
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_f = push_name(&mut strings, "f");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_t,
size: 16,
members: vec![SynMember {
name_off: n_f,
type_id: 1,
byte_offset: 8,
}],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_id = 2;
let insns = vec![ldx(BPF_SIZE_DW, 2, 1, 8), ldx(BPF_SIZE_DW, 3, 2, 8), exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"candidate set containing only the source must drop: {map:?}"
);
}
#[test]
fn finalize_max_seen_type_id_slack_finds_distant_candidate() {
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_q = push_name(&mut strings, "Q");
let n_f = push_name(&mut strings, "f");
let n_x = push_name(&mut strings, "x");
let mut types: Vec<SynType> = Vec::new();
types.push(SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
});
types.push(SynType::Struct {
name_off: n_t,
size: 16,
members: vec![SynMember {
name_off: n_f,
type_id: 1,
byte_offset: 8,
}],
});
for _ in 0..200 {
types.push(SynType::Ptr { type_id: 1 });
}
types.push(SynType::Struct {
name_off: n_q,
size: 8,
members: vec![SynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
});
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_id = 2;
let q_id = 203;
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 8),
addr_space_cast(2, 2, 1),
ldx(BPF_SIZE_DW, 3, 2, 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: q_id,
addr_space: AddrSpace::Arena,
}),
"slack must carry search well past max_seen, capped within \
MAX_BTF_ID_PROBE: {map:?}"
);
}