use super::*;
fn btf_bss_with_kptr() -> (Vec<u8>, u32, u32, u32, u32) {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_t = push_name(&mut strings, "task_struct");
let n_x = push_name(&mut strings, "x");
let n_kptr = push_name(&mut strings, "my_kptr");
let n_bss = push_name(&mut strings, ".bss");
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::Var {
name_off: n_kptr,
type_id: 1,
linkage: 1,
},
SynType::Datasec {
name_off: n_bss,
size: 8,
entries: vec![SynVarSecinfo {
type_id: 4,
offset: 0,
size: 8,
}],
},
SynType::FuncProto {
return_type_id: 3,
params: vec![],
},
SynType::Func {
name_off: n_kfunc,
type_id: 6,
linkage: 1,
},
];
let blob = build_btf(&types, &strings);
(blob, 5, 2, 0, 7)
}
#[test]
fn bss_kptr_records_kernel_cast() {
let (blob, datasec_id, t_id, var_off, kfunc_id) = btf_bss_with_kptr();
let btf = Btf::from_bytes(&blob).unwrap();
let [ld_lo, ld_hi] = ld_imm64(1, var_off as i32);
let stx_kptr = stx(BPF_SIZE_DW, 1, 0, 0);
let insns = vec![kfunc_call(kfunc_id), ld_lo, ld_hi, stx_kptr, exit()];
let datasec_pointers = vec![DatasecPointer {
insn_offset: 1,
datasec_type_id: datasec_id,
base_offset: var_off,
}];
let map = analyze_casts(&insns, &btf, &[], &[], &datasec_pointers, &[]);
assert_eq!(
map.get(&(datasec_id, var_off)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"kfunc-returned T* stored into .bss[my_kptr] must record \
(datasec_id, 0) -> (T, Kernel): {map:?}"
);
}
#[test]
fn ld_imm64_without_annotation_no_record() {
let (blob, _datasec_id, _t_id, var_off, kfunc_id) = btf_bss_with_kptr();
let btf = Btf::from_bytes(&blob).unwrap();
let [ld_lo, ld_hi] = ld_imm64(1, var_off as i32);
let stx_kptr = stx(BPF_SIZE_DW, 1, 0, 0);
let insns = vec![kfunc_call(kfunc_id), ld_lo, ld_hi, stx_kptr, exit()];
let map = analyze_casts(&insns, &btf, &[], &[], &[], &[]);
assert!(
map.is_empty(),
"LD_IMM64 without DatasecPointer annotation must not record \
a kptr finding: {map:?}"
);
}
#[test]
fn bss_stx_with_untyped_value_no_record() {
let (blob, datasec_id, _t_id, var_off, _kfunc_id) = btf_bss_with_kptr();
let btf = Btf::from_bytes(&blob).unwrap();
let [ld_lo, ld_hi] = ld_imm64(1, var_off as i32);
let mov_zero = mov_k(0, 0);
let stx_kptr = stx(BPF_SIZE_DW, 1, 0, 0);
let insns = vec![ld_lo, ld_hi, mov_zero, stx_kptr, exit()];
let datasec_pointers = vec![DatasecPointer {
insn_offset: 0,
datasec_type_id: datasec_id,
base_offset: var_off,
}];
let map = analyze_casts(&insns, &btf, &[], &[], &datasec_pointers, &[]);
assert!(
map.is_empty(),
"STX with untyped value register must not record kptr: {map:?}"
);
}
#[test]
fn bss_multi_variable_layout() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_t = push_name(&mut strings, "task_struct");
let n_x = push_name(&mut strings, "x");
let n_a = push_name(&mut strings, "kptr_a");
let n_b = push_name(&mut strings, "kptr_b");
let n_bss = push_name(&mut strings, ".bss");
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::Var {
name_off: n_a,
type_id: 1,
linkage: 1,
},
SynType::Var {
name_off: n_b,
type_id: 1,
linkage: 1,
},
SynType::Datasec {
name_off: n_bss,
size: 24,
entries: vec![
SynVarSecinfo {
type_id: 4,
offset: 0,
size: 8,
},
SynVarSecinfo {
type_id: 5,
offset: 16,
size: 8,
},
],
},
SynType::FuncProto {
return_type_id: 3,
params: vec![],
},
SynType::Func {
name_off: n_kfunc,
type_id: 7,
linkage: 1,
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let datasec_id = 6;
let t_id = 2;
let kfunc_id = 8;
let [ld_a_lo, ld_a_hi] = ld_imm64(1, 0);
let [ld_b_lo, ld_b_hi] = ld_imm64(2, 16);
let insns = vec![
kfunc_call(kfunc_id),
ld_a_lo,
ld_a_hi,
stx(BPF_SIZE_DW, 1, 0, 0),
kfunc_call(kfunc_id),
ld_b_lo,
ld_b_hi,
stx(BPF_SIZE_DW, 2, 0, 0),
exit(),
];
let datasec_pointers = vec![
DatasecPointer {
insn_offset: 1,
datasec_type_id: datasec_id,
base_offset: 0,
},
DatasecPointer {
insn_offset: 5,
datasec_type_id: datasec_id,
base_offset: 16,
},
];
let map = analyze_casts(&insns, &btf, &[], &[], &datasec_pointers, &[]);
assert_eq!(
map.get(&(datasec_id, 0)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"kptr_a at offset 0: {map:?}"
);
assert_eq!(
map.get(&(datasec_id, 16)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"kptr_b at offset 16: {map:?}"
);
}
#[test]
fn struct_member_at_datasec_resolves_variables() {
let (blob, datasec_id, _t_id, _var_off, _kfunc_id) = btf_bss_with_kptr();
let btf = Btf::from_bytes(&blob).unwrap();
let m0 = struct_member_at(&btf, datasec_id, 0).expect("byte 0 must hit my_kptr");
match m0 {
MemberAt::Datasec {
var_byte_offset, ..
} => assert_eq!(var_byte_offset, 0),
MemberAt::Struct { .. } => panic!("Datasec parent must yield Datasec match"),
}
let m4 = struct_member_at(&btf, datasec_id, 4).expect("byte 4 must hit my_kptr range");
match m4 {
MemberAt::Datasec {
var_byte_offset, ..
} => assert_eq!(var_byte_offset, 0),
MemberAt::Struct { .. } => panic!("Datasec parent must yield Datasec match"),
}
assert!(
struct_member_at(&btf, datasec_id, 100).is_none(),
"byte 100 outside section must return None"
);
}
#[test]
fn end_to_end_bss_global_stores_kfunc_pointer() {
let (blob, datasec_id, t_id, var_off, kfunc_id) = btf_bss_with_kptr();
let btf = Btf::from_bytes(&blob).unwrap();
let [ld_lo, ld_hi] = ld_imm64(1, var_off as i32);
let insns = vec![
kfunc_call(kfunc_id),
ld_lo,
ld_hi,
stx(BPF_SIZE_DW, 1, 0, 0),
exit(),
];
let datasec_pointers = vec![DatasecPointer {
insn_offset: 1,
datasec_type_id: datasec_id,
base_offset: var_off,
}];
let map = analyze_casts(&insns, &btf, &[], &[], &datasec_pointers, &[]);
assert_eq!(map.len(), 1, "exactly one finding expected: {map:?}");
assert_eq!(
map.get(&(datasec_id, var_off)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
);
}
#[test]
fn kfunc_call_imm_zero_leaves_r0_unknown() {
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![
kfunc_call(0),
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!(
map.is_empty(),
"kfunc_call imm=0 must leave R0 Unknown: {map:?}"
);
}
#[test]
fn jmp32_gotol_resets_state_at_target() {
let (blob, t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let gotol = mk_insn(BPF_CLASS_JMP32, 0, 0, 0, 1);
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 8),
gotol,
exit(),
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(), "JMP32|JA target must reset state: {map:?}");
}
#[test]
fn out_of_range_jump_targets_dropped() {
let (blob, t_id, q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let jeq_neg = mk_insn(BPF_CLASS_JMP | 0x10, 2, 0, -100, 0);
let jeq_pos = mk_insn(BPF_CLASS_JMP | 0x10, 2, 0, 100, 0);
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 8),
addr_space_cast(2, 2, 1),
jeq_neg,
jeq_pos,
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,
}),
"out-of-range jumps must drop, state survives: {map:?}"
);
}
#[test]
fn all_conditional_jumps_register_targets() {
let (blob, t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let ops: [u8; 11] = [
0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0xa0, 0xb0, 0xc0, 0xd0,
];
for op in ops {
let cond = mk_insn(BPF_CLASS_JMP | op, 2, 0, 1, 0);
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 8),
cond,
exit(),
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(),
"JMP op 0x{op:02x} target must reset state: {map:?}"
);
}
}
#[test]
fn func_entry_multiple_at_same_pc_last_wins() {
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_t = push_name(&mut strings, "arg_t");
let n_arg_p = push_name(&mut strings, "arg_p");
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::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,
},
],
},
SynType::FuncProto {
return_type_id: 0,
params: vec![
SynParam {
name_off: n_arg_p,
type_id: 5,
},
SynParam {
name_off: n_arg_t,
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_a = 6;
let proto_b = 7;
let insns = vec![stx(BPF_SIZE_DW, 1, 2, slot_off as i16), exit()];
let map = analyze_casts(
&insns,
&btf,
&[],
&[
FuncEntry {
insn_offset: 0,
func_proto_id: proto_a,
},
FuncEntry {
insn_offset: 0,
func_proto_id: proto_b,
},
],
&[],
&[],
);
assert_eq!(
map.get(&(p_id, slot_off)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"later FuncEntry at same PC must win: {map:?}"
);
}
#[test]
fn func_entry_past_insns_len_no_op() {
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),
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,
}],
&[FuncEntry {
insn_offset: 999,
func_proto_id: 1,
}],
&[],
&[],
);
assert_eq!(
map.get(&(t_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
"FuncEntry past insns.len() must not affect run: {map:?}"
);
}
#[test]
fn func_entry_pc0_no_params_clears_initial_regs() {
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::Struct {
name_off: n_q,
size: 8,
members: vec![SynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
SynType::FuncProto {
return_type_id: 0,
params: vec![],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_id = 2;
let proto_id = 4;
let insns = vec![ldx(BPF_SIZE_DW, 2, 1, 8), ldx(BPF_SIZE_DW, 3, 2, 0), exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[FuncEntry {
insn_offset: 0,
func_proto_id: proto_id,
}],
&[],
&[],
);
assert!(
map.is_empty(),
"FuncEntry with empty params must clear all regs: {map:?}"
);
}
#[test]
fn func_entry_proto_id_zero_clears_regs_no_seed() {
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), ldx(BPF_SIZE_DW, 3, 2, 0), exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[FuncEntry {
insn_offset: 0,
func_proto_id: 0,
}],
&[],
&[],
);
assert!(
map.is_empty(),
"FuncEntry with proto_id=0 must clear regs and not seed: {map:?}"
);
}
#[test]
fn func_entry_pc_gt_0_reseeds_mid_stream() {
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_t = push_name(&mut strings, "arg_t");
let n_arg_p = push_name(&mut strings, "arg_p");
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::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![exit(), stx(BPF_SIZE_DW, 2, 1, slot_off as i16), exit()];
let map = analyze_casts(
&insns,
&btf,
&[],
&[FuncEntry {
insn_offset: 1,
func_proto_id: proto_id,
}],
&[],
&[],
);
assert_eq!(
map.get(&(p_id, slot_off)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"FuncEntry at PC>0 must reseed: {map:?}"
);
}
#[test]
fn ld_imm64_second_slot_with_non_zero_content_skipped() {
let (blob, t_id, q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let ld_imm64_lo = mk_insn(BPF_CLASS_LD | BPF_SIZE_DW | BPF_MODE_IMM, 6, 0, 0, 42);
let fake_mov = mk_insn(BPF_CLASS_ALU64 | BPF_OP_MOV | BPF_SRC_X, 4, 3, 0, 0);
let insns = vec![
ld_imm64_lo,
fake_mov,
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,
}),
"non-zero LD_IMM64 second slot must skip: {map:?}"
);
}
#[test]
fn initial_reg_duplicate_seeds_last_wins() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_s1 = push_name(&mut strings, "S1");
let n_s2 = push_name(&mut strings, "S2");
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_s1,
size: 16,
members: vec![SynMember {
name_off: n_f,
type_id: 1,
byte_offset: 8,
}],
},
SynType::Struct {
name_off: n_s2,
size: 24,
members: vec![SynMember {
name_off: n_f,
type_id: 1,
byte_offset: 16,
}],
},
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 s1_id = 2;
let s2_id = 3;
let q_id = 4;
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 16),
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: s1_id,
},
InitialReg {
reg: 1,
struct_type_id: s2_id,
},
],
&[],
&[],
&[],
);
assert_eq!(
map.get(&(s2_id, 16)),
Some(&CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
"duplicate InitialReg seed must use last value: {map:?}"
);
assert!(
!map.contains_key(&(s1_id, 16)),
"first InitialReg seed must NOT take effect: {map:?}"
);
}
#[test]
fn initial_reg_struct_type_id_zero_dropped() {
let (blob, _t, _q) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let insns = vec![ldx(BPF_SIZE_DW, 2, 1, 8), ldx(BPF_SIZE_DW, 3, 2, 0), exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: 0,
}],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"InitialReg with struct_type_id=0 must be dropped: {map:?}"
);
}