use super::*;
#[test]
fn struct_member_at_skips_bitfield_at_target_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_f = push_name(&mut strings, "f");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::StructBitfields {
name_off: n_t,
size: 16,
members: vec![SynMemberBits {
name_off: n_f,
type_id: 1,
bit_offset: 8 * 8, bitfield_size_bits: 32, }],
},
];
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(),
"bitfield at target offset must not seed cast: {map:?}"
);
}
#[test]
fn struct_member_at_skips_non_byte_aligned_member() {
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_f = push_name(&mut strings, "f");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynType::StructBitfields {
name_off: n_t,
size: 24,
members: vec![SynMemberBits {
name_off: n_f,
type_id: 1,
bit_offset: 65, bitfield_size_bits: 0, }],
},
];
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(),
"non-byte-aligned member must not seed cast: {map:?}"
);
}
#[test]
fn member_size_bytes_unsupported_terminals_skipped() {
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_u = push_name(&mut strings, "U");
let n_fwd_target = push_name(&mut strings, "fwd_struct");
let n_func = push_name(&mut strings, "fn");
let n_fwd_ref = push_name(&mut strings, "fwd_ref");
let n_func_ref = push_name(&mut strings, "func_ref");
let n_void_ref = push_name(&mut strings, "void_ref");
let n_v = push_name(&mut strings, "v");
let n_f = push_name(&mut strings, "f");
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::Fwd {
name_off: n_fwd_target,
kind_flag: 0,
},
SynType::FuncProto {
return_type_id: 0,
params: vec![],
},
SynType::Func {
name_off: n_func,
type_id: 4,
linkage: 1,
},
SynType::Struct {
name_off: n_u,
size: 32,
members: vec![
SynMember {
name_off: n_fwd_ref,
type_id: 3, byte_offset: 0,
},
SynMember {
name_off: n_func_ref,
type_id: 5, byte_offset: 8,
},
SynMember {
name_off: n_void_ref,
type_id: 0, byte_offset: 16,
},
SynMember {
name_off: n_v,
type_id: 1, byte_offset: 24,
},
],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_id = 2;
let u_id = 6;
let insns = vec![
ldx(BPF_SIZE_DW, 2, 1, 8),
addr_space_cast(2, 2, 1),
ldx(BPF_SIZE_DW, 3, 2, 24),
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: u_id,
addr_space: AddrSpace::Arena,
}),
"unsupported terminals must be skipped without crashing: {map:?}"
);
}
#[test]
fn build_layout_index_skips_bitfields_in_candidates() {
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_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 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::StructBitfields {
name_off: n_q1,
size: 8,
members: vec![SynMemberBits {
name_off: n_a,
type_id: 1,
bit_offset: 0,
bitfield_size_bits: 32,
}],
},
SynType::Struct {
name_off: n_q2,
size: 8,
members: vec![SynMember {
name_off: n_a,
type_id: 1,
byte_offset: 0,
}],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_id = 2;
let q2_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),
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: q2_id,
addr_space: AddrSpace::Arena,
}),
"bitfield candidate must be skipped: {map:?}"
);
}
#[test]
fn union_works_like_struct_for_layout_and_member_lookup() {
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_su = push_name(&mut strings, "SourceU");
let n_tu = push_name(&mut strings, "TargetU");
let n_x = push_name(&mut strings, "x");
let n_slot = push_name(&mut strings, "slot");
let n_f = push_name(&mut strings, "f");
let n_a = push_name(&mut strings, "a");
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_x,
type_id: 1,
byte_offset: 8,
}],
},
SynType::Ptr { type_id: 2 }, SynType::Union {
name_off: n_p,
size: 24,
members: vec![SynMember {
name_off: n_slot,
type_id: 1,
byte_offset: 16,
}],
},
SynType::Struct {
name_off: n_su,
size: 16,
members: vec![SynMember {
name_off: n_f,
type_id: 1,
byte_offset: 8,
}],
},
SynType::Union {
name_off: n_tu,
size: 8,
members: vec![SynMember {
name_off: n_a,
type_id: 1,
byte_offset: 0,
}],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_id = 2;
let p_id = 4;
let source_u_id = 5;
let target_u_id = 6;
let insns = vec![
stx(BPF_SIZE_DW, 1, 6, 16),
ldx(BPF_SIZE_DW, 3, 2, 8),
addr_space_cast(3, 3, 1),
ldx(BPF_SIZE_DW, 4, 3, 0),
exit(),
];
let map = analyze_casts(
&insns,
&btf,
&[
InitialReg {
reg: 1,
struct_type_id: p_id,
},
InitialReg {
reg: 6,
struct_type_id: t_id,
},
InitialReg {
reg: 2,
struct_type_id: source_u_id,
},
],
&[],
&[],
&[],
);
assert_eq!(
map.get(&(p_id, 16)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"kptr through union parent must record: {map:?}"
);
assert_eq!(
map.get(&(source_u_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: target_u_id,
addr_space: AddrSpace::Arena,
}),
"union target must be a layout candidate: {map:?}"
);
}
#[test]
fn build_layout_index_consecutive_fail_cap_short_circuits() {
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,
}],
&[],
&[],
&[],
);
assert_eq!(
map.get(&(t_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
"valid candidate must be found before fail cap; sparse \
BTF must not panic: {map:?}"
);
}
#[test]
fn kind_flag_struct_includes_non_bitfield_members() {
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_src = push_name(&mut strings, "src");
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_t,
size: 16,
members: vec![SynMember {
name_off: n_src,
type_id: 1,
byte_offset: 8,
}],
},
SynType::StructBitfields {
name_off: n_q,
size: 16,
members: vec![
SynMemberBits {
name_off: n_a,
type_id: 1,
bit_offset: 0,
bitfield_size_bits: 0,
},
SynMemberBits {
name_off: n_b,
type_id: 1,
bit_offset: 64,
bitfield_size_bits: 32,
},
],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_id = 2;
let q_id = 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: t_id,
}],
&[],
&[],
&[],
);
assert_eq!(
map.get(&(t_id, 8)),
Some(&CastHit {
alloc_size: None,
target_type_id: q_id,
addr_space: AddrSpace::Arena,
}),
"non-bitfield member of kind_flag=1 struct must be a \
layout candidate: {map:?}"
);
}
#[test]
fn stack_off_non_negative_through_r10_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, 0),
ldx(BPF_SIZE_DW, 3, 10, 0),
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(),
"non-negative r10 store must not save state, reload \
returns Unknown: {map:?}"
);
}
#[test]
fn negative_off_in_non_r10_context_drops() {
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,
}],
&[],
&[],
&[],
);
assert!(
map.is_empty(),
"negative offset through Pointer{{T}} must drop, no \
pattern recorded: {map:?}"
);
}
#[test]
fn stack_spill_same_target_stays_single() {
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_DW, 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_eq!(
map.get(&(p_id, slot_off)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"spill of identical Pointer{{T}} must reload as Pointer{{T}}, \
kptr stays Single: {map:?}"
);
}
#[test]
fn kfunc_call_returning_int_ptr_leaves_r0_unknown() {
let slot_off: u32 = 16;
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_slot = push_name(&mut strings, "slot");
let n_kfunc = push_name(&mut strings, "bpf_returns_int_ptr");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
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: 1 }, SynType::FuncProto {
return_type_id: 3,
params: vec![],
},
SynType::Func {
name_off: n_kfunc,
type_id: 4,
linkage: 1,
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let p_id = 2;
let kfunc_id = 5;
let insns = vec![
kfunc_call(kfunc_id),
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 returning int* must leave R0 Unknown: {map:?}"
);
}
#[test]
fn kfunc_call_void_return_leaves_r0_unknown() {
let slot_off: u32 = 16;
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_slot = push_name(&mut strings, "slot");
let n_kfunc = push_name(&mut strings, "bpf_void_return");
let types = vec![
SynType::Int {
name_off: n_u64,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
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::FuncProto {
return_type_id: 0,
params: vec![],
},
SynType::Func {
name_off: n_kfunc,
type_id: 3,
linkage: 1,
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let p_id = 2;
let kfunc_id = 4;
let insns = vec![
kfunc_call(kfunc_id),
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 with void return must leave R0 Unknown: {map:?}"
);
}
#[test]
fn kfunc_call_with_funcproto_id_directly() {
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 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::FuncProto {
return_type_id: 3,
params: vec![],
},
];
let blob = build_btf(&types, &strings);
let btf = Btf::from_bytes(&blob).unwrap();
let t_id = 2;
let p_id = 4;
let proto_id = 5;
let insns = vec![
kfunc_call(proto_id),
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_eq!(
map.get(&(p_id, slot_off)),
Some(&CastHit {
alloc_size: None,
target_type_id: t_id,
addr_space: AddrSpace::Kernel,
}),
"kfunc with direct FuncProto id must seed R0 from return \
type: {map:?}"
);
}