use super::*;
fn atomic_stx(dst: u8, src: u8, off: i16, imm: i32) -> BpfInsn {
mk_insn(
BPF_CLASS_STX | BPF_SIZE_DW | BPF_MODE_ATOMIC,
dst,
src,
off,
imm,
)
}
#[test]
fn atomic_xchg_clobbers_src() {
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![
atomic_stx(2, 1, 0, 0xe0 | BPF_FETCH),
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(),
"XCHG must clobber src R1 typed state: {map:?}"
);
}
#[test]
fn atomic_cmpxchg_clobbers_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 insns = vec![
atomic_stx(2, 1, 0, 0xf0 | BPF_FETCH),
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(),
"CMPXCHG must clobber R0 typed state: {map:?}"
);
}
#[test]
fn atomic_non_fetch_preserves_regs() {
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();
const BPF_ATOMIC_ADD: i32 = 0x00;
const BPF_ATOMIC_OR: i32 = 0x40;
const BPF_ATOMIC_AND: i32 = 0x50;
const BPF_ATOMIC_XOR: i32 = 0xa0;
for imm in [
BPF_ATOMIC_ADD,
BPF_ATOMIC_OR,
BPF_ATOMIC_AND,
BPF_ATOMIC_XOR,
] {
let insns = vec![
atomic_stx(2, 1, 0, imm),
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_eq!(
map.len(),
1,
"imm=0x{imm:02x}: exactly one kptr finding expected, got: {map:?}"
);
assert_eq!(
map.get(&(p_id, slot_off)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"imm=0x{imm:02x}: non-fetch ATOMIC must preserve src register: {map:?}"
);
}
}
#[test]
fn atomic_on_stack_invalidates_slot() {
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![
stx(BPF_SIZE_DW, 10, 1, -8),
atomic_stx(10, 2, -8, 0xe0 | BPF_FETCH),
ldx(BPF_SIZE_DW, 3, 10, -8),
stx(BPF_SIZE_DW, 6, 3, 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(),
"ATOMIC on stack slot must invalidate, reload yields Unknown: {map:?}"
);
}
#[test]
fn atomic_load_acq_clobbers_dst() {
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![
atomic_stx(1, 2, 0, BPF_LOAD_ACQ_IMM),
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(),
"LOAD_ACQ must clobber dst R1 typed state: {map:?}"
);
}
#[test]
fn atomic_store_rel_preserves_src_and_dst() {
let slot_off1: u32 = 16;
let slot_off2: u32 = 24;
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_slot1 = push_name(&mut strings, "slot1");
let n_slot2 = push_name(&mut strings, "slot2");
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_off2 + 8,
members: vec![
SynMember {
name_off: n_slot1,
type_id: 1,
byte_offset: slot_off1,
},
SynMember {
name_off: n_slot2,
type_id: 1,
byte_offset: slot_off2,
},
],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_id = 2;
let p_id = 4;
let insns = vec![
atomic_stx(7, 1, 0, BPF_STORE_REL_IMM),
stx(BPF_SIZE_DW, 6, 1, slot_off1 as i16),
stx(BPF_SIZE_DW, 6, 2, slot_off2 as i16),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: t_id,
},
InitialReg {
reg: 2,
struct_type_id: t_id,
},
InitialReg {
reg: 6,
struct_type_id: p_id,
},
],
&[],
&[],
&[],
);
assert_eq!(
map.get(&(p_id, slot_off1)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"STORE_REL must preserve src R1 typed state (slot1 missing): {map:?}"
);
assert_eq!(
map.get(&(p_id, slot_off2)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"STORE_REL must not affect uninvolved R2 (slot2 missing): {map:?}"
);
}
#[test]
fn atomic_store_rel_invalidates_stack_slot() {
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![
stx(BPF_SIZE_DW, 10, 1, -8),
atomic_stx(10, 2, -8, BPF_STORE_REL_IMM),
ldx(BPF_SIZE_DW, 3, 10, -8),
stx(BPF_SIZE_DW, 6, 3, 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(),
"STORE_REL through r10 must invalidate slot, reload Unknown: {map:?}"
);
}
#[test]
fn atomic_add_fetch_clobbers_src() {
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![
atomic_stx(2, 1, 0, BPF_FETCH),
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(),
"ADD|FETCH must clobber src R1 typed state: {map:?}"
);
}
#[test]
fn atomic_and_fetch_clobbers_src() {
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![
atomic_stx(2, 1, 0, 0x50 | BPF_FETCH),
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(),
"AND|FETCH must clobber src R1 typed state: {map:?}"
);
}
#[test]
fn atomic_or_fetch_clobbers_src() {
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![
atomic_stx(2, 1, 0, 0x40 | BPF_FETCH),
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(),
"OR|FETCH must clobber src R1 typed state: {map:?}"
);
}
#[test]
fn atomic_xor_fetch_clobbers_src() {
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![
atomic_stx(2, 1, 0, 0xa0 | BPF_FETCH),
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(),
"XOR|FETCH must clobber src R1 typed state: {map:?}"
);
}
#[test]
fn atomic_w_size_invalidates_stack_slot() {
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 atomic_w = mk_insn(
BPF_CLASS_STX | BPF_SIZE_W | BPF_MODE_ATOMIC,
10,
2,
-8,
0xe0 | BPF_FETCH,
);
let insns = vec![
stx(BPF_SIZE_DW, 10, 1, -8),
atomic_w,
ldx(BPF_SIZE_DW, 3, 10, -8),
stx(BPF_SIZE_DW, 6, 3, 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(),
"W-size ATOMIC on stack slot must invalidate, reload Unknown: {map:?}"
);
}
#[test]
fn atomic_cmpxchg_clobbers_src() {
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![
atomic_stx(2, 1, 0, 0xf0 | BPF_FETCH),
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(),
"CMPXCHG must clobber src R1 typed state via has_fetch arm: {map:?}"
);
}
#[test]
fn stack_spill_overwrite_uses_latest() {
let slot_off: u32 = 16;
let mut strings: Vec<u8> = vec![0];
let n_u64 = push_name(&mut strings, "u64");
let n_t1 = push_name(&mut strings, "T1");
let n_t2 = push_name(&mut strings, "T2");
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 types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::Struct {
name_off: n_t1,
size: 8,
members: vec![SynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
SynType::Struct {
name_off: n_t2,
size: 8,
members: vec![SynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
SynType::Struct {
name_off: n_p,
size: slot_off + 8,
members: vec![SynMember {
name_off: n_slot,
type_id: 1,
byte_offset: slot_off,
}],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t1_id = 2;
let t2_id = 3;
let p_id = 4;
let insns = vec![
stx(BPF_SIZE_DW, 10, 1, -8),
stx(BPF_SIZE_DW, 10, 2, -8),
ldx(BPF_SIZE_DW, 3, 10, -8),
stx(BPF_SIZE_DW, 6, 3, slot_off as i16),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: t1_id,
},
InitialReg {
reg: 2,
struct_type_id: t2_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: t2_id,
addr_space: AddrSpace::Kernel,
}),
"second spill to same slot must win: {map:?}"
);
}
#[test]
fn stack_spill_survives_helper_call() {
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![
stx(BPF_SIZE_DW, 10, 1, -8),
call(),
ldx(BPF_SIZE_DW, 3, 10, -8),
stx(BPF_SIZE_DW, 6, 3, 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_eq!(
map.get(&(p_id, slot_off)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"stack-spilled pointer must survive helper call: {map:?}"
);
}
#[test]
fn sub_dw_spill_invalidates() {
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![
stx(BPF_SIZE_DW, 10, 1, -8),
stx(BPF_SIZE_W, 10, 1, -8),
ldx(BPF_SIZE_DW, 3, 10, -8),
stx(BPF_SIZE_DW, 6, 3, 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(),
"sub-DW store must invalidate slot, reload Unknown: {map:?}"
);
}
#[test]
fn st_imm_invalidates_stack_slot() {
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 st_imm_dw = mk_insn(BPF_CLASS_ST | BPF_MODE_MEM | BPF_SIZE_DW, 10, 0, -8, 0);
let insns = vec![
stx(BPF_SIZE_DW, 10, 1, -8),
st_imm_dw,
ldx(BPF_SIZE_DW, 3, 10, -8),
stx(BPF_SIZE_DW, 6, 3, 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(),
"BPF_ST imm to stack slot must invalidate, reload Unknown: {map:?}"
);
}