use super::*;
#[test]
fn stx_flow_alloc_return_records_arena_finding() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_m = push_name(&mut strings, "M");
let n_cgx = push_name(&mut strings, "cgx_raw");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_m,
size: 16,
members: vec![SynMember {
name_off: n_cgx,
type_id: 1,
byte_offset: 8,
}],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let m_id = 2;
let pseudo_call = mk_insn(
BPF_CLASS_JMP | BPF_OP_CALL,
0,
BPF_PSEUDO_CALL,
0,
0, );
let insns = vec![pseudo_call, stx(BPF_SIZE_DW, 6, 0, 8), exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 6,
struct_type_id: m_id,
}],
&[],
&[],
&[SubprogReturn {
alloc_size: None,
insn_offset: 0,
}],
);
assert_eq!(
map.get(&(m_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: 0,
addr_space: AddrSpace::Arena,
}),
"STX-flow alloc-return must record Arena finding with \
target_type_id=0: {map:?}"
);
}
#[test]
fn stx_flow_alloc_return_propagates_through_mov() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_m = push_name(&mut strings, "M");
let n_cgx = push_name(&mut strings, "cgx_raw");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_m,
size: 16,
members: vec![SynMember {
name_off: n_cgx,
type_id: 1,
byte_offset: 8,
}],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let m_id = 2;
let pseudo_call = mk_insn(BPF_CLASS_JMP | BPF_OP_CALL, 0, BPF_PSEUDO_CALL, 0, 0);
let insns = vec![pseudo_call, mov_x(7, 0), stx(BPF_SIZE_DW, 6, 7, 8), exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 6,
struct_type_id: m_id,
}],
&[],
&[],
&[SubprogReturn {
alloc_size: None,
insn_offset: 0,
}],
);
assert_eq!(
map.get(&(m_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: 0,
addr_space: AddrSpace::Arena,
}),
"MOV must propagate ArenaU64FromAlloc through r7: {map:?}"
);
}
#[test]
fn stx_flow_alloc_return_round_trips_through_stack() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_m = push_name(&mut strings, "M");
let n_cgx = push_name(&mut strings, "cgx_raw");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_m,
size: 16,
members: vec![SynMember {
name_off: n_cgx,
type_id: 1,
byte_offset: 8,
}],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let m_id = 2;
let pseudo_call = mk_insn(BPF_CLASS_JMP | BPF_OP_CALL, 0, BPF_PSEUDO_CALL, 0, 0);
let insns = vec![
pseudo_call,
stx(BPF_SIZE_DW, 10, 0, -8),
ldx(BPF_SIZE_DW, 7, 10, -8),
stx(BPF_SIZE_DW, 6, 7, 8),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 6,
struct_type_id: m_id,
}],
&[],
&[],
&[SubprogReturn {
alloc_size: None,
insn_offset: 0,
}],
);
assert_eq!(
map.get(&(m_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: 0,
addr_space: AddrSpace::Arena,
}),
"Stack spill/reload must round-trip ArenaU64FromAlloc: {map:?}"
);
}
#[test]
fn stx_flow_alias_tracking_propagates_via_ldx() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_m = push_name(&mut strings, "M");
let n_src = push_name(&mut strings, "src_slot");
let n_dst = push_name(&mut strings, "dst_slot");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_m,
size: 16,
members: vec![
SynMember {
name_off: n_src,
type_id: 1,
byte_offset: 0,
},
SynMember {
name_off: n_dst,
type_id: 1,
byte_offset: 8,
},
],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let m_id = 2;
let pseudo_call = mk_insn(BPF_CLASS_JMP | BPF_OP_CALL, 0, BPF_PSEUDO_CALL, 0, 0);
let insns = vec![
pseudo_call,
stx(BPF_SIZE_DW, 6, 0, 0),
ldx(BPF_SIZE_DW, 7, 6, 0),
stx(BPF_SIZE_DW, 6, 7, 8),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 6,
struct_type_id: m_id,
}],
&[],
&[],
&[SubprogReturn {
alloc_size: None,
insn_offset: 0,
}],
);
assert_eq!(
map.get(&(m_id, 0)),
Some(&CastHit {
alloc_size: None,
target_type_id: 0,
addr_space: AddrSpace::Arena,
}),
"first STX must record (M, 0) -> Arena: {map:?}"
);
assert_eq!(
map.get(&(m_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: 0,
addr_space: AddrSpace::Arena,
}),
"alias-tracked LDX from (M, 0) must propagate to (M, 8) STX: {map:?}"
);
}
#[test]
fn stx_flow_conflict_with_kptr_drops_both() {
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_m = push_name(&mut strings, "M");
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_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_m,
size: 8,
members: vec![SynMember {
name_off: n_slot,
type_id: 1,
byte_offset: 0,
}],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let m_id = 4;
let t_id = 2;
let pseudo_call = mk_insn(BPF_CLASS_JMP | BPF_OP_CALL, 0, BPF_PSEUDO_CALL, 0, 0);
let insns = vec![
pseudo_call,
stx(BPF_SIZE_DW, 6, 0, 0),
stx(BPF_SIZE_DW, 6, 7, 0),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 6,
struct_type_id: m_id,
},
InitialReg {
reg: 7,
struct_type_id: t_id,
},
],
&[],
&[],
&[SubprogReturn {
alloc_size: None,
insn_offset: 0,
}],
);
assert!(
!map.contains_key(&(m_id, 0)),
"arena/kptr conflict must drop the slot from output: {map:?}"
);
}
#[test]
fn stx_flow_resolves_target_via_shape_inference_under_alias_tracking() {
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_q = push_name(&mut strings, "Q");
let n_f = push_name(&mut strings, "f");
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_p,
size: 16,
members: vec![SynMember {
name_off: n_f,
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 p_id = 2;
let q_id = 3;
let pseudo_call = mk_insn(BPF_CLASS_JMP | BPF_OP_CALL, 0, BPF_PSEUDO_CALL, 0, 0);
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 8),
ldx(BPF_SIZE_DW, 3, 2, 0),
ldx(BPF_SIZE_DW, 4, 2, 8),
mov_x(6, 1),
pseudo_call,
stx(BPF_SIZE_DW, 6, 0, 8),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: p_id,
}],
&[],
&[],
&[SubprogReturn {
alloc_size: None,
insn_offset: 4,
}],
);
assert_eq!(
map.get(&(p_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
"STX-flow gates emission past the arena-evidence requirement; shape inference resolves \
target_type_id from the recorded access pattern (Q is the \
only struct of size 16 with u64@0 and u64@8): {map:?}"
);
}
fn btf_with_arena_alloc_kfunc(func_name: &str) -> (Vec<u8>, u32, u32) {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_m = push_name(&mut strings, "M");
let n_cgx = push_name(&mut strings, "cgx_raw");
let n_func = push_name(&mut strings, func_name);
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_m,
size: 16,
members: vec![SynMember {
name_off: n_cgx,
type_id: 1,
byte_offset: 8,
}],
},
SynType::Ptr { type_id: 0 },
SynType::FuncProto {
return_type_id: 3,
params: vec![],
},
SynType::Func {
name_off: n_func,
type_id: 4,
linkage: 2, },
];
let blob = build_btf(&types, &strings);
(blob, 2, 5)
}
#[test]
fn kfunc_arena_alloc_allowlist_records_arena_finding() {
let (blob, m_id, kfunc_id) = btf_with_arena_alloc_kfunc("bpf_arena_alloc_pages");
let btf = Btf::from_bytes(&blob).unwrap();
let kfunc_call = mk_insn(
BPF_CLASS_JMP | BPF_OP_CALL,
0,
BPF_PSEUDO_KFUNC_CALL,
0,
kfunc_id as i32,
);
let insns = vec![kfunc_call, stx(BPF_SIZE_DW, 6, 0, 8), exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 6,
struct_type_id: m_id,
}],
&[],
&[],
&[],
);
assert_eq!(
map.get(&(m_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: 0,
addr_space: AddrSpace::Arena,
}),
"allowlisted kfunc with `Ptr -> Void` return must seed R0 \
as ArenaU64FromAlloc; subsequent STX must record an Arena \
finding: {map:?}"
);
}
#[test]
fn kfunc_arena_alloc_typed_return_falls_through() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_m = push_name(&mut strings, "M");
let n_cgx = push_name(&mut strings, "cgx_raw");
let n_r = push_name(&mut strings, "R");
let n_x = push_name(&mut strings, "x");
let n_func = push_name(&mut strings, "bpf_arena_alloc_pages");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_m,
size: 16,
members: vec![SynMember {
name_off: n_cgx,
type_id: 1,
byte_offset: 8,
}],
},
SynType::Struct {
name_off: n_r,
size: 8,
members: vec![SynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
SynType::Ptr { type_id: 3 },
SynType::FuncProto {
return_type_id: 4,
params: vec![],
},
SynType::Func {
name_off: n_func,
type_id: 5,
linkage: 2,
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let m_id = 2;
let kfunc_id = 6;
let kfunc_call = mk_insn(
BPF_CLASS_JMP | BPF_OP_CALL,
0,
BPF_PSEUDO_KFUNC_CALL,
0,
kfunc_id,
);
let insns = vec![kfunc_call, stx(BPF_SIZE_DW, 6, 0, 8), exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 6,
struct_type_id: m_id,
}],
&[],
&[],
&[],
);
assert_eq!(
map.get(&(m_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: 3, addr_space: AddrSpace::Kernel,
}),
"kfunc whose return is typed (Ptr -> Struct) must take the \
typed-pointer arm, NOT the arena allocator arm; the \
allowlist arm must not produce a false-positive Arena \
finding: {map:?}"
);
}
#[test]
fn kfunc_arena_alloc_non_allowlist_name_drops() {
let (blob, m_id, kfunc_id) = btf_with_arena_alloc_kfunc("ktstr_unlisted_kfunc");
let btf = Btf::from_bytes(&blob).unwrap();
let kfunc_call = mk_insn(
BPF_CLASS_JMP | BPF_OP_CALL,
0,
BPF_PSEUDO_KFUNC_CALL,
0,
kfunc_id as i32,
);
let insns = vec![kfunc_call, stx(BPF_SIZE_DW, 6, 0, 8), exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 6,
struct_type_id: m_id,
}],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"kfunc with `Ptr -> Void` return but non-allowlist name must \
NOT seed an arena finding: {map:?}"
);
}