use super::*;
#[test]
fn mov_to_r10_rejected_keeps_r10_unknown() {
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),
mov_x(10, 2),
mov_x(3, 10),
ldx(BPF_SIZE_DW, 4, 3, 0),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"MOV r10, r2 must be rejected so r10 stays Unknown: {map:?}"
);
}
#[test]
fn ldx_into_r10_rejected_keeps_r10_unknown() {
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, 10, 1, 8),
mov_x(3, 10),
ldx(BPF_SIZE_DW, 4, 3, 0),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"LDX r10, [r1+8] must be rejected so r10 stays Unknown: {map:?}"
);
}
#[test]
fn oob_stx_reg_does_not_panic() {
let (blob, _t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let bad = BpfInsn::new(BPF_CLASS_STX | BPF_SIZE_DW | BPF_MODE_MEM, 15, 15, 0, 0);
let insns = vec![bad, exit()];
let map = analyze_casts(&insns, &btf, &[], &[], &[], &[]);
assert!(
map.is_empty(),
"OOB STX (dst=15, src=15) must not panic: {map:?}"
);
}
#[test]
fn oob_mov_reg_does_not_panic() {
let (blob, _t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let bad = BpfInsn::new(BPF_CLASS_ALU64 | BPF_OP_MOV | BPF_SRC_X, 15, 0, 0, 0);
let insns = vec![bad, exit()];
let map = analyze_casts(&insns, &btf, &[], &[], &[], &[]);
assert!(map.is_empty(), "OOB MOV (dst=15) must not panic: {map:?}");
}
#[test]
fn oob_atomic_reg_does_not_panic() {
let (blob, _t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let bad = BpfInsn::new(
BPF_CLASS_STX | BPF_SIZE_DW | BPF_MODE_ATOMIC,
15,
15,
0,
BPF_FETCH | 0xe0,
);
let insns = vec![bad, exit()];
let map = analyze_casts(&insns, &btf, &[], &[], &[], &[]);
assert!(
map.is_empty(),
"OOB ATOMIC (dst=15, src=15) must not panic: {map:?}"
);
}
#[test]
fn mov_x_unknown_source_overwrites_typed_dst() {
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),
mov_x(2, 3),
ldx(BPF_SIZE_DW, 4, 2, 0),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"MOV with Unknown source must overwrite typed dst: {map:?}"
);
}
#[test]
fn mov_x_self_copy_preserves_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(2, 2),
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,
}),
"MOV self-copy must preserve LoadedU64Field state: {map:?}"
);
}
#[test]
fn mov32_destroys_typed_state() {
let (blob, t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let mov32 = mk_insn(BPF_CLASS_ALU | BPF_OP_MOV | BPF_SRC_X, 4, 2, 0, 0);
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 8),
mov32,
ldx(BPF_SIZE_DW, 5, 4, 0),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(map.is_empty(), "32-bit MOV must drop typed state: {map:?}");
}
#[test]
fn alu64_add_x_destroys_typed_pointer() {
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 add_x = mk_insn(BPF_CLASS_ALU64 | BPF_OP_ADD | BPF_SRC_X, 1, 3, 0, 0);
let insns = vec![add_x, stx(BPF_SIZE_DW, 6, 1, slot_off as i16), exit()];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: t_id,
},
InitialReg {
reg: 6,
struct_type_id: p_id,
},
],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"ALU64 ADD X must destroy typed pointer: {map:?}"
);
}
#[test]
fn alu64_sub_x_destroys_typed_pointer() {
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 sub_x = mk_insn(
BPF_CLASS_ALU64 | (bs::BPF_SUB as u8) | BPF_SRC_X,
1,
3,
0,
0,
);
let insns = vec![sub_x, stx(BPF_SIZE_DW, 6, 1, slot_off as i16), exit()];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: t_id,
},
InitialReg {
reg: 6,
struct_type_id: p_id,
},
],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"ALU64 SUB X must destroy typed pointer: {map:?}"
);
}
#[test]
fn alu64_and_x_destroys_typed_pointer() {
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 and_x = mk_insn(
BPF_CLASS_ALU64 | (bs::BPF_AND as u8) | BPF_SRC_X,
1,
3,
0,
0,
);
let insns = vec![and_x, stx(BPF_SIZE_DW, 6, 1, slot_off as i16), exit()];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: t_id,
},
InitialReg {
reg: 6,
struct_type_id: p_id,
},
],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"ALU64 AND X must destroy typed pointer: {map:?}"
);
}
#[test]
fn alu64_add_k_destroys_typed_pointer() {
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 add_k = mk_insn(BPF_CLASS_ALU64 | BPF_OP_ADD, 1, 0, 0, 8);
let insns = vec![add_k, stx(BPF_SIZE_DW, 6, 1, slot_off as i16), exit()];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: t_id,
},
InitialReg {
reg: 6,
struct_type_id: p_id,
},
],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"ALU64 ADD K must destroy typed pointer: {map:?}"
);
}
#[test]
fn mov_k_destroys_typed_pointer() {
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![
mov_k(1, 42),
stx(BPF_SIZE_DW, 6, 1, slot_off as i16),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: t_id,
},
InitialReg {
reg: 6,
struct_type_id: p_id,
},
],
&[],
&[],
&[],
);
assert!(map.is_empty(), "mov_k must destroy typed pointer: {map:?}");
}
#[test]
fn addr_space_cast_unknown_imm_drops_dst() {
let (blob, t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let cast = mk_insn(BPF_CLASS_ALU64 | BPF_OP_MOV | BPF_SRC_X, 4, 3, 1, 2);
let insns = vec![
ldx(BPF_SIZE_DW, 3, 1, 8),
cast,
ldx(BPF_SIZE_DW, 5, 4, 0),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[InitialReg {
reg: 1,
struct_type_id: t_id,
}],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"BPF_ADDR_SPACE_CAST with reserved imm must drop dst: {map:?}"
);
}
#[test]
fn addr_space_cast_arena_imm1_on_pointer_propagates() {
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 cast = mk_insn(BPF_CLASS_ALU64 | BPF_OP_MOV | BPF_SRC_X, 4, 3, 1, 1);
let insns = vec![cast, stx(BPF_SIZE_DW, 6, 4, slot_off as i16), exit()];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 3,
struct_type_id: t_id,
},
InitialReg {
reg: 6,
struct_type_id: p_id,
},
],
&[],
&[],
&[],
);
assert_eq!(
map.get(&(p_id, slot_off)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"ADDR_SPACE_CAST imm=1 on Pointer{{T}} must propagate state: {map:?}"
);
}
#[test]
fn addr_space_cast_kernel_arena_preserves_pointer_source() {
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 cast = mk_insn(BPF_CLASS_ALU64 | BPF_OP_MOV | BPF_SRC_X, 4, 3, 1, 0x10000);
let insns = vec![cast, stx(BPF_SIZE_DW, 6, 4, slot_off as i16), exit()];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 3,
struct_type_id: t_id,
},
InitialReg {
reg: 6,
struct_type_id: p_id,
},
],
&[],
&[],
&[],
);
assert_eq!(
map.len(),
1,
"Pointer through addr_space_cast should produce a kptr CastHit: {map:?}"
);
}
#[test]
fn bpf_ld_abs_clears_r0() {
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 ld_abs = mk_insn(BPF_CLASS_LD | BPF_SIZE_W | (bs::BPF_ABS as u8), 0, 0, 0, 0);
let insns = vec![ld_abs, stx(BPF_SIZE_DW, 6, 0, slot_off as i16), exit()];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 0,
struct_type_id: t_id,
},
InitialReg {
reg: 6,
struct_type_id: p_id,
},
],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"BPF_LD_ABS must clear r0 typed state: {map:?}"
);
}
#[test]
fn bpf_ld_ind_clears_r0() {
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 ld_ind = mk_insn(BPF_CLASS_LD | BPF_SIZE_W | (bs::BPF_IND as u8), 0, 0, 0, 0);
let insns = vec![ld_ind, stx(BPF_SIZE_DW, 6, 0, slot_off as i16), exit()];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 0,
struct_type_id: t_id,
},
InitialReg {
reg: 6,
struct_type_id: p_id,
},
],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"BPF_LD_IND must clear r0 typed state: {map:?}"
);
}
#[test]
fn single_exit_does_not_panic() {
let (blob, _t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let insns = vec![exit()];
let map = analyze_casts(&insns, &btf, &[], &[], &[], &[]);
assert!(map.is_empty(), "single EXIT must yield empty map: {map:?}");
}
#[test]
fn jumps_only_program_does_not_panic() {
let (blob, _t_id, _q_id) = btf_with_source_and_target(8, 0);
let btf = Btf::from_bytes(&blob).unwrap();
let jeq = mk_insn(BPF_CLASS_JMP | 0x10, 1, 0, 1, 0);
let ja_plus = mk_insn(BPF_CLASS_JMP, 0, 0, 1, 0);
let ja_minus = mk_insn(BPF_CLASS_JMP, 0, 0, -2, 0);
let insns = vec![jeq, ja_plus, ja_minus, exit()];
let map = analyze_casts(&insns, &btf, &[], &[], &[], &[]);
assert!(
map.is_empty(),
"all-jumps program must yield empty map: {map:?}"
);
}