use super::*;
fn btf_map_update_value_v(slot_off: u32) -> (Vec<u8>, u32, u32, u32, u32) {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_v = push_name(&mut strings, "V");
let n_v_field = push_name(&mut strings, "v_field");
let n_p = push_name(&mut strings, "P");
let n_p_field = push_name(&mut strings, "p_field");
let n_type = push_name(&mut strings, "type");
let n_value = push_name(&mut strings, "value");
let n_map_def = push_name(&mut strings, "anon_map_def");
let n_map_var = push_name(&mut strings, "the_map");
let n_maps = push_name(&mut strings, ".maps");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_v,
size: slot_off + 8,
members: vec![SynMember {
name_off: n_v_field,
type_id: 1,
byte_offset: slot_off,
}],
},
SynType::Struct {
name_off: n_p,
size: 8,
members: vec![SynMember {
name_off: n_p_field,
type_id: 1,
byte_offset: 0,
}],
},
SynType::Ptr { type_id: 2 },
SynType::Struct {
name_off: n_map_def,
size: 16,
members: vec![
SynMember {
name_off: n_type,
type_id: 1,
byte_offset: 0,
},
SynMember {
name_off: n_value,
type_id: 4,
byte_offset: 8,
},
],
},
SynType::Var {
name_off: n_map_var,
type_id: 5,
linkage: 1,
},
SynType::Datasec {
name_off: n_maps,
size: 16,
entries: vec![SynVarSecinfo {
type_id: 6,
offset: 0,
size: 16,
}],
},
];
(build_btf(&types, &strings), 7, 0, 2, 3)
}
fn map_update_with_r3_add_k(spill_off: i16, add_k_imm: i32) -> (Vec<BpfInsn>, Vec<DatasecPointer>) {
let pseudo_call = mk_insn(BPF_CLASS_JMP | BPF_OP_CALL, 0, BPF_PSEUDO_CALL, 0, 0);
let stx_spill = stx(BPF_SIZE_DW, 10, 0, spill_off);
let [ld_lo, ld_hi] = ld_imm64(1, 0);
let mov_r2 = mov_x(2, 10);
let mov_r3 = mov_x(3, 10);
let r3_add_k = mk_insn(BPF_CLASS_ALU64 | BPF_OP_ADD, 3, 0, 0, add_k_imm);
let call_update = mk_insn(
BPF_CLASS_JMP | BPF_OP_CALL,
0,
0,
0,
BPF_FUNC_MAP_UPDATE_ELEM,
);
let insns = vec![
pseudo_call,
stx_spill,
ld_lo,
ld_hi,
mov_r2,
mov_r3,
r3_add_k,
call_update,
exit(),
];
let datasec_pointers = vec![DatasecPointer {
insn_offset: 2,
datasec_type_id: 7,
base_offset: 0,
}];
(insns, datasec_pointers)
}
#[test]
fn step_alu64_add_k_in_range_keeps_frameaddr_and_records() {
let (blob, _datasec_id, _var_off, v_id, _p_id) = btf_map_update_value_v(8);
let btf = Btf::from_bytes(&blob).unwrap();
let (insns, datasec_pointers) = map_update_with_r3_add_k(-8, -16);
let map = analyze_casts(
&insns,
&btf,
&[],
&[],
&datasec_pointers,
&[SubprogReturn {
alloc_size: None,
insn_offset: 0,
}],
);
assert_eq!(
map.get(&(v_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: 0,
addr_space: AddrSpace::Arena,
}),
"in-range r3 += -16 keeps FrameAddr{{-16}}; the update_elem bridge \
must record (V, 8) -> Arena from the spilled arena slot: {map:?}"
);
}
#[test]
fn step_alu64_add_k_overflow_drops_frameaddr_to_unknown() {
let (blob, _datasec_id, _var_off, _v_id, _p_id) = btf_map_update_value_v(8);
let btf = Btf::from_bytes(&blob).unwrap();
let (insns, datasec_pointers) = map_update_with_r3_add_k(-8, 40000);
let map = analyze_casts(
&insns,
&btf,
&[],
&[],
&datasec_pointers,
&[SubprogReturn {
alloc_size: None,
insn_offset: 0,
}],
);
assert!(
map.is_empty(),
"ALU64 ADD K overflowing i16::MAX on a FrameAddr must drop r3 to \
Unknown so the update_elem bridge records nothing: {map:?}"
);
}
#[test]
fn step_alu64_add_k_on_non_frameaddr_drops_dst() {
let (blob, t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let add_k = mk_insn(BPF_CLASS_ALU64 | BPF_OP_ADD, 2, 0, 0, 8);
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 8),
addr_space_cast(2, 2, 1),
add_k,
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(),
"ALU64 ADD K on a non-FrameAddr (LoadedU64Field) register must \
drop dst to Unknown; the deref through it records no access \
despite the prior arena_confirmed: {map:?}"
);
}
fn btf_kfunc_pointing_at(slot_off: u32, func_type_id: u32) -> (Vec<u8>, u32, u32) {
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_func = push_name(&mut strings, "ktstr_kfunc");
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::Func {
name_off: n_func,
type_id: func_type_id,
linkage: 2, },
];
(build_btf(&types, &strings), 4, 5)
}
#[test]
fn kfunc_call_func_pointing_to_non_funcproto_leaves_r0_unknown() {
let slot_off: u32 = 8;
let (blob, p_id, kfunc_id) = btf_kfunc_pointing_at(slot_off, 2);
let btf = Btf::from_bytes(&blob).unwrap();
let kfunc = kfunc_call(kfunc_id);
let insns = vec![kfunc, 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)),
None,
"kfunc whose Func type field points at a non-FuncProto must leave \
R0 Unknown (inner _ => return); the STX records nothing: {map:?}"
);
assert!(
map.is_empty(),
"no other findings expected from the non-FuncProto kfunc: {map:?}"
);
}
fn btf_arena_kfunc_ptr_to_int() -> (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_slot = push_name(&mut strings, "slot");
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_slot,
type_id: 1,
byte_offset: 8,
}],
},
SynType::Ptr { type_id: 1 },
SynType::FuncProto {
return_type_id: 3,
params: vec![],
},
SynType::Func {
name_off: n_func,
type_id: 4,
linkage: 2,
},
];
(build_btf(&types, &strings), 2, 5)
}
#[test]
fn kfunc_allowlist_name_ptr_to_nonvoid_nonstruct_drops_r0() {
let (blob, m_id, kfunc_id) = btf_arena_kfunc_ptr_to_int();
let btf = Btf::from_bytes(&blob).unwrap();
let kfunc = kfunc_call(kfunc_id);
let insns = vec![kfunc, 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)),
None,
"allowlisted kfunc returning Ptr -> Int declines on both arms \
(no struct, non-void pointee); R0 stays Unknown: {map:?}"
);
assert!(
map.is_empty(),
"Ptr -> Int return must record neither an Arena nor a kptr \
finding: {map:?}"
);
}
#[test]
fn handle_stx_unknown_value_does_not_invalidate_prior_arena_stx_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_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_m,
size: 16,
members: vec![SynMember {
name_off: n_slot,
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),
stx(BPF_SIZE_DW, 6, 5, 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,
}),
"a later Unknown-valued STX to the same slot must NOT erase the \
prior arena_stx_findings Arena hit: {map:?}"
);
}