use super::*;
#[test]
fn empty_insns_yields_empty_map() {
let (blob, _t, _q) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let map = analyze_casts(&[], &btf, &[], &[], &[], &[]);
assert!(map.is_empty());
}
#[test]
fn no_initial_seed_yields_empty_map() {
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, &[], &[], &[], &[]);
assert!(map.is_empty());
}
#[test]
fn simple_cast_recovers_target() {
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(4, 2, 1),
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, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
"got: {map:?}"
);
}
#[test]
fn shape_inference_alone_drops_without_arena_confirmed() {
let (blob, t_id, q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let insns_no_evidence = vec![ldx(BPF_SIZE_DW, 2, 1, 8), ldx(BPF_SIZE_DW, 3, 2, 0), exit()];
let map_no_evidence = analyze_casts(
&insns_no_evidence,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(
map_no_evidence.is_empty(),
"shape inference without `arena_confirmed` / `arena_stx_findings` \
must drop per the arena-evidence mitigation: {map_no_evidence:?}"
);
let insns_with_evidence = vec![
ldx(BPF_SIZE_DW, 2, 1, 8),
addr_space_cast(4, 2, 1),
ldx(BPF_SIZE_DW, 3, 4, 0),
exit(),
];
let map_with_evidence = analyze_casts(
&insns_with_evidence,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert_eq!(
map_with_evidence.get(&(t_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
"with addr_space_cast evidence the same shape MUST emit, \
proving (a)'s empty result is the arena-evidence gate firing: \
{map_with_evidence:?}"
);
}
#[test]
fn arena_evidence_rejects_shape_inference_without_evidence() {
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_u32 = push_name(&mut strings, "u32");
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_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::Int {
name_off: n_u32,
size: 4,
encoding: 0,
offset: 0,
bits: 32,
},
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: 12,
members: vec![
SynMember {
name_off: n_a,
type_id: 1,
byte_offset: 0,
},
SynMember {
name_off: n_b,
type_id: 2,
byte_offset: 8,
},
],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_id = 3;
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 8),
ldx(BPF_SIZE_DW, 3, 2, 0),
ldx(BPF_SIZE_W, 4, 2, 8),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"multi-offset shape inference with NO direct arena evidence \
(no addr_space_cast, no STX-flow tag) must drop per the \
arena-evidence mitigation: {map:?}"
);
}
#[test]
fn ambiguous_targets_drop_silently() {
let mut strings: Vec<u8> = vec![0];
let n_int = push_name(&mut strings, "u64");
let n_t = push_name(&mut strings, "T");
let n_q1 = push_name(&mut strings, "Q1");
let n_q2 = push_name(&mut strings, "Q2");
let n_f = push_name(&mut strings, "f");
let n_x = push_name(&mut strings, "x");
let types = vec![
SynType::Int {
name_off: n_int,
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_q1,
size: 8,
members: vec![SynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
SynType::Struct {
name_off: n_q2,
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 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: 2,
}],
&[],
&[],
&[],
);
assert!(map.is_empty(), "ambiguous candidates must drop: {map:?}");
}
#[test]
fn multi_offset_disambiguates_target() {
let mut strings: Vec<u8> = vec![0];
let n_u32 = push_name(&mut strings, "u32");
let n_u64 = push_name(&mut strings, "u64");
let n_t = push_name(&mut strings, "T");
let n_q1 = push_name(&mut strings, "Q1");
let n_q2 = push_name(&mut strings, "Q2");
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_u32,
size: 4,
encoding: 0,
offset: 0,
bits: 32,
},
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: 2,
byte_offset: 8,
}],
},
SynType::Struct {
name_off: n_q1,
size: 16,
members: vec![
SynMember {
name_off: n_a,
type_id: 2,
byte_offset: 0,
},
SynMember {
name_off: n_b,
type_id: 2,
byte_offset: 8,
},
],
},
SynType::Struct {
name_off: n_q2,
size: 16,
members: vec![
SynMember {
name_off: n_a,
type_id: 2,
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 = 3;
let q1_id = 4;
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 8),
addr_space_cast(2, 2, 1),
ldx(BPF_SIZE_DW, 3, 2, 0),
ldx(BPF_SIZE_DW, 4, 2, 8),
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: q1_id,
addr_space: AddrSpace::Arena,
}),
"map: {map:?}"
);
}
#[test]
fn multiple_distinct_casts_recorded() {
let mut strings: Vec<u8> = vec![0];
let n_u32 = push_name(&mut strings, "u32");
let n_u64 = push_name(&mut strings, "u64");
let n_t = push_name(&mut strings, "T");
let n_q1 = push_name(&mut strings, "Q1");
let n_q2 = push_name(&mut strings, "Q2");
let n_f1 = push_name(&mut strings, "f1");
let n_f2 = push_name(&mut strings, "f2");
let n_a = push_name(&mut strings, "a");
let n_b = push_name(&mut strings, "b");
let types = vec![
SynType::Int {
name_off: n_u32,
size: 4,
encoding: 0,
offset: 0,
bits: 32,
},
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_t,
size: 24,
members: vec![
SynMember {
name_off: n_f1,
type_id: 2,
byte_offset: 8,
},
SynMember {
name_off: n_f2,
type_id: 2,
byte_offset: 16,
},
],
},
SynType::Struct {
name_off: n_q1,
size: 16,
members: vec![SynMember {
name_off: n_a,
type_id: 2,
byte_offset: 8,
}],
},
SynType::Struct {
name_off: n_q2,
size: 12,
members: vec![
SynMember {
name_off: n_a,
type_id: 2,
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 = 3;
let q1_id = 4;
let q2_id = 5;
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 8),
addr_space_cast(2, 2, 1),
ldx(BPF_SIZE_DW, 3, 2, 8),
mov_k(2, 0),
ldx(BPF_SIZE_DW, 2, 1, 16),
addr_space_cast(2, 2, 1),
ldx(BPF_SIZE_DW, 4, 2, 0),
ldx(BPF_SIZE_W, 5, 2, 8),
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: q1_id,
addr_space: AddrSpace::Arena,
}),
"f1: {map:?}"
);
assert_eq!(
map.get(&(t_id, 16)),
Some(&CastHit {
alloc_size: None,
target_type_id: q2_id,
addr_space: AddrSpace::Arena,
}),
"f2: {map:?}"
);
}
#[test]
fn register_reuse_after_call_clears_state() {
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), call(), 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(),
"post-call r2 must not retain T.f source: {map:?}"
);
}
#[test]
fn nondw_load_does_not_track_u64_field() {
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_W, 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,
}],
&[],
&[],
&[],
);
assert!(map.is_empty(), "32-bit load must not seed cast: {map:?}");
}
#[test]
fn ptr_field_tracked_as_typed_pointer_not_cast() {
let mut strings: Vec<u8> = vec![0];
let n_int = 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_int,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_q,
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_t,
size: 16,
members: vec![SynMember {
name_off: n_f,
type_id: 3,
byte_offset: 8,
}],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_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,
}],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"typed Ptr field must not be recorded as cast: {map:?}"
);
}
#[test]
fn null_check_fall_through_preserves_state() {
let (blob, t_id, q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let variants: &[(u8, &str)] = &[
(BPF_CLASS_JMP | 0x10, "JEQ_K"),
(BPF_CLASS_JMP | 0x10 | BPF_SRC_X, "JEQ_X"),
(BPF_CLASS_JMP | 0x20, "JGT_K"),
(BPF_CLASS_JMP | 0x30, "JGE_K"),
(BPF_CLASS_JMP | 0x40, "JSET_K"),
(BPF_CLASS_JMP | 0x50, "JNE_K"),
(BPF_CLASS_JMP | 0x60, "JSGT_K"),
(BPF_CLASS_JMP | 0x70, "JSGE_K"),
(BPF_CLASS_JMP | 0xa0, "JLT_K"),
(BPF_CLASS_JMP | 0xb0, "JLE_K"),
(BPF_CLASS_JMP | 0xc0, "JSLT_K"),
(BPF_CLASS_JMP | 0xd0, "JSLE_K"),
(BPF_CLASS_JMP32 | 0x10, "JEQ32_K"),
];
for (code, label) in variants {
let jcc = mk_insn(*code, 2, 0, 1, 0);
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 8),
addr_space_cast(2, 2, 1),
jcc,
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.len(),
1,
"{label}: exactly one cast expected on fall-through, got: {map:?}"
);
assert_eq!(
map.get(&(t_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
"{label}: fall-through deref must record: {map:?}"
);
}
}
#[test]
fn deref_at_jump_target_is_dropped() {
let (blob, t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let jne = mk_insn(BPF_CLASS_JMP | 0x50, 2, 0, 1, 0); let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 8),
jne,
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(), "deref at branch target must drop: {map:?}");
}
#[test]
fn mov_x_propagates_loaded_state() {
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),
mov_x(4, 2),
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, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
"MOV must propagate: {map:?}"
);
}
#[test]
fn ld_imm64_skips_second_slot() {
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, 0);
let ld_imm64_hi = mk_insn(0, 0, 0, 0, 0);
let insns = vec![
ld_imm64_lo,
ld_imm64_hi,
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,
}),
"LD_IMM64 second slot must skip: {map:?}"
);
}
#[test]
fn r10_seed_rejected() {
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, 10, 8),
ldx(BPF_SIZE_DW, 3, 2, 0),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 10,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(map.is_empty(), "r10 seed must be ignored: {map:?}");
}
#[test]
fn nonu64_field_at_source_offset_not_tracked() {
let mut strings: Vec<u8> = vec![0];
let n_u32 = push_name(&mut strings, "u32");
let n_t = push_name(&mut strings, "T");
let n_f = push_name(&mut strings, "f");
let types = vec![
SynType::Int {
name_off: n_u32,
size: 4,
encoding: 0,
offset: 0,
bits: 32,
},
SynType::Struct {
name_off: n_t,
size: 12,
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, 0), exit()];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"u32-typed field must not seed cast: {map:?}"
);
}