use super::super::*;
use super::*;
use goblin::elf::header as h;
use goblin::elf::section_header as sh;
use goblin::elf::sym as syms;
#[test]
fn build_fwd_index_first_write_wins_on_duplicate_name() {
let mut strings_0 = vec![0u8];
let n_int_0 = push_btf_name(&mut strings_0, "u64");
let n_foo_0 = push_btf_name(&mut strings_0, "foo");
let n_x_0 = push_btf_name(&mut strings_0, "x");
let types_0 = vec![
SynKind::Int {
name_off: n_int_0,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynKind::Struct {
name_off: n_foo_0,
size: 8,
members: vec![SynMember {
name_off: n_x_0,
type_id: 1,
byte_offset: 0,
}],
},
];
let blob_0 = build_btf_full(&types_0, &strings_0);
let btf_0 = Arc::new(Btf::from_bytes(&blob_0).expect("parse btf 0"));
let mut strings_1 = vec![0u8];
let n_int_1 = push_btf_name(&mut strings_1, "u64");
let n_foo_1 = push_btf_name(&mut strings_1, "foo");
let n_y_1 = push_btf_name(&mut strings_1, "y");
let types_1 = vec![
SynKind::Int {
name_off: n_int_1,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynKind::Struct {
name_off: n_foo_1,
size: 16,
members: vec![SynMember {
name_off: n_y_1,
type_id: 1,
byte_offset: 8,
}],
},
];
let blob_1 = build_btf_full(&types_1, &strings_1);
let btf_1 = Arc::new(Btf::from_bytes(&blob_1).expect("parse btf 1"));
let btfs = vec![btf_0, btf_1];
let index = build_fwd_index(&btfs);
assert_eq!(
index.get("foo"),
Some(&FwdIndexEntry {
btfs_idx: 0,
type_id: 2,
}),
"first-write-wins: BTF #0 wins on duplicate name"
);
}
#[test]
fn build_fwd_index_skips_anonymous_structs() {
let mut strings = vec![0u8];
let n_int = push_btf_name(&mut strings, "u64");
let n_x = push_btf_name(&mut strings, "x");
let types = vec![
SynKind::Int {
name_off: n_int,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynKind::Struct {
name_off: 0,
size: 8,
members: vec![SynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
];
let blob = build_btf_full(&types, &strings);
let btf = Arc::new(Btf::from_bytes(&blob).expect("parse btf"));
let btfs = vec![btf];
let index = build_fwd_index(&btfs);
assert!(
index.is_empty(),
"anonymous structs must not be indexed: {index:?}"
);
}
#[test]
fn build_fwd_index_skips_fwd_when_complete_body_in_later_btf() {
let mut strings_0 = vec![0u8];
let n_int_0 = push_btf_name(&mut strings_0, "u64");
let n_shared_0 = push_btf_name(&mut strings_0, "shared");
let types_0 = vec![
SynKind::Int {
name_off: n_int_0,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynKind::Fwd {
name_off: n_shared_0,
kind_flag: 0,
},
];
let blob_0 = build_btf_full(&types_0, &strings_0);
let btf_0 = Arc::new(Btf::from_bytes(&blob_0).expect("parse btf 0"));
let mut strings_1 = vec![0u8];
let n_int_1 = push_btf_name(&mut strings_1, "u64");
let n_shared_1 = push_btf_name(&mut strings_1, "shared");
let n_v_1 = push_btf_name(&mut strings_1, "v");
let types_1 = vec![
SynKind::Int {
name_off: n_int_1,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynKind::Struct {
name_off: n_shared_1,
size: 8,
members: vec![SynMember {
name_off: n_v_1,
type_id: 1,
byte_offset: 0,
}],
},
];
let blob_1 = build_btf_full(&types_1, &strings_1);
let btf_1 = Arc::new(Btf::from_bytes(&blob_1).expect("parse btf 1"));
let ty_0_id_2 = btf_0
.resolve_type_by_id(2)
.expect("BTF #0 id 2 must resolve");
assert!(
matches!(ty_0_id_2, Type::Fwd(_)),
"BTF #0 id 2 must be Fwd, got {ty_0_id_2:?}"
);
let btfs = vec![btf_0, btf_1];
let index = build_fwd_index(&btfs);
assert_eq!(
index.get("shared"),
Some(&FwdIndexEntry {
btfs_idx: 1,
type_id: 2,
}),
"Fwd in BTF #0 must not register; complete body in BTF #1 wins: {index:?}"
);
assert_eq!(
index.len(),
1,
"only the BTF #1 complete body should be indexed: {index:?}"
);
}
#[test]
fn build_fwd_index_handles_empty_name_fwd_without_panic() {
let mut strings = vec![0u8];
let n_int = push_btf_name(&mut strings, "u64");
let n_named = push_btf_name(&mut strings, "named");
let n_x = push_btf_name(&mut strings, "x");
let types = vec![
SynKind::Int {
name_off: n_int,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynKind::Fwd {
name_off: 0,
kind_flag: 0,
},
SynKind::Struct {
name_off: n_named,
size: 8,
members: vec![SynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
];
let blob = build_btf_full(&types, &strings);
let btf = Arc::new(Btf::from_bytes(&blob).expect("parse btf"));
let ty_id_2 = btf.resolve_type_by_id(2).expect("BTF id 2 must resolve");
assert!(
matches!(ty_id_2, Type::Fwd(_)),
"BTF id 2 must be Fwd, got {ty_id_2:?}"
);
let btfs = vec![btf];
let index = build_fwd_index(&btfs);
assert!(
!index.contains_key(""),
"empty-string key must not appear (anonymous Fwd): {index:?}"
);
assert_eq!(
index.get("named"),
Some(&FwdIndexEntry {
btfs_idx: 0,
type_id: 3,
}),
"named struct at id 3 must register after the empty-named Fwd at id 2: {index:?}"
);
assert_eq!(
index.len(),
1,
"only the named struct should be indexed: {index:?}"
);
}
#[test]
fn build_cast_analysis_indexes_cross_object_struct_body() {
let mut strings_a = vec![0u8];
let n_int_a = push_btf_name(&mut strings_a, "u64");
let n_cgx_a = push_btf_name(&mut strings_a, "cgx_target");
let n_t_a = push_btf_name(&mut strings_a, "outer_a");
let n_field_a = push_btf_name(&mut strings_a, "ptr_to_target");
let n_func_a = push_btf_name(&mut strings_a, "func_a");
let n_text_a = push_btf_name(&mut strings_a, ".text");
let types_a = vec![
SynKind::Int {
name_off: n_int_a,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynKind::Struct {
name_off: n_t_a,
size: 8,
members: vec![SynMember {
name_off: n_field_a,
type_id: 1,
byte_offset: 0,
}],
},
SynKind::FuncProto {
return_type_id: 0,
params: vec![SynParam {
name_off: 0,
type_id: 1,
}],
},
SynKind::Func {
name_off: n_func_a,
type_id: 3,
linkage: 1,
},
];
let _ = n_cgx_a; let btf_blob_a = build_btf_full(&types_a, &strings_a);
let insns_a = vec![exit_insn()];
let text_a = insns_to_text_bytes(&insns_a);
let btf_ext_a = build_btf_ext(n_text_a, &[(0, 3)], 8);
let inner_a = build_full_bpf_object_elf(text_a, btf_blob_a, btf_ext_a);
let mut strings_b = vec![0u8];
let n_int_b = push_btf_name(&mut strings_b, "u64");
let n_cgx_b = push_btf_name(&mut strings_b, "cgx_target");
let n_marker_b = push_btf_name(&mut strings_b, "marker");
let n_func_b = push_btf_name(&mut strings_b, "func_b");
let n_text_b = push_btf_name(&mut strings_b, ".text");
let types_b = vec![
SynKind::Int {
name_off: n_int_b,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynKind::Struct {
name_off: n_cgx_b,
size: 8,
members: vec![SynMember {
name_off: n_marker_b,
type_id: 1,
byte_offset: 0,
}],
},
SynKind::FuncProto {
return_type_id: 0,
params: vec![SynParam {
name_off: 0,
type_id: 1,
}],
},
SynKind::Func {
name_off: n_func_b,
type_id: 3,
linkage: 1,
},
];
let btf_blob_b = build_btf_full(&types_b, &strings_b);
let insns_b = vec![exit_insn()];
let text_b = insns_to_text_bytes(&insns_b);
let btf_ext_b = build_btf_ext(n_text_b, &[(0, 3)], 8);
let inner_b = build_full_bpf_object_elf(text_b, btf_blob_b, btf_ext_b);
let strtab = b"\0obj_a\0obj_b\0".to_vec();
let mut symtab = Vec::new();
symtab.extend_from_slice(&elf64_sym(0, 0, 0, 0, 0));
symtab.extend_from_slice(&elf64_sym(
1,
st_info(syms::STB_GLOBAL, syms::STT_OBJECT),
1, 0,
inner_a.len() as u64,
));
symtab.extend_from_slice(&elf64_sym(
7,
st_info(syms::STB_GLOBAL, syms::STT_OBJECT),
1,
inner_a.len() as u64,
inner_b.len() as u64,
));
let mut bpf_objs_data = Vec::new();
bpf_objs_data.extend_from_slice(&inner_a);
bpf_objs_data.extend_from_slice(&inner_b);
let outer = build_elf64(
vec![
SecSpec::new(".bpf.objs", sh::SHT_PROGBITS).data(bpf_objs_data),
SecSpec::new(".strtab", sh::SHT_STRTAB).data(strtab),
SecSpec::new(".symtab", sh::SHT_SYMTAB)
.data(symtab)
.link(2)
.entsize(24),
],
h::EM_X86_64,
h::ET_REL,
);
let out = build_cast_analysis_from_bytes(&outer);
assert_eq!(
out.btfs.len(),
2,
"both embedded objects' BTFs must be retained: {}",
out.btfs.len()
);
let cgx_hit = out.fwd_index.get("cgx_target");
assert_eq!(
cgx_hit,
Some(&FwdIndexEntry {
btfs_idx: 1,
type_id: 2,
}),
"cross-BTF index must point cgx_target to BTF #1 at type id 2: {:?}",
out.fwd_index
);
assert_eq!(
out.fwd_index.get("outer_a"),
Some(&FwdIndexEntry {
btfs_idx: 0,
type_id: 2,
}),
"object A's struct outer_a must be indexed in BTF #0 at id 2"
);
}
#[test]
fn lazy_cast_map_get_full_returns_none_when_no_scheduler() {
let lazy = LazyCastMap::new(None);
assert!(
lazy.get_full().is_none(),
"no-scheduler builder must short-circuit `.get_full()` to None",
);
}
#[test]
fn cached_cast_analysis_concurrent_callers_share_one_oncelock_init() {
use std::sync::{Arc as StdArc, Barrier};
let blob = build_recovers_arena_cast_outer_elf();
let dir = tempfile::tempdir().expect("tempdir");
let p = dir.path().join("concurrent.bin");
std::fs::write(&p, &blob).expect("write");
const N_THREADS: usize = 8;
let barrier = StdArc::new(Barrier::new(N_THREADS));
let path = p.clone();
let results: Vec<Arc<CastAnalysisOutput>> = std::thread::scope(|s| {
let handles: Vec<_> = (0..N_THREADS)
.map(|_| {
let barrier = barrier.clone();
let path = path.clone();
s.spawn(move || {
barrier.wait();
cached_cast_analysis_for_scheduler(&path)
.expect("non-empty fixture must produce Some")
})
})
.collect();
handles.into_iter().map(|h| h.join().unwrap()).collect()
});
assert_eq!(results.len(), N_THREADS);
let first = &results[0];
for (i, other) in results.iter().enumerate().skip(1) {
assert!(
Arc::ptr_eq(first, other),
"thread {i}: Arc must be pointer-equal to thread 0's; \
OnceLock dedup did NOT fire across concurrent callers",
);
}
}
#[test]
fn recover_alloc_size_adjacent_mov_returns_imm() {
let text = vec![BpfInsn::new(MOV_R1_CODE, 1, 0, 0, 4096), call_insn()];
assert_eq!(
super::super::recover_alloc_size_from_r1(&text, 1),
Some(4096)
);
}
#[test]
fn recover_alloc_size_stops_at_call_clobber_returns_none() {
let text = vec![
BpfInsn::new(MOV_R1_CODE, 1, 0, 0, 4096),
call_insn(), call_insn(), ];
assert_eq!(super::super::recover_alloc_size_from_r1(&text, 2), None);
}
#[test]
fn recover_alloc_size_stops_at_alu_clobber_returns_none() {
let text = vec![
BpfInsn::new(MOV_R1_CODE, 1, 0, 0, 4096),
BpfInsn::new(0x07, 1, 0, 0, 0),
call_insn(),
];
assert_eq!(super::super::recover_alloc_size_from_r1(&text, 2), None);
}
#[test]
fn recover_alloc_size_stops_at_atomic_fetch_clobber_returns_none() {
let text = vec![
BpfInsn::new(MOV_R1_CODE, 1, 0, 0, 4096),
BpfInsn::new(0xdb, 10, 1, -8, 0xe1),
call_insn(),
];
assert_eq!(super::super::recover_alloc_size_from_r1(&text, 2), None);
}
#[test]
fn recover_alloc_size_no_mov_returns_none() {
let text = vec![
BpfInsn::new(MOV_R1_CODE, 2, 0, 0, 4096), call_insn(),
];
assert_eq!(super::super::recover_alloc_size_from_r1(&text, 1), None);
}
#[test]
fn recover_alloc_size_call_pc_zero_returns_none() {
let text = vec![BpfInsn::new(MOV_R1_CODE, 1, 0, 0, 4096)];
assert_eq!(super::super::recover_alloc_size_from_r1(&text, 0), None);
}
#[test]
fn recover_alloc_size_lookback_window_boundary() {
let lb = super::super::ALLOC_SIZE_LOOKBACK;
let mov_r2 = || BpfInsn::new(MOV_R1_CODE, 2, 0, 0, 0);
let mut text = vec![BpfInsn::new(MOV_R1_CODE, 1, 0, 0, 4096)];
for _ in 0..(lb - 1) {
text.push(mov_r2());
}
text.push(call_insn());
assert_eq!(
super::super::recover_alloc_size_from_r1(&text, lb),
Some(4096)
);
let mut text = vec![BpfInsn::new(MOV_R1_CODE, 1, 0, 0, 4096)];
for _ in 0..lb {
text.push(mov_r2());
}
text.push(call_insn());
assert_eq!(
super::super::recover_alloc_size_from_r1(&text, lb + 1),
None
);
}