use super::super::*;
use super::*;
use crate::monitor::cast_analysis::{AddrSpace, CastHit};
use goblin::elf::header as h;
use goblin::elf::section_header as sh;
use goblin::elf::sym as syms;
#[test]
fn cached_cast_analysis_nonexistent_path_returns_none() {
let p = std::path::Path::new("/tmp/ktstr-cast-analysis-nonexistent-fixture-path-do-not-create");
assert!(
!p.exists(),
"fixture path must not exist; remove it before running this test"
);
assert!(cached_cast_analysis_for_scheduler(p).is_none());
}
#[test]
fn cached_cast_analysis_empty_file_returns_none() {
let dir = tempfile::tempdir().expect("tempdir");
let p = dir.path().join("empty.bin");
std::fs::write(&p, b"").expect("write empty file");
assert!(cached_cast_analysis_for_scheduler(&p).is_none());
}
#[test]
fn cached_cast_analysis_no_bpf_objs_section_returns_none() {
let blob = build_elf64(
vec![SecSpec::new(".text", sh::SHT_PROGBITS).flags(sh::SHF_EXECINSTR.into())],
h::EM_X86_64,
h::ET_REL,
);
let dir = tempfile::tempdir().expect("tempdir");
let p = dir.path().join("no_bpf_objs.elf");
std::fs::write(&p, &blob).expect("write");
assert!(cached_cast_analysis_for_scheduler(&p).is_none());
}
#[test]
fn btf_str_at_empty_returns_none() {
assert!(btf_str_at(&[], 0).is_none());
assert!(btf_str_at(&[0u8; 23], 0).is_none());
}
#[test]
fn btf_str_at_offset_past_strtab_returns_none() {
let strings = b"\0abc\0\0";
let blob = build_btf_blob(&[], strings);
assert!(btf_str_at(&blob, 100).is_none());
}
#[test]
fn btf_str_at_offset_at_boundary_returns_none() {
let strings = b"\0abc\0";
let blob = build_btf_blob(&[], strings);
assert!(btf_str_at(&blob, strings.len() as u32).is_none());
}
#[test]
fn btf_str_at_no_null_terminator_invalid_utf8_returns_none() {
let strings = vec![0u8, 0xff, 0xff];
let blob = build_btf_blob(&[], &strings);
assert!(btf_str_at(&blob, 1).is_none());
}
#[test]
fn btf_str_at_valid_returns_string() {
let strings = b"\0hello\0world\0";
let blob = build_btf_blob(&[], strings);
assert_eq!(btf_str_at(&blob, 1), Some("hello"));
assert_eq!(btf_str_at(&blob, 7), Some("world"));
assert_eq!(btf_str_at(&blob, 0), Some(""));
}
#[test]
fn parse_btf_ext_too_short_returns_empty() {
let btf_bytes = build_btf_blob(&[], b"\0");
let blob = build_elf64(vec![], h::EM_BPF, h::ET_REL);
let elf = goblin::elf::Elf::parse(&blob).unwrap();
let bases = HashMap::new();
for short_len in [0usize, 23] {
let data = vec![0u8; short_len];
let out = parse_btf_ext_func_entries(&data, &btf_bytes, &elf, &bases);
assert!(out.is_empty(), "len={short_len}");
}
}
#[test]
fn parse_btf_ext_wrong_magic_returns_empty() {
let mut data = vec![0u8; 24];
data[0..2].copy_from_slice(&0xDEADu16.to_le_bytes());
let btf_bytes = build_btf_blob(&[], b"\0");
let blob = build_elf64(vec![], h::EM_BPF, h::ET_REL);
let elf = goblin::elf::Elf::parse(&blob).unwrap();
let bases = HashMap::new();
let out = parse_btf_ext_func_entries(&data, &btf_bytes, &elf, &bases);
assert!(out.is_empty());
}
#[test]
fn parse_btf_ext_bad_hdr_len_returns_empty() {
let btf_bytes = build_btf_blob(&[], b"\0");
let blob = build_elf64(vec![], h::EM_BPF, h::ET_REL);
let elf = goblin::elf::Elf::parse(&blob).unwrap();
let bases = HashMap::new();
let mut data = vec![0u8; 24];
data[0..2].copy_from_slice(&0xEB9F_u16.to_le_bytes());
data[4..8].copy_from_slice(&16u32.to_le_bytes());
let out = parse_btf_ext_func_entries(&data, &btf_bytes, &elf, &bases);
assert!(out.is_empty(), "hdr_len=16 should be rejected");
let mut data = vec![0u8; 24];
data[0..2].copy_from_slice(&0xEB9F_u16.to_le_bytes());
data[4..8].copy_from_slice(&1024u32.to_le_bytes());
let out = parse_btf_ext_func_entries(&data, &btf_bytes, &elf, &bases);
assert!(out.is_empty(), "hdr_len > data.len should be rejected");
}
#[test]
fn parse_btf_ext_func_info_window_oob_returns_empty() {
let btf_bytes = build_btf_blob(&[], b"\0");
let blob = build_elf64(vec![], h::EM_BPF, h::ET_REL);
let elf = goblin::elf::Elf::parse(&blob).unwrap();
let bases = HashMap::new();
let mut data = vec![0u8; 32];
data[0..2].copy_from_slice(&0xEB9F_u16.to_le_bytes());
data[4..8].copy_from_slice(&24u32.to_le_bytes()); data[8..12].copy_from_slice(&0u32.to_le_bytes()); data[12..16].copy_from_slice(&10_000u32.to_le_bytes()); let out = parse_btf_ext_func_entries(&data, &btf_bytes, &elf, &bases);
assert!(out.is_empty());
}
#[test]
fn parse_btf_ext_record_size_too_small_returns_empty() {
let btf_bytes = build_btf_blob(&[], b"\0");
let blob = build_elf64(vec![], h::EM_BPF, h::ET_REL);
let elf = goblin::elf::Elf::parse(&blob).unwrap();
let bases = HashMap::new();
let mut data = vec![0u8; 32];
data[0..2].copy_from_slice(&0xEB9F_u16.to_le_bytes());
data[4..8].copy_from_slice(&24u32.to_le_bytes()); data[8..12].copy_from_slice(&0u32.to_le_bytes()); data[12..16].copy_from_slice(&8u32.to_le_bytes()); data[24..28].copy_from_slice(&4u32.to_le_bytes());
let out = parse_btf_ext_func_entries(&data, &btf_bytes, &elf, &bases);
assert!(out.is_empty());
}
#[test]
fn parse_btf_ext_non_multiple_insn_off_skips_entry() {
let bytes_strs = b"\0txt\0";
let btf_bytes = build_btf_blob(&[], bytes_strs);
let inner = build_elf64(
vec![SecSpec::new("txt", sh::SHT_PROGBITS).flags(sh::SHF_EXECINSTR.into())],
h::EM_BPF,
h::ET_REL,
);
let elf = goblin::elf::Elf::parse(&inner).unwrap();
let mut bases: HashMap<u32, usize> = HashMap::new();
bases.insert(1, 0);
let mut data = Vec::new();
data.extend_from_slice(&0xEB9F_u16.to_le_bytes()); data.push(1); data.push(0); data.extend_from_slice(&24u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&28u32.to_le_bytes()); data.extend_from_slice(&28u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&8u32.to_le_bytes()); data.extend_from_slice(&1u32.to_le_bytes()); data.extend_from_slice(&2u32.to_le_bytes()); data.extend_from_slice(&8u32.to_le_bytes());
data.extend_from_slice(&42u32.to_le_bytes()); data.extend_from_slice(&12u32.to_le_bytes());
data.extend_from_slice(&99u32.to_le_bytes()); let out = parse_btf_ext_func_entries(&data, &btf_bytes, &elf, &bases);
assert_eq!(out.len(), 1, "got {out:?}");
assert_eq!(out[0].insn_offset, 1);
assert_eq!(out[0].func_proto_id, 42);
}
#[test]
fn iter_embedded_bpf_objects_no_symbols_falls_back_to_full_section() {
let payload = b"DUMMY_BPF_OBJ_BYTES".to_vec();
let payload_len = payload.len();
let blob = build_elf64(
vec![SecSpec::new(".bpf.objs", sh::SHT_PROGBITS).data(payload)],
h::EM_X86_64,
h::ET_REL,
);
let elf = goblin::elf::Elf::parse(&blob).unwrap();
let bpf_objs_idx = find_section(&elf, ".bpf.objs").expect(".bpf.objs");
let out = iter_embedded_bpf_objects(&elf, &blob, bpf_objs_idx);
assert_eq!(out.len(), 1, "expected one fallback slice");
assert_eq!(out[0].len(), payload_len);
assert_eq!(out[0], b"DUMMY_BPF_OBJ_BYTES");
}
#[test]
fn section_data_overflow_returns_none() {
let payload = b"PAYLOAD".to_vec();
let mut blob = build_elf64(
vec![SecSpec::new(".x", sh::SHT_PROGBITS).data(payload)],
h::EM_X86_64,
h::ET_REL,
);
let elf_view = goblin::elf::Elf::parse(&blob).unwrap();
let shoff = elf_view.header.e_shoff as usize;
let shdr1_off = shoff + 64;
blob[shdr1_off + 24..shdr1_off + 32].copy_from_slice(&u64::MAX.to_le_bytes());
blob[shdr1_off + 32..shdr1_off + 40].copy_from_slice(&u64::MAX.to_le_bytes());
let elf = goblin::elf::Elf::parse(&blob).unwrap();
let idx = find_section(&elf, ".x").expect(".x");
assert!(section_data(&elf, &blob, idx).is_none());
}
#[test]
fn smoke_symtab_helpers_compile() {
let strtab = b"\0bpf_obj\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, 8, ));
let blob = build_elf64(
vec![
SecSpec::new(".bpf.objs", sh::SHT_PROGBITS).data(vec![0u8; 8]),
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 _ = goblin::elf::Elf::parse(&blob).expect("parse");
}
#[test]
fn find_section_locates_named_section() {
let blob = build_elf64(
vec![
SecSpec::new(".text", sh::SHT_PROGBITS).flags(sh::SHF_EXECINSTR.into()),
SecSpec::new(".bpf.objs", sh::SHT_PROGBITS).data(vec![0u8; 4]),
],
h::EM_BPF,
h::ET_REL,
);
let elf = goblin::elf::Elf::parse(&blob).unwrap();
assert_eq!(find_section(&elf, ".text"), Some(1));
assert_eq!(find_section(&elf, ".bpf.objs"), Some(2));
}
#[test]
fn find_section_missing_returns_none() {
let blob = build_elf64(
vec![SecSpec::new(".text", sh::SHT_PROGBITS).flags(sh::SHF_EXECINSTR.into())],
h::EM_BPF,
h::ET_REL,
);
let elf = goblin::elf::Elf::parse(&blob).unwrap();
assert_eq!(find_section(&elf, ".nope"), None);
}
#[test]
fn section_data_returns_section_bytes() {
let payload = b"section-bytes-payload-12345".to_vec();
let payload_len = payload.len();
let blob = build_elf64(
vec![SecSpec::new(".x", sh::SHT_PROGBITS).data(payload)],
h::EM_BPF,
h::ET_REL,
);
let elf = goblin::elf::Elf::parse(&blob).unwrap();
let idx = find_section(&elf, ".x").unwrap();
let bytes = section_data(&elf, &blob, idx).expect("payload slice");
assert_eq!(bytes.len(), payload_len);
assert_eq!(bytes, &b"section-bytes-payload-12345"[..]);
}
#[test]
fn section_data_out_of_range_returns_none() {
let blob = build_elf64(
vec![SecSpec::new(".text", sh::SHT_PROGBITS)],
h::EM_BPF,
h::ET_REL,
);
let elf = goblin::elf::Elf::parse(&blob).unwrap();
assert!(section_data(&elf, &blob, 9999).is_none());
}
#[test]
fn iter_embedded_bpf_objects_uses_object_symbol() {
let payload: Vec<u8> = (0..32u8).collect();
let strtab = b"\0bpf_obj\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, 4, 24, ));
let blob = build_elf64(
vec![
SecSpec::new(".bpf.objs", sh::SHT_PROGBITS).data(payload),
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 elf = goblin::elf::Elf::parse(&blob).unwrap();
let bpf_objs_idx = find_section(&elf, ".bpf.objs").unwrap();
let out = iter_embedded_bpf_objects(&elf, &blob, bpf_objs_idx);
assert_eq!(out.len(), 1);
assert_eq!(out[0].len(), 24);
let expected: Vec<u8> = (4..28u8).collect();
assert_eq!(out[0], expected.as_slice());
}
#[test]
fn iter_embedded_bpf_objects_rejects_oversized_symbol() {
let payload = b"0123456789abcdef".to_vec(); let payload_len = payload.len();
let strtab = b"\0bpf_obj\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,
200,
));
let blob = build_elf64(
vec![
SecSpec::new(".bpf.objs", sh::SHT_PROGBITS).data(payload),
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 elf = goblin::elf::Elf::parse(&blob).unwrap();
let bpf_objs_idx = find_section(&elf, ".bpf.objs").unwrap();
let out = iter_embedded_bpf_objects(&elf, &blob, bpf_objs_idx);
assert_eq!(out.len(), 1, "fallback yields exactly one slice");
assert_eq!(out[0].len(), payload_len);
}
#[test]
fn iter_embedded_bpf_objects_skips_non_object_symbols() {
let payload = b"hello-bpf-objects".to_vec();
let payload_len = payload.len();
let strtab = b"\0func_sym\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_FUNC),
1,
0,
8,
));
let blob = build_elf64(
vec![
SecSpec::new(".bpf.objs", sh::SHT_PROGBITS).data(payload),
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 elf = goblin::elf::Elf::parse(&blob).unwrap();
let bpf_objs_idx = find_section(&elf, ".bpf.objs").unwrap();
let out = iter_embedded_bpf_objects(&elf, &blob, bpf_objs_idx);
assert_eq!(out.len(), 1);
assert_eq!(out[0].len(), payload_len);
}
#[test]
fn analyze_one_object_corrupt_elf_returns_empty() {
let bytes = vec![0u8; 64]; let (map, btf, _alloc_sizes) = analyze_one_object_with_btf(&bytes);
assert!(map.is_empty());
assert!(btf.is_none());
}
#[test]
fn analyze_one_object_no_btf_returns_empty() {
let bytes = build_elf64(
vec![
SecSpec::new(".text", sh::SHT_PROGBITS)
.flags(sh::SHF_EXECINSTR.into())
.data(vec![0u8; 8]),
],
h::EM_BPF,
h::ET_REL,
);
let (map, btf, _alloc_sizes) = analyze_one_object_with_btf(&bytes);
assert!(map.is_empty());
assert!(btf.is_none());
}
#[test]
fn analyze_one_object_corrupt_btf_returns_empty() {
let bytes = build_elf64(
vec![
SecSpec::new(".text", sh::SHT_PROGBITS)
.flags(sh::SHF_EXECINSTR.into())
.data(insns_to_text_bytes(&[exit_insn()])),
SecSpec::new(".BTF", sh::SHT_PROGBITS).data(vec![0xFFu8; 32]),
],
h::EM_BPF,
h::ET_REL,
);
let (map, btf, _alloc_sizes) = analyze_one_object_with_btf(&bytes);
assert!(map.is_empty());
assert!(btf.is_none());
}
#[test]
fn analyze_one_object_no_text_section_returns_empty() {
let bytes = build_elf64(
vec![SecSpec::new(".BTF", sh::SHT_PROGBITS).data(build_btf_blob(&[], b"\0"))],
h::EM_BPF,
h::ET_REL,
);
let (map, btf, _alloc_sizes) = analyze_one_object_with_btf(&bytes);
assert!(map.is_empty());
assert!(btf.is_some());
}
#[test]
fn analyze_one_object_misaligned_text_skipped() {
let bytes = build_elf64(
vec![
SecSpec::new(".text", sh::SHT_PROGBITS)
.flags(sh::SHF_EXECINSTR.into())
.data(vec![0u8; 7]),
SecSpec::new(".BTF", sh::SHT_PROGBITS).data(build_btf_blob(&[], b"\0")),
],
h::EM_BPF,
h::ET_REL,
);
let (map, btf, _alloc_sizes) = analyze_one_object_with_btf(&bytes);
assert!(map.is_empty());
assert!(btf.is_some());
}
#[test]
fn analyze_one_object_recovers_arena_cast_end_to_end() {
let mut strings = vec![0u8];
let n_int = push_btf_name(&mut strings, "u64");
let n_t = push_btf_name(&mut strings, "T");
let n_q = push_btf_name(&mut strings, "Q");
let n_f = push_btf_name(&mut strings, "f");
let n_x = push_btf_name(&mut strings, "x");
let n_func = push_btf_name(&mut strings, "myfunc");
let n_text = push_btf_name(&mut strings, ".text");
let types = vec![
SynKind::Int {
name_off: n_int,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynKind::Struct {
name_off: n_t,
size: 16,
members: vec![SynMember {
name_off: n_f,
type_id: 1,
byte_offset: 8,
}],
},
SynKind::Struct {
name_off: n_q,
size: 8,
members: vec![SynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
SynKind::Ptr { type_id: 2 },
SynKind::FuncProto {
return_type_id: 0,
params: vec![SynParam {
name_off: 0,
type_id: 4,
}],
},
SynKind::Func {
name_off: n_func,
type_id: 5,
linkage: 1,
},
];
let btf_blob = build_btf_full(&types, &strings);
let insns = vec![
ldx_dw_mem(2, 1, 8),
addr_space_cast_insn(2, 2),
ldx_dw_mem(3, 2, 0),
exit_insn(),
];
let text = insns_to_text_bytes(&insns);
let btf_ext = build_btf_ext(n_text, &[(0, 5)], 8);
let bytes = build_full_bpf_object_elf(text, btf_blob, btf_ext);
let (map, btf, _alloc_sizes) = analyze_one_object_with_btf(&bytes);
assert!(btf.is_some(), "valid BTF must be returned");
let hit = map.get(&(2u32, 8u32)).copied();
assert_eq!(
hit,
Some(CastHit {
alloc_size: None,
target_type_id: 3,
addr_space: AddrSpace::Arena,
}),
"expected arena cast T.f → Q*, got {map:?}"
);
}
#[test]
fn cached_cast_analysis_corrupt_inner_returns_none() {
let outer = build_elf64(
vec![SecSpec::new(".bpf.objs", sh::SHT_PROGBITS).data(b"not-an-elf".to_vec())],
h::EM_X86_64,
h::ET_REL,
);
let dir = tempfile::tempdir().expect("tempdir");
let p = dir.path().join("bad_inner.bin");
std::fs::write(&p, &outer).expect("write");
assert!(cached_cast_analysis_for_scheduler(&p).is_none());
}
#[test]
fn cached_cast_analysis_inner_without_btf_returns_none() {
let inner = build_elf64(
vec![
SecSpec::new(".text", sh::SHT_PROGBITS)
.flags(sh::SHF_EXECINSTR.into())
.data(vec![0u8; 8]),
],
h::EM_BPF,
h::ET_REL,
);
let outer = build_elf64(
vec![SecSpec::new(".bpf.objs", sh::SHT_PROGBITS).data(inner)],
h::EM_X86_64,
h::ET_REL,
);
let dir = tempfile::tempdir().expect("tempdir");
let p = dir.path().join("no_inner_btf.bin");
std::fs::write(&p, &outer).expect("write");
assert!(cached_cast_analysis_for_scheduler(&p).is_none());
}
#[test]
fn cached_cast_analysis_recovers_arena_cast_end_to_end() {
let mut strings = vec![0u8];
let n_int = push_btf_name(&mut strings, "u64");
let n_t = push_btf_name(&mut strings, "T");
let n_q = push_btf_name(&mut strings, "Q");
let n_f = push_btf_name(&mut strings, "f");
let n_x = push_btf_name(&mut strings, "x");
let n_func = push_btf_name(&mut strings, "myfunc");
let n_text = push_btf_name(&mut strings, ".text");
let types = vec![
SynKind::Int {
name_off: n_int,
size: 8,
encoding: 0,
offset: 0,
bits: 64,
},
SynKind::Struct {
name_off: n_t,
size: 16,
members: vec![SynMember {
name_off: n_f,
type_id: 1,
byte_offset: 8,
}],
},
SynKind::Struct {
name_off: n_q,
size: 8,
members: vec![SynMember {
name_off: n_x,
type_id: 1,
byte_offset: 0,
}],
},
SynKind::Ptr { type_id: 2 },
SynKind::FuncProto {
return_type_id: 0,
params: vec![SynParam {
name_off: 0,
type_id: 4,
}],
},
SynKind::Func {
name_off: n_func,
type_id: 5,
linkage: 1,
},
];
let btf_blob = build_btf_full(&types, &strings);
let insns = vec![
ldx_dw_mem(2, 1, 8),
addr_space_cast_insn(2, 2),
ldx_dw_mem(3, 2, 0),
exit_insn(),
];
let text = insns_to_text_bytes(&insns);
let btf_ext = build_btf_ext(n_text, &[(0, 5)], 8);
let inner = build_full_bpf_object_elf(text, btf_blob, btf_ext);
let outer = build_elf64(
vec![SecSpec::new(".bpf.objs", sh::SHT_PROGBITS).data(inner)],
h::EM_X86_64,
h::ET_REL,
);
let dir = tempfile::tempdir().expect("tempdir");
let p = dir.path().join("full.bin");
std::fs::write(&p, &outer).expect("write");
let out = cached_cast_analysis_for_scheduler(&p).expect("non-empty fixture must produce Some");
let hit = out.cast_maps[0].get(&(2u32, 8u32)).copied();
assert_eq!(
hit,
Some(CastHit {
alloc_size: None,
target_type_id: 3,
addr_space: AddrSpace::Arena,
}),
"expected arena cast T.f → Q*, got {:?}",
out.cast_maps[0]
);
}
#[test]
fn cached_cast_analysis_returns_same_arc_for_same_content() {
let blob = build_recovers_arena_cast_outer_elf();
let dir = tempfile::tempdir().expect("tempdir");
let p1 = dir.path().join("first.bin");
let p2 = dir.path().join("second.bin");
std::fs::write(&p1, &blob).expect("write 1");
std::fs::write(&p2, &blob).expect("write 2");
let first = cached_cast_analysis_for_scheduler(&p1).expect("Some on non-empty analysis");
let second = cached_cast_analysis_for_scheduler(&p2).expect("cache hit on identical content");
assert!(
Arc::ptr_eq(&first, &second),
"expected pointer-equal Arc when two paths have identical content"
);
assert_eq!(
first.cast_maps[0].get(&(2u32, 8u32)).copied(),
Some(CastHit {
alloc_size: None,
target_type_id: 3,
addr_space: AddrSpace::Arena,
}),
);
}
#[test]
fn cached_cast_analysis_collapses_empty_to_none() {
let empty_blob = build_elf64(
vec![SecSpec::new(".text", sh::SHT_PROGBITS).flags(sh::SHF_EXECINSTR.into())],
h::EM_X86_64,
h::ET_REL,
);
let dir = tempfile::tempdir().expect("tempdir");
let p = dir.path().join("empty.bin");
std::fs::write(&p, &empty_blob).expect("write");
assert!(cached_cast_analysis_for_scheduler(&p).is_none());
assert!(cached_cast_analysis_for_scheduler(&p).is_none());
}
#[test]
fn cached_cast_analysis_read_failure_does_not_pollute_cache() {
let dir = tempfile::tempdir().expect("tempdir");
let p = dir.path().join("appears_later.bin");
assert!(!p.exists());
assert!(cached_cast_analysis_for_scheduler(&p).is_none());
let blob = build_recovers_arena_cast_outer_elf();
std::fs::write(&p, &blob).expect("write");
let out = cached_cast_analysis_for_scheduler(&p)
.expect("post-creation read should succeed and produce a non-empty CastAnalysisOutput");
assert_eq!(
out.cast_maps[0].get(&(2u32, 8u32)).copied(),
Some(CastHit {
alloc_size: None,
target_type_id: 3,
addr_space: AddrSpace::Arena,
}),
"post-creation analysis should recover the seeded cast"
);
}
#[test]
fn lazy_cast_map_get_full_is_idempotent_and_lazy() {
let blob = build_recovers_arena_cast_outer_elf();
let dir = tempfile::tempdir().expect("tempdir");
let p = dir.path().join("lazy.bin");
std::fs::write(&p, &blob).expect("write");
let lazy = LazyCastMap::new(Some(p.clone()));
assert!(
lazy.inner.get().is_none(),
"LazyCastMap::new must not run analysis"
);
let first = lazy.get_full().expect("non-empty result");
let second = lazy.get_full().expect("non-empty result");
assert!(
Arc::ptr_eq(&first, &second),
"OnceLock-backed `.get_full()` must return the same Arc on every call"
);
}
#[test]
fn lazy_cast_map_get_full_returns_none_for_no_findings() {
let empty_blob = build_elf64(
vec![SecSpec::new(".text", sh::SHT_PROGBITS).flags(sh::SHF_EXECINSTR.into())],
h::EM_X86_64,
h::ET_REL,
);
let dir = tempfile::tempdir().expect("tempdir");
let p = dir.path().join("no_findings.bin");
std::fs::write(&p, &empty_blob).expect("write");
let lazy = LazyCastMap::new(Some(p));
assert!(
lazy.get_full().is_none(),
"no-`.bpf.objs` binary must collapse to None on `.get_full()`"
);
}
#[test]
fn objects_with_casts_counts_only_cast_bearing_objects() {
let empty = Arc::new(CastMap::new());
let mut m = CastMap::new();
m.insert(
(1, 0),
CastHit {
target_type_id: 2,
addr_space: AddrSpace::Arena,
alloc_size: None,
},
);
let with_cast = Arc::new(m);
assert_eq!(objects_with_casts(&[]), 0);
assert_eq!(objects_with_casts(std::slice::from_ref(&empty)), 0);
assert_eq!(objects_with_casts(std::slice::from_ref(&with_cast)), 1);
assert_eq!(
objects_with_casts(&[with_cast.clone(), empty.clone()]),
1,
"empty objects do not count toward the multi-object gate"
);
assert_eq!(
objects_with_casts(&[with_cast.clone(), with_cast.clone()]),
2,
"two cast-bearing objects is the case the guard fires on"
);
}
#[test]
fn parse_btf_ext_records_produce_func_entries() {
let mut strings = vec![0u8];
let n_text = push_btf_name(&mut strings, ".text");
let btf_blob = build_btf_full(&[], &strings);
let inner = build_elf64(
vec![
SecSpec::new(".text", sh::SHT_PROGBITS)
.flags(sh::SHF_EXECINSTR.into())
.data(vec![0u8; 32]),
],
h::EM_BPF,
h::ET_REL,
);
let elf = goblin::elf::Elf::parse(&inner).unwrap();
let text_idx = find_section(&elf, ".text").expect(".text") as u32;
let mut bases: HashMap<u32, usize> = HashMap::new();
bases.insert(text_idx, 0);
let data = build_btf_ext(n_text, &[(0, 11), (16, 22)], 8);
let out = parse_btf_ext_func_entries(&data, &btf_blob, &elf, &bases);
assert_eq!(out.len(), 2, "got {out:?}");
assert_eq!(out[0].insn_offset, 0);
assert_eq!(out[0].func_proto_id, 11);
assert_eq!(out[1].insn_offset, 2);
assert_eq!(out[1].func_proto_id, 22);
}
#[test]
fn parse_btf_ext_applies_section_base_offset() {
let mut strings = vec![0u8];
let n_text = push_btf_name(&mut strings, ".text");
let btf_blob = build_btf_full(&[], &strings);
let inner = build_elf64(
vec![
SecSpec::new(".text", sh::SHT_PROGBITS)
.flags(sh::SHF_EXECINSTR.into())
.data(vec![0u8; 32]),
],
h::EM_BPF,
h::ET_REL,
);
let elf = goblin::elf::Elf::parse(&inner).unwrap();
let text_idx = find_section(&elf, ".text").expect(".text") as u32;
let mut bases: HashMap<u32, usize> = HashMap::new();
bases.insert(text_idx, 10);
let data = build_btf_ext(n_text, &[(16, 5)], 8);
let out = parse_btf_ext_func_entries(&data, &btf_blob, &elf, &bases);
assert_eq!(out.len(), 1);
assert_eq!(out[0].insn_offset, 12);
assert_eq!(out[0].func_proto_id, 5);
}
#[test]
fn parse_btf_ext_handles_padded_records() {
let mut strings = vec![0u8];
let n_text = push_btf_name(&mut strings, ".text");
let btf_blob = build_btf_full(&[], &strings);
let inner = build_elf64(
vec![
SecSpec::new(".text", sh::SHT_PROGBITS)
.flags(sh::SHF_EXECINSTR.into())
.data(vec![0u8; 32]),
],
h::EM_BPF,
h::ET_REL,
);
let elf = goblin::elf::Elf::parse(&inner).unwrap();
let text_idx = find_section(&elf, ".text").expect(".text") as u32;
let mut bases: HashMap<u32, usize> = HashMap::new();
bases.insert(text_idx, 0);
let data = build_btf_ext(n_text, &[(0, 11), (8, 22)], 16);
let out = parse_btf_ext_func_entries(&data, &btf_blob, &elf, &bases);
assert_eq!(out.len(), 2);
assert_eq!(out[0].insn_offset, 0);
assert_eq!(out[0].func_proto_id, 11);
assert_eq!(out[1].insn_offset, 1);
assert_eq!(out[1].func_proto_id, 22);
}
#[test]
fn parse_btf_ext_skips_unresolvable_section_name() {
let strings = vec![0u8];
let btf_blob = build_btf_full(&[], &strings);
let inner = build_elf64(
vec![
SecSpec::new(".text", sh::SHT_PROGBITS)
.flags(sh::SHF_EXECINSTR.into())
.data(vec![0u8; 32]),
],
h::EM_BPF,
h::ET_REL,
);
let elf = goblin::elf::Elf::parse(&inner).unwrap();
let bases: HashMap<u32, usize> = HashMap::new();
let data = build_btf_ext(999, &[(0, 7)], 8);
let out = parse_btf_ext_func_entries(&data, &btf_blob, &elf, &bases);
assert!(out.is_empty());
}
#[test]
fn parse_btf_ext_skips_section_not_in_elf() {
let mut strings = vec![0u8];
let n_other = push_btf_name(&mut strings, ".not_in_elf");
let btf_blob = build_btf_full(&[], &strings);
let inner = build_elf64(
vec![
SecSpec::new(".text", sh::SHT_PROGBITS)
.flags(sh::SHF_EXECINSTR.into())
.data(vec![0u8; 32]),
],
h::EM_BPF,
h::ET_REL,
);
let elf = goblin::elf::Elf::parse(&inner).unwrap();
let bases: HashMap<u32, usize> = HashMap::new();
let data = build_btf_ext(n_other, &[(0, 7)], 8);
let out = parse_btf_ext_func_entries(&data, &btf_blob, &elf, &bases);
assert!(out.is_empty());
}
#[test]
fn parse_btf_ext_skips_section_without_base() {
let mut strings = vec![0u8];
let n_text = push_btf_name(&mut strings, ".text");
let btf_blob = build_btf_full(&[], &strings);
let inner = build_elf64(
vec![
SecSpec::new(".text", sh::SHT_PROGBITS)
.flags(sh::SHF_EXECINSTR.into())
.data(vec![0u8; 32]),
],
h::EM_BPF,
h::ET_REL,
);
let elf = goblin::elf::Elf::parse(&inner).unwrap();
let bases: HashMap<u32, usize> = HashMap::new();
let data = build_btf_ext(n_text, &[(0, 7)], 8);
let out = parse_btf_ext_func_entries(&data, &btf_blob, &elf, &bases);
assert!(out.is_empty());
}
#[test]
fn parse_btf_ext_zero_func_info_len_returns_empty() {
let btf_blob = build_btf_full(&[], b"\0");
let inner = build_elf64(vec![], h::EM_BPF, h::ET_REL);
let elf = goblin::elf::Elf::parse(&inner).unwrap();
let bases = HashMap::new();
let mut data = vec![0u8; 24];
data[0..2].copy_from_slice(&0xEB9F_u16.to_le_bytes());
data[4..8].copy_from_slice(&24u32.to_le_bytes());
let out = parse_btf_ext_func_entries(&data, &btf_blob, &elf, &bases);
assert!(out.is_empty());
}