use super::*;
fn helper_call(helper_id: i32) -> BpfInsn {
mk_insn(BPF_CLASS_JMP | BPF_OP_CALL, 0, 0, 0, helper_id)
}
enum MapValueShape {
Struct,
U64,
Typedef,
#[allow(dead_code)] Void,
}
#[test]
fn helper_map_lookup_elem_typed_value_seeds_r0() {
let (blob, datasec_id, var_off, value_sid, parent_id) =
btf_with_maps_and_task_ctx(MapValueShape::Struct);
let btf = Btf::from_bytes(&blob).unwrap();
let [ld_lo, ld_hi] = ld_imm64(1, var_off as i32);
let mov_key = mov_k(2, 0); let call_lookup = helper_call(BPF_FUNC_MAP_LOOKUP_ELEM);
let stx_kptr = stx(BPF_SIZE_DW, 6, 0, 8);
let insns = vec![ld_lo, ld_hi, mov_key, call_lookup, 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,
&[InitialReg {
reg: 6,
struct_type_id: parent_id,
}],
&[],
&datasec_pointers,
&[],
);
assert_eq!(
map.get(&(parent_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: value_sid,
addr_space: AddrSpace::Kernel,
}),
"lookup-derived R0 stored into task_ctx.cgx_raw must record \
(task_ctx, 8) -> (cbw_cgrp_entry, Kernel): {map:?}"
);
}
#[test]
fn helper_map_lookup_elem_value_type_unresolvable_keeps_r0_unknown() {
let (blob, datasec_id, var_off, _value_sid, parent_id) =
btf_with_maps_and_task_ctx(MapValueShape::U64);
let btf = Btf::from_bytes(&blob).unwrap();
let [ld_lo, ld_hi] = ld_imm64(1, var_off as i32);
let mov_key = mov_k(2, 0);
let call_lookup = helper_call(BPF_FUNC_MAP_LOOKUP_ELEM);
let stx_kptr = stx(BPF_SIZE_DW, 6, 0, 8);
let insns = vec![ld_lo, ld_hi, mov_key, call_lookup, 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,
&[InitialReg {
reg: 6,
struct_type_id: parent_id,
}],
&[],
&datasec_pointers,
&[],
);
assert!(
map.is_empty(),
"stat-counter map (`__type(value, u64)`) must keep R0 Unknown \
so the STX records nothing: {map:?}"
);
}
#[test]
fn helper_map_lookup_elem_value_type_void_keeps_r0_unknown() {
let (blob, datasec_id, var_off, _value_sid, parent_id) =
btf_with_maps_and_task_ctx(MapValueShape::Void);
let btf = Btf::from_bytes(&blob).unwrap();
let [ld_lo, ld_hi] = ld_imm64(1, var_off as i32);
let mov_key = mov_k(2, 0);
let call_lookup = helper_call(BPF_FUNC_MAP_LOOKUP_ELEM);
let stx_kptr = stx(BPF_SIZE_DW, 6, 0, 8);
let insns = vec![ld_lo, ld_hi, mov_key, call_lookup, 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,
&[InitialReg {
reg: 6,
struct_type_id: parent_id,
}],
&[],
&datasec_pointers,
&[],
);
assert!(
map.is_empty(),
"void-value map (`Ptr -> Void`) must keep R0 Unknown: {map:?}"
);
}
#[test]
fn helper_map_lookup_elem_value_type_struct_via_typedef() {
let (blob, datasec_id, var_off, value_sid, parent_id) =
btf_with_maps_and_task_ctx(MapValueShape::Typedef);
let btf = Btf::from_bytes(&blob).unwrap();
let [ld_lo, ld_hi] = ld_imm64(1, var_off as i32);
let mov_key = mov_k(2, 0);
let call_lookup = helper_call(BPF_FUNC_MAP_LOOKUP_ELEM);
let stx_kptr = stx(BPF_SIZE_DW, 6, 0, 8);
let insns = vec![ld_lo, ld_hi, mov_key, call_lookup, 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,
&[InitialReg {
reg: 6,
struct_type_id: parent_id,
}],
&[],
&datasec_pointers,
&[],
);
assert_eq!(
map.get(&(parent_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: value_sid,
addr_space: AddrSpace::Kernel,
}),
"typedef-wrapped value type must peel to the underlying struct id: {map:?}"
);
}
#[test]
fn helper_map_lookup_elem_no_map_metadata_keeps_r0_unknown() {
let (blob, _datasec_id, var_off, _value_sid, parent_id) =
btf_with_maps_and_task_ctx(MapValueShape::Struct);
let btf = Btf::from_bytes(&blob).unwrap();
let [ld_lo, ld_hi] = ld_imm64(1, var_off as i32);
let mov_key = mov_k(2, 0);
let call_lookup = helper_call(BPF_FUNC_MAP_LOOKUP_ELEM);
let stx_kptr = stx(BPF_SIZE_DW, 6, 0, 8);
let insns = vec![ld_lo, ld_hi, mov_key, call_lookup, stx_kptr, exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 6,
struct_type_id: parent_id,
}],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"without DatasecPointer annotation R1 stays Unknown so the \
helper-return arm cannot type R0: {map:?}"
);
}
#[test]
fn helper_not_in_allowlist_keeps_r0_unknown() {
let (blob, datasec_id, var_off, _value_sid, parent_id) =
btf_with_maps_and_task_ctx(MapValueShape::Struct);
let btf = Btf::from_bytes(&blob).unwrap();
let [ld_lo, ld_hi] = ld_imm64(1, var_off as i32);
let mov_key = mov_k(2, 0);
let call_other = helper_call(35);
let stx_kptr = stx(BPF_SIZE_DW, 6, 0, 8);
let insns = vec![ld_lo, ld_hi, mov_key, call_other, 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,
&[InitialReg {
reg: 6,
struct_type_id: parent_id,
}],
&[],
&datasec_pointers,
&[],
);
assert!(
map.is_empty(),
"non-bpf_map_lookup_elem helper must not type R0 even with \
a valid `.maps` R1: {map:?}"
);
}
#[test]
fn helper_imm_negative_or_zero_keeps_r0_unknown() {
let (blob, datasec_id, var_off, _value_sid, parent_id) =
btf_with_maps_and_task_ctx(MapValueShape::Struct);
let btf = Btf::from_bytes(&blob).unwrap();
let [ld_lo, ld_hi] = ld_imm64(1, var_off as i32);
let mov_key = mov_k(2, 0);
let stx_kptr = stx(BPF_SIZE_DW, 6, 0, 8);
let datasec_pointers = vec![DatasecPointer {
insn_offset: 0,
datasec_type_id: datasec_id,
base_offset: var_off,
}];
{
let call_zero = helper_call(0);
let insns = vec![ld_lo, ld_hi, mov_key, call_zero, stx_kptr, exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 6,
struct_type_id: parent_id,
}],
&[],
&datasec_pointers,
&[],
);
assert!(
map.is_empty(),
"helper id 0 (BPF_FUNC_unspec) must not seed R0: {map:?}"
);
}
{
let call_neg = helper_call(-1);
let insns = vec![ld_lo, ld_hi, mov_key, call_neg, stx_kptr, exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 6,
struct_type_id: parent_id,
}],
&[],
&datasec_pointers,
&[],
);
assert!(map.is_empty(), "helper id -1 must not seed R0: {map:?}");
}
}
#[test]
fn stx_through_helper_returned_pointer_records_finding() {
let (blob, datasec_id, var_off, value_sid, parent_id) =
btf_with_maps_and_task_ctx(MapValueShape::Struct);
let btf = Btf::from_bytes(&blob).unwrap();
let [ld_lo, ld_hi] = ld_imm64(1, var_off as i32);
let mov_key = mov_k(2, 0);
let call_lookup = helper_call(BPF_FUNC_MAP_LOOKUP_ELEM);
let mov_r7 = mov_x(7, 0);
let stx_kptr = stx(BPF_SIZE_DW, 6, 7, 8);
let insns = vec![ld_lo, ld_hi, mov_key, call_lookup, mov_r7, 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,
&[InitialReg {
reg: 6,
struct_type_id: parent_id,
}],
&[],
&datasec_pointers,
&[],
);
assert_eq!(
map.get(&(parent_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: value_sid,
addr_space: AddrSpace::Kernel,
}),
"lookup -> mov -> stx into task_ctx.cgx_raw must record a kernel \
cast finding: {map:?}"
);
}
fn btf_with_maps_and_task_ctx(value_kind: MapValueShape) -> (Vec<u8>, u32, u32, u32, u32) {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_entry = push_name(&mut strings, "cbw_cgrp_entry");
let n_cgx = push_name(&mut strings, "cgx");
let n_value = push_name(&mut strings, "value");
let n_type = push_name(&mut strings, "type");
let n_map_def = push_name(&mut strings, "anon_map_def");
let n_map_var = push_name(&mut strings, "cbw_cgrp_map");
let n_maps = push_name(&mut strings, ".maps");
let n_entry_typedef = push_name(&mut strings, "cbw_cgrp_entry_t");
let n_task_ctx = push_name(&mut strings, "task_ctx");
let n_cgx_raw = push_name(&mut strings, "cgx_raw");
let mut types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_entry,
size: 8,
members: vec![SynMember {
name_off: n_cgx,
type_id: 1,
byte_offset: 0,
}],
},
SynType::Struct {
name_off: n_task_ctx,
size: 16,
members: vec![SynMember {
name_off: n_cgx_raw,
type_id: 1,
byte_offset: 8,
}],
},
];
let parent_id = 3u32;
let (value_ptr_id, expected_struct_id) = match value_kind {
MapValueShape::Struct => {
types.push(SynType::Ptr { type_id: 2 });
(4u32, 2u32)
}
MapValueShape::U64 => {
types.push(SynType::Ptr { type_id: 1 });
(4u32, 0u32)
}
MapValueShape::Typedef => {
types.push(SynType::Ptr { type_id: 5 });
types.push(SynType::Typedef {
name_off: n_entry_typedef,
type_id: 2,
});
(4u32, 2u32)
}
MapValueShape::Void => {
types.push(SynType::Ptr { type_id: 0 });
(4u32, 0u32)
}
};
let map_def_id = types.len() as u32 + 1;
types.push(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: value_ptr_id,
byte_offset: 8,
},
],
});
let map_var_id = map_def_id + 1;
types.push(SynType::Var {
name_off: n_map_var,
type_id: map_def_id,
linkage: 1,
});
let datasec_id = map_var_id + 1;
types.push(SynType::Datasec {
name_off: n_maps,
size: 16,
entries: vec![SynVarSecinfo {
type_id: map_var_id,
offset: 0,
size: 16,
}],
});
let blob = build_btf(&types, &strings);
(blob, datasec_id, 0, expected_struct_id, parent_id)
}
#[test]
fn helper_map_lookup_elem_non_dot_maps_datasec_drops() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_entry = push_name(&mut strings, "cbw_cgrp_entry");
let n_cgx = push_name(&mut strings, "cgx");
let n_value = push_name(&mut strings, "value");
let n_type = push_name(&mut strings, "type");
let n_map_def = push_name(&mut strings, "anon_map_def");
let n_map_var = push_name(&mut strings, "fake_map");
let n_bss = push_name(&mut strings, ".bss");
let n_task_ctx = push_name(&mut strings, "task_ctx");
let n_cgx_raw = 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_entry,
size: 8,
members: vec![SynMember {
name_off: n_cgx,
type_id: 1,
byte_offset: 0,
}],
},
SynType::Struct {
name_off: n_task_ctx,
size: 16,
members: vec![SynMember {
name_off: n_cgx_raw,
type_id: 1,
byte_offset: 8,
}],
},
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_bss,
size: 16,
entries: vec![SynVarSecinfo {
type_id: 6,
offset: 0,
size: 16,
}],
},
];
let parent_id = 3u32;
let datasec_id = 7u32;
let var_off = 0u32;
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let [ld_lo, ld_hi] = ld_imm64(1, var_off as i32);
let mov_key = mov_k(2, 0);
let call_lookup = helper_call(BPF_FUNC_MAP_LOOKUP_ELEM);
let stx_kptr = stx(BPF_SIZE_DW, 6, 0, 8);
let insns = vec![ld_lo, ld_hi, mov_key, call_lookup, 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,
&[InitialReg {
reg: 6,
struct_type_id: parent_id,
}],
&[],
&datasec_pointers,
&[],
);
assert!(
map.is_empty(),
"non-`.maps` datasec must not drive the helper-return arm even \
with a structurally matching map-def shape: {map:?}"
);
}
#[test]
fn empty_access_pattern_does_not_trigger_conflict_with_kptr() {
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), stx(BPF_SIZE_DW, 1, 5, 8), exit()];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: t_id,
},
InitialReg {
reg: 5,
struct_type_id: q_id,
},
],
&[],
&[],
&[],
);
assert!(
map.contains_key(&(t_id, 8)),
"kptr finding on slot with empty-access pattern (LDX without deref) \
must NOT be dropped by conflict detection: {map:?}"
);
}
#[test]
fn only_ld_imm64_no_oob() {
const N_PAIRS: usize = 50;
let (blob, t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let mut insns: Vec<BpfInsn> = Vec::with_capacity(2 * N_PAIRS + 1);
let lo = mk_insn(BPF_CLASS_LD | BPF_SIZE_DW | BPF_MODE_IMM, 2, 0, 0, 0);
let hi = mk_insn(0, 0, 0, 0, 0);
for _ in 0..N_PAIRS {
insns.push(lo);
insns.push(hi);
}
insns.push(exit());
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"all-LD_IMM64 stream must produce no findings, no OOB panic: {map:?}"
);
}
#[test]
fn arena_stx_pending_then_duplicate_is_idempotent() {
let (blob, t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
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, 0, 8),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 6,
struct_type_id: t_id,
}],
&[],
&[],
&[SubprogReturn {
alloc_size: None,
insn_offset: 0,
}],
);
assert!(
!map.is_empty(),
"duplicate STX to same slot must not conflict; map: {map:?}"
);
}
#[test]
fn three_way_conflict_arena_kptr_pattern_drops_all() {
let (blob, t_id, q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let cast = addr_space_cast(3, 2, 1);
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 8),
cast,
stx(BPF_SIZE_DW, 1, 3, 8),
stx(BPF_SIZE_DW, 1, 5, 8),
ldx(BPF_SIZE_DW, 6, 1, 8),
ldx(BPF_SIZE_DW, 7, 6, 0),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: t_id,
},
InitialReg {
reg: 5,
struct_type_id: q_id,
},
],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"arena + kptr + pattern on same slot must all drop: {map:?}"
);
}
#[test]
fn struct_member_at_resolves_array_element_offset() {
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_history = push_name(&mut strings, "history");
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::Array {
type_id: 1,
index_type_id: 1,
nelems: 4,
},
SynType::Struct {
name_off: n_t,
size: 32,
members: vec![SynMember {
name_off: n_history,
type_id: 2,
byte_offset: 0,
}],
},
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 = 3u32;
let q_id = 4u32;
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 16),
addr_space_cast(4, 2, 1),
stx(BPF_SIZE_DW, 1, 4, 16),
ldx(BPF_SIZE_DW, 3, 4, 0),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert_eq!(
map.get(&(t_id, 16)),
Some(&CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
"(T={t_id}, 16) — third u64 element of `history[4]` — must \
appear in the cast map with target=Q ({q_id}) after \
struct_member_at peels the array member type to `u64`: \
{map:?}"
);
}
#[test]
fn stx_nested_struct_arena_finding_keys_on_inner() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_inner = push_name(&mut strings, "Inner");
let n_cgx = push_name(&mut strings, "cgx_raw");
let n_llcx = push_name(&mut strings, "llcx_raw");
let n_outer = push_name(&mut strings, "Outer");
let n_inner_field = push_name(&mut strings, "inner");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_inner,
size: 16,
members: vec![
SynMember {
name_off: n_cgx,
type_id: 1,
byte_offset: 0,
},
SynMember {
name_off: n_llcx,
type_id: 1,
byte_offset: 8,
},
],
},
SynType::Struct {
name_off: n_outer,
size: 16,
members: vec![SynMember {
name_off: n_inner_field,
type_id: 2,
byte_offset: 0,
}],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let inner_id: u32 = 2;
let outer_id: u32 = 3;
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),
mov_x(7, 0),
stx(BPF_SIZE_DW, 6, 7, 8),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 6,
struct_type_id: outer_id,
}],
&[],
&[],
&[SubprogReturn {
alloc_size: None,
insn_offset: 0,
}],
);
assert_eq!(
map.get(&(inner_id, 0)),
Some(&CastHit {
alloc_size: None,
target_type_id: 0,
addr_space: AddrSpace::Arena,
}),
"nested struct STX must key on (Inner, 0) not (Outer, 0): {map:?}"
);
assert_eq!(
map.get(&(inner_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: 0,
addr_space: AddrSpace::Arena,
}),
"nested struct STX must key on (Inner, 8) not (Outer, 8): {map:?}"
);
assert!(
!map.contains_key(&(outer_id, 0)),
"outer struct id must NOT appear as key: {map:?}"
);
assert!(
!map.contains_key(&(outer_id, 8)),
"outer struct id must NOT appear as key: {map:?}"
);
}
#[test]
fn ldx_nested_struct_loads_inner_key() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_inner = push_name(&mut strings, "Inner");
let n_field = push_name(&mut strings, "ptr_field");
let n_outer = push_name(&mut strings, "Outer");
let n_embed = push_name(&mut strings, "embed");
let n_target = push_name(&mut strings, "Target");
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_inner,
size: 16,
members: vec![SynMember {
name_off: n_field,
type_id: 1,
byte_offset: 8,
}],
},
SynType::Struct {
name_off: n_outer,
size: 16,
members: vec![SynMember {
name_off: n_embed,
type_id: 2,
byte_offset: 0,
}],
},
SynType::Struct {
name_off: n_target,
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 inner_id: u32 = 2;
let outer_id: u32 = 3;
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: outer_id,
}],
&[],
&[],
&[],
);
assert!(
map.contains_key(&(inner_id, 8)),
"nested LDX + deref must key on (Inner={inner_id}, 8) \
not (Outer={outer_id}, 8): {map:?}"
);
assert!(
!map.contains_key(&(outer_id, 8)),
"outer id must NOT appear as key for nested member: {map:?}"
);
}
#[test]
fn cross_function_u64_param_inherits_caller_pointer_type() {
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_caller = push_name(&mut strings, "caller");
let n_callee = push_name(&mut strings, "callee");
let n_taskc_raw = push_name(&mut strings, "taskc_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,
}],
},
SynType::FuncProto {
return_type_id: 0,
params: vec![SynParam {
name_off: n_taskc_raw,
type_id: 1,
}],
},
SynType::Func {
name_off: n_callee,
type_id: 3,
linkage: 1,
},
SynType::Func {
name_off: n_caller,
type_id: 3,
linkage: 1,
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let m_id = 2;
let insns = vec![
mk_insn(BPF_CLASS_JMP | BPF_OP_CALL, 0, BPF_PSEUDO_CALL, 0, 2),
exit(),
exit(),
mov_x(6, 1),
mk_insn(BPF_CLASS_JMP | BPF_OP_CALL, 0, BPF_PSEUDO_CALL, 0, 0),
stx(BPF_SIZE_DW, 6, 0, 8),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: m_id,
}],
&[FuncEntry {
insn_offset: 3,
func_proto_id: 3,
}],
&[],
&[SubprogReturn {
alloc_size: None,
insn_offset: 4,
}],
);
assert_eq!(
map.get(&(m_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: 0,
addr_space: AddrSpace::Arena,
}),
"cross-function u64 param must inherit caller's Pointer{{M}} \
and record arena STX at (M, 8): {map:?}"
);
}
#[test]
fn helper_map_update_then_lookup_propagates_arena_through_map_value() {
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, "field");
let n_p = push_name(&mut strings, "P");
let n_p_field = push_name(&mut strings, "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: 16,
members: vec![SynMember {
name_off: n_v_field,
type_id: 1,
byte_offset: 8,
}],
},
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,
}],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let v_id = 2u32;
let p_id = 3u32;
let datasec_id = 7u32;
let var_off = 0u32;
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, -16);
let [ld_lo_pre, ld_hi_pre] = ld_imm64(1, var_off as i32);
let mov_r2_from_r10 = mov_x(2, 10);
let r2_minus_24 = mk_insn(BPF_CLASS_ALU64 | BPF_OP_ADD, 2, 0, 0, -24);
let mov_r3_from_r10 = mov_x(3, 10);
let r3_minus_24 = mk_insn(BPF_CLASS_ALU64 | BPF_OP_ADD, 3, 0, 0, -24);
let call_update = helper_call(2);
let [ld_lo_post, ld_hi_post] = ld_imm64(1, var_off as i32);
let call_lookup = helper_call(BPF_FUNC_MAP_LOOKUP_ELEM);
let ldx_v_field = ldx(BPF_SIZE_DW, 2, 0, 8);
let stx_into_p = stx(BPF_SIZE_DW, 6, 2, 0);
let insns = vec![
pseudo_call,
stx_spill,
ld_lo_pre,
ld_hi_pre,
mov_r2_from_r10,
r2_minus_24,
mov_r3_from_r10,
r3_minus_24,
call_update,
ld_lo_post,
ld_hi_post,
call_lookup,
ldx_v_field,
stx_into_p,
exit(),
];
let datasec_pointers = vec![
DatasecPointer {
insn_offset: 2,
datasec_type_id: datasec_id,
base_offset: var_off,
},
DatasecPointer {
insn_offset: 9,
datasec_type_id: datasec_id,
base_offset: var_off,
},
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 6,
struct_type_id: p_id,
}],
&[],
&datasec_pointers,
&[SubprogReturn {
alloc_size: None,
insn_offset: 0,
}],
);
let _ = v_id;
assert_eq!(
map.get(&(p_id, 0)),
Some(&CastHit {
alloc_size: None,
target_type_id: 0,
addr_space: AddrSpace::Arena,
}),
"map-value arena propagation must surface `(P, 0) -> Arena` \
after update_elem(&V_with_arena_at_off_8) -> lookup_elem -> \
LDX V.field -> STX into P.field: {map:?}"
);
}
#[test]
fn cross_function_fixpoint_callee_before_caller() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_parent = push_name(&mut strings, "Parent");
let n_field = push_name(&mut strings, "arena_field");
let n_caller = push_name(&mut strings, "caller");
let n_callee = push_name(&mut strings, "callee");
let n_p1 = push_name(&mut strings, "parent_raw");
let n_p2 = push_name(&mut strings, "val_raw");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_parent,
size: 16,
members: vec![SynMember {
name_off: n_field,
type_id: 1,
byte_offset: 8,
}],
},
SynType::Ptr { type_id: 2 },
SynType::FuncProto {
return_type_id: 0,
params: vec![
SynParam {
name_off: n_p1,
type_id: 1,
},
SynParam {
name_off: n_p2,
type_id: 1,
},
],
},
SynType::Func {
name_off: n_callee,
type_id: 4,
linkage: 1,
},
SynType::FuncProto {
return_type_id: 0,
params: vec![SynParam {
name_off: n_p1,
type_id: 3,
}],
},
SynType::Func {
name_off: n_caller,
type_id: 6,
linkage: 1,
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let parent_id: u32 = 2;
let alloc_call = mk_insn(BPF_CLASS_JMP | BPF_OP_CALL, 0, BPF_PSEUDO_CALL, 0, 0);
let callee_call = mk_insn(BPF_CLASS_JMP | BPF_OP_CALL, 0, BPF_PSEUDO_CALL, 0, -7);
let insns = vec![
stx(BPF_SIZE_DW, 1, 2, 8), exit(), mov_x(6, 1), alloc_call, mov_x(2, 0), mov_x(1, 6), callee_call, exit(), ];
let map = analyze_casts(
&insns,
&btf,
&[],
&[
FuncEntry {
insn_offset: 0,
func_proto_id: 4,
}, FuncEntry {
insn_offset: 2,
func_proto_id: 6,
}, ],
&[],
&[SubprogReturn {
alloc_size: None,
insn_offset: 3,
}], );
assert_eq!(
map.get(&(parent_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: 0,
addr_space: AddrSpace::Arena,
}),
"fixpoint must propagate caller's [Pointer{{Parent}}, ArenaU64FromAlloc] \
into callee at lower PC, producing (Parent, 8) -> Arena: {map:?}"
);
}
#[test]
fn finalize_arena_stx_emits_deferred_resolve_when_shape_inference_ambiguous() {
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_r = push_name(&mut strings, "R");
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,
},
],
},
SynType::Struct {
name_off: n_r,
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 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,
}],
);
let hit = map
.get(&(p_id, 8))
.expect("(P, 8) must be in CastMap even when shape inference is ambiguous");
assert_eq!(
hit.addr_space,
AddrSpace::Arena,
"ambiguous-shape STX-flow tag must still emit AddrSpace::Arena: {map:?}"
);
assert_eq!(
hit.target_type_id, 0,
"ambiguous shape (Q and R both 16-byte u64@0+u64@8) must yield \
target_type_id=0 (deferred resolve via MemReader::resolve_arena_type \
bridge at chase time). target_type_id={} suggests one of Q/R was \
picked arbitrarily — false-positive render of the wrong struct \
shape: {map:?}",
hit.target_type_id
);
assert!(
hit.target_type_id != 3 && hit.target_type_id != 4,
"target_type_id={} must NOT be one of the ambiguous candidates Q (3) or R (4); \
picking one arbitrarily would render the slot's payload against the wrong \
struct shape at chase time: {map:?}",
hit.target_type_id
);
}
#[test]
fn stx_flow_stx_before_deref_resolves_target_via_shape_inference() {
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_cgx = push_name(&mut strings, "cgx_raw");
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_cgx,
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![
pseudo_call,
stx(BPF_SIZE_DW, 6, 0, 8),
ldx(BPF_SIZE_DW, 2, 6, 8),
ldx(BPF_SIZE_DW, 3, 2, 0),
ldx(BPF_SIZE_DW, 4, 2, 8),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 6,
struct_type_id: p_id,
}],
&[],
&[],
&[SubprogReturn {
alloc_size: None,
insn_offset: 0,
}],
);
let hit = map
.get(&(p_id, 8))
.expect("(P, 8) must be in CastMap — STX-flow gates emission");
assert_eq!(
hit.addr_space,
AddrSpace::Arena,
"STX-flow tag must yield AddrSpace::Arena: {map:?}"
);
assert_eq!(
hit.target_type_id, q_id,
"post-fix shape inference must resolve target_type_id=q_id ({q_id}) \
even when the STX-flow tag fires BEFORE the deref pattern \
(pre-fix bug: alias-tagged register dropped accesses, leaving \
target_type_id=0): {map:?}"
);
}