use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, Mutex, OnceLock};
use crate::monitor::cast_analysis::{
BPF_PSEUDO_CALL, BPF_PSEUDO_KFUNC_CALL, BpfInsn, CastMap, DatasecPointer, FuncEntry,
SubprogReturn, analyze_casts,
};
use btf_rs::{Btf, Type};
const BPF_INSN_SIZE: usize = 8;
fn btf_str_at(btf_bytes: &[u8], str_off: u32) -> Option<&str> {
if btf_bytes.len() < 24 {
return None;
}
let hdr_len = u32::from_le_bytes(btf_bytes[4..8].try_into().ok()?) as usize;
let str_section_off = u32::from_le_bytes(btf_bytes[16..20].try_into().ok()?) as usize;
let str_section_len = u32::from_le_bytes(btf_bytes[20..24].try_into().ok()?) as usize;
let str_start = hdr_len + str_section_off;
let off = str_off as usize;
if off >= str_section_len {
return None;
}
let base = str_start + off;
if base >= btf_bytes.len() {
return None;
}
let strtab_end = (str_start + str_section_len).min(btf_bytes.len());
if base >= strtab_end {
return None;
}
let end = btf_bytes[base..strtab_end]
.iter()
.position(|&b| b == 0)
.map(|p| base + p)
.unwrap_or(strtab_end);
std::str::from_utf8(&btf_bytes[base..end]).ok()
}
const BTF_MAGIC: u16 = 0xEB9F;
const BTF_EXT_HEADER_MIN_LEN: u32 = 24;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct FwdIndexEntry {
pub(crate) btfs_idx: usize,
pub(crate) type_id: u32,
}
pub(crate) struct CastAnalysisOutput {
pub(crate) cast_maps: Vec<Arc<CastMap>>,
pub(crate) btfs: Vec<Arc<Btf>>,
pub(crate) fwd_index: HashMap<String, FwdIndexEntry>,
pub(crate) alloc_size_types: Vec<(u64, String)>,
}
pub(crate) struct LazyCastMap {
scheduler_binary: Option<std::path::PathBuf>,
inner: OnceLock<Option<Arc<CastAnalysisOutput>>>,
}
impl LazyCastMap {
pub(crate) fn new(scheduler_binary: Option<std::path::PathBuf>) -> Self {
Self {
scheduler_binary,
inner: OnceLock::new(),
}
}
pub(crate) fn get_full(&self) -> Option<Arc<CastAnalysisOutput>> {
self.inner
.get_or_init(|| {
self.scheduler_binary
.as_deref()
.and_then(cached_cast_analysis_for_scheduler)
})
.clone()
}
}
type CastCacheEntry = Arc<OnceLock<Option<Arc<CastAnalysisOutput>>>>;
fn cast_cache() -> &'static Mutex<HashMap<u64, CastCacheEntry>> {
static CACHE: OnceLock<Mutex<HashMap<u64, CastCacheEntry>>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
fn ahash_bytes(bytes: &[u8]) -> u64 {
use std::hash::{BuildHasher, Hasher};
let mut hasher = ahash::RandomState::with_seeds(0, 0, 0, 0).build_hasher();
hasher.write(bytes);
hasher.finish()
}
pub(crate) fn cached_cast_analysis_for_scheduler(path: &Path) -> Option<Arc<CastAnalysisOutput>> {
let bytes = match std::fs::read(path) {
Ok(b) => b,
Err(e) => {
tracing::warn!(
error = %e,
path = %path.display(),
"cast_analysis: read scheduler binary failed; \
dump renderer will fall back to plain u64 counters"
);
return None;
}
};
let hash_t0 = std::time::Instant::now();
let hash = ahash_bytes(&bytes);
tracing::debug!(
elapsed_us = hash_t0.elapsed().as_micros() as u64,
len = bytes.len(),
hash = format_args!("{hash:016x}"),
"cast_analysis: scheduler binary content hash finished"
);
let entry: CastCacheEntry = {
let mut cache = cast_cache().lock().unwrap();
cache
.entry(hash)
.or_insert_with(|| Arc::new(OnceLock::new()))
.clone()
};
entry
.get_or_init(|| {
let btfs = parse_btfs_from_bytes(&bytes);
if let Some((cast_map, fwd_index, alloc_size_types)) =
persist::try_load(hash, btfs.len())
{
tracing::debug!("cast_analysis: disk cache hit");
let out = CastAnalysisOutput {
cast_maps: vec![Arc::new(cast_map)],
btfs,
fwd_index,
alloc_size_types,
};
let total: usize = out.cast_maps.iter().map(|m| m.len()).sum();
return if total == 0 && out.fwd_index.is_empty() {
None
} else {
Some(Arc::new(out))
};
}
let analyze_t0 = std::time::Instant::now();
let out = build_cast_analysis_from_bytes(&bytes);
tracing::debug!(
elapsed_ms = analyze_t0.elapsed().as_millis() as u64,
casts = out.cast_maps.iter().map(|m| m.len()).sum::<usize>(),
btfs = out.btfs.len(),
fwd_index = out.fwd_index.len(),
"cast_analysis: on-demand analysis finished"
);
let merged_for_cache: CastMap = out
.cast_maps
.iter()
.flat_map(|m| m.iter())
.map(|(&k, &v)| (k, v))
.collect();
persist::try_save(
hash,
&merged_for_cache,
&out.fwd_index,
out.btfs.len(),
&out.alloc_size_types,
);
let total_casts: usize = out.cast_maps.iter().map(|m| m.len()).sum();
if total_casts == 0 && out.fwd_index.is_empty() {
None
} else {
Some(Arc::new(out))
}
})
.clone()
}
pub(crate) fn build_cast_analysis_from_bytes(bytes: &[u8]) -> CastAnalysisOutput {
let parse_t0 = std::time::Instant::now();
let outer = match goblin::elf::Elf::parse(bytes) {
Ok(e) => e,
Err(e) => {
tracing::warn!(
error = %e,
"cast_analysis: parse outer ELF failed; \
dump renderer will fall back to plain u64 counters"
);
return CastAnalysisOutput {
cast_maps: vec![Arc::new(CastMap::new())],
btfs: Vec::new(),
fwd_index: HashMap::new(),
alloc_size_types: Vec::new(),
};
}
};
let bpf_objs_section = match find_section(&outer, ".bpf.objs") {
Some(s) => s,
None => {
tracing::debug!(
"cast_analysis: scheduler binary has no .bpf.objs section; \
typed-pointer rendering disabled"
);
return CastAnalysisOutput {
cast_maps: vec![Arc::new(CastMap::new())],
btfs: Vec::new(),
fwd_index: HashMap::new(),
alloc_size_types: Vec::new(),
};
}
};
tracing::debug!(
elapsed_us = parse_t0.elapsed().as_micros() as u64,
"cast_analysis: outer ELF parse + .bpf.objs lookup finished"
);
let mut cast_maps: Vec<Arc<CastMap>> = Vec::new();
let mut btfs: Vec<Arc<Btf>> = Vec::new();
let mut all_alloc_sizes: Vec<u64> = Vec::new();
let started = std::time::Instant::now();
tracing::debug!("cast_analysis: starting analyze_casts pipeline");
for inner in iter_embedded_bpf_objects(&outer, bytes, bpf_objs_section) {
let one_t0 = std::time::Instant::now();
let (one, btf_for_obj, obj_alloc_sizes) = analyze_one_object_with_btf(inner);
tracing::debug!(
elapsed_ms = one_t0.elapsed().as_millis() as u64,
casts = one.len(),
"cast_analysis: analyze_one_object_with_btf finished"
);
cast_maps.push(Arc::new(one));
all_alloc_sizes.extend_from_slice(&obj_alloc_sizes);
if let Some(btf) = btf_for_obj {
btfs.push(btf);
}
}
let total_casts: usize = cast_maps.iter().map(|m| m.len()).sum();
tracing::debug!(
elapsed_ms = started.elapsed().as_millis() as u64,
casts = total_casts,
btfs = btfs.len(),
objects = cast_maps.len(),
"cast_analysis: analyze_casts pipeline finished"
);
let fwd_t0 = std::time::Instant::now();
let fwd_index = build_fwd_index(&btfs);
tracing::debug!(
elapsed_us = fwd_t0.elapsed().as_micros() as u64,
entries = fwd_index.len(),
"cast_analysis: build_fwd_index finished"
);
if total_casts == 0 {
tracing::debug!(
casts = 0,
"cast_analysis: recovered 0 typed pointers from scheduler"
);
} else {
tracing::info!(
casts = total_casts,
"cast_analysis: recovered typed pointers from scheduler"
);
}
all_alloc_sizes.sort_unstable();
all_alloc_sizes.dedup();
let mut alloc_size_types: Vec<(u64, String)> = Vec::with_capacity(all_alloc_sizes.len());
let mut seen_names: std::collections::HashSet<String> = std::collections::HashSet::new();
let per_btf_structs: Vec<Vec<(u64, String)>> = btfs
.iter()
.map(|ebtf| enumerate_named_structs(ebtf))
.collect();
for &size in &all_alloc_sizes {
if size == 0 {
continue;
}
for (ebtf, structs) in btfs.iter().zip(per_btf_structs.iter()) {
let choice =
super::super::monitor::sdt_alloc::discover_payload_btf_id(ebtf, size as usize, "");
if choice.target_type_id != 0 {
if let Ok(ty) = ebtf.resolve_type_by_id(choice.target_type_id)
&& let Some(bt) = ty.as_btf_type()
&& let Ok(name) = ebtf.resolve_name(bt)
&& !name.is_empty()
&& seen_names.insert(name.to_string())
{
alloc_size_types.push((size, name.to_string()));
}
break;
}
for (struct_size, name) in structs {
if *struct_size != size {
continue;
}
let dominated =
name == "task_ctx" || name.ends_with("_ctx") || name.ends_with("_arena_ctx");
if dominated && seen_names.insert(name.clone()) {
alloc_size_types.push((size, name.clone()));
}
}
}
}
CastAnalysisOutput {
cast_maps,
btfs,
fwd_index,
alloc_size_types,
}
}
fn parse_btfs_from_bytes(bytes: &[u8]) -> Vec<Arc<Btf>> {
let outer = match goblin::elf::Elf::parse(bytes) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
let bpf_objs_section = match find_section(&outer, ".bpf.objs") {
Some(s) => s,
None => return Vec::new(),
};
let mut btfs = Vec::new();
for inner in iter_embedded_bpf_objects(&outer, bytes, bpf_objs_section) {
let elf = match goblin::elf::Elf::parse(inner) {
Ok(e) => e,
Err(_) => continue,
};
let btf_bytes = match find_section(&elf, ".BTF").and_then(|i| section_data(&elf, inner, i))
{
Some(b) => b,
None => continue,
};
if let Ok(btf) = Btf::from_bytes(btf_bytes) {
btfs.push(Arc::new(btf));
}
}
btfs
}
fn build_fwd_index(btfs: &[Arc<Btf>]) -> HashMap<String, FwdIndexEntry> {
let mut out: HashMap<String, FwdIndexEntry> = HashMap::new();
const CONSECUTIVE_FAIL_CAP: u32 = 256;
for (idx, btf) in btfs.iter().enumerate() {
let mut tid: u32 = 1;
let mut consecutive_fail: u32 = 0;
while tid < crate::monitor::sdt_alloc::MAX_BTF_ID_PROBE {
match btf.resolve_type_by_id(tid) {
Ok(ty) => {
consecutive_fail = 0;
match &ty {
Type::Struct(s) | Type::Union(s) => {
if let Ok(name) = btf.resolve_name(s)
&& !name.is_empty()
{
out.entry(name).or_insert(FwdIndexEntry {
btfs_idx: idx,
type_id: tid,
});
}
}
Type::Typedef(td) => {
if let Ok(td_name) = btf.resolve_name(td)
&& !td_name.is_empty()
&& let Ok(pid) = <dyn btf_rs::BtfType>::get_type_id(td)
&& let Ok(Type::Struct(s)) = btf.resolve_type_by_id(pid)
&& btf.resolve_name(&s).map_or(true, |n| n.is_empty())
{
let base = td_name.strip_suffix("_t").unwrap_or(&td_name);
out.entry(base.to_string()).or_insert(FwdIndexEntry {
btfs_idx: idx,
type_id: pid,
});
}
}
_ => {}
}
}
Err(_) => {
consecutive_fail += 1;
if consecutive_fail >= CONSECUTIVE_FAIL_CAP {
break;
}
}
}
tid += 1;
}
}
out
}
fn enumerate_named_structs(btf: &Btf) -> Vec<(u64, String)> {
const CONSECUTIVE_FAIL_CAP: u32 = 256;
let mut out: Vec<(u64, String)> = Vec::new();
let mut tid: u32 = 1;
let mut consecutive_fail: u32 = 0;
while tid < crate::monitor::sdt_alloc::MAX_BTF_ID_PROBE {
match btf.resolve_type_by_id(tid) {
Ok(ty) => {
consecutive_fail = 0;
if let Type::Struct(s) = &ty
&& let Ok(name) = btf.resolve_name(s)
&& !name.is_empty()
{
out.push((s.size() as u64, name));
}
}
Err(_) => {
consecutive_fail += 1;
if consecutive_fail >= CONSECUTIVE_FAIL_CAP {
break;
}
}
}
tid += 1;
}
out
}
fn iter_embedded_bpf_objects<'data>(
outer: &goblin::elf::Elf<'_>,
file_bytes: &'data [u8],
bpf_objs_idx: usize,
) -> Vec<&'data [u8]> {
let mut out: Vec<&[u8]> = Vec::new();
let sh = &outer.section_headers[bpf_objs_idx];
let sec_file_start = sh.sh_offset as usize;
let sec_file_end = sec_file_start.saturating_add(sh.sh_size as usize);
let sec_va_start = sh.sh_addr;
for sym in outer.syms.iter() {
if sym.st_type() != goblin::elf::sym::STT_OBJECT {
continue;
}
if sym.st_shndx != bpf_objs_idx {
continue;
}
if sym.st_size == 0 {
continue;
}
let Some(rel) = sym.st_value.checked_sub(sec_va_start) else {
continue;
};
let Some(start) = (sec_file_start as u64).checked_add(rel) else {
continue;
};
let Some(end) = start.checked_add(sym.st_size) else {
continue;
};
if (start as usize) < sec_file_start || (end as usize) > sec_file_end {
continue;
}
if let Some(slice) = file_bytes.get(start as usize..end as usize) {
out.push(slice);
}
}
if out.is_empty() {
if let Some(slice) = file_bytes.get(sec_file_start..sec_file_end) {
out.push(slice);
}
}
out
}
fn analyze_one_object_with_btf(obj_bytes: &[u8]) -> (CastMap, Option<Arc<Btf>>, Vec<u64>) {
let elf = match goblin::elf::Elf::parse(obj_bytes) {
Ok(e) => e,
Err(e) => {
tracing::warn!(
error = %e,
"cast_analysis: parse inner BPF object ELF failed"
);
return (CastMap::new(), None, Vec::new());
}
};
let btf_bytes = match find_section(&elf, ".BTF").and_then(|i| section_data(&elf, obj_bytes, i))
{
Some(b) => b,
None => {
tracing::debug!("cast_analysis: inner ELF has no .BTF section");
return (CastMap::new(), None, Vec::new());
}
};
let btf = match Btf::from_bytes(btf_bytes) {
Ok(b) => b,
Err(e) => {
tracing::warn!(
error = ?e,
"cast_analysis: parse .BTF failed"
);
return (CastMap::new(), None, Vec::new());
}
};
let btf = Arc::new(btf);
let total_insns: usize = elf
.section_headers
.iter()
.enumerate()
.filter(|(_, sh)| {
sh.sh_type == goblin::elf::section_header::SHT_PROGBITS
&& sh.sh_flags & u64::from(goblin::elf::section_header::SHF_EXECINSTR) != 0
})
.filter_map(|(idx, _)| section_data(&elf, obj_bytes, idx))
.filter(|d| d.len().is_multiple_of(BPF_INSN_SIZE))
.map(|d| d.len() / BPF_INSN_SIZE)
.sum();
let mut text_concat: Vec<BpfInsn> = Vec::with_capacity(total_insns);
let mut section_bases: HashMap<u32, usize> = HashMap::new();
for (idx, sh) in elf.section_headers.iter().enumerate() {
if sh.sh_type != goblin::elf::section_header::SHT_PROGBITS {
continue;
}
if sh.sh_flags & u64::from(goblin::elf::section_header::SHF_EXECINSTR) == 0 {
continue;
}
let Some(data) = section_data(&elf, obj_bytes, idx) else {
continue;
};
if data.len() % BPF_INSN_SIZE != 0 {
continue;
}
let base = text_concat.len();
for chunk in data.chunks_exact(BPF_INSN_SIZE) {
let mut buf = [0u8; BPF_INSN_SIZE];
buf.copy_from_slice(chunk);
text_concat.push(BpfInsn::from_le_bytes(buf));
}
section_bases.insert(idx as u32, base);
}
if text_concat.is_empty() {
tracing::debug!("cast_analysis: inner ELF has no executable BPF program sections");
return (CastMap::new(), Some(btf), Vec::new());
}
let func_entries = find_section(&elf, ".BTF.ext")
.and_then(|i| section_data(&elf, obj_bytes, i))
.map(|d| parse_btf_ext_func_entries(d, btf_bytes, &elf, §ion_bases))
.unwrap_or_default();
let patch_t0 = std::time::Instant::now();
patch_kfunc_calls(&mut text_concat, btf.as_ref(), &elf, §ion_bases);
tracing::debug!(
elapsed_us = patch_t0.elapsed().as_micros() as u64,
insns = text_concat.len(),
"cast_analysis: patch_kfunc_calls finished"
);
let subprog_patch_t0 = std::time::Instant::now();
patch_subprog_calls(&mut text_concat, &elf, §ion_bases);
tracing::debug!(
elapsed_us = subprog_patch_t0.elapsed().as_micros() as u64,
insns = text_concat.len(),
"cast_analysis: patch_subprog_calls finished"
);
let datasec_t0 = std::time::Instant::now();
let datasec_pointers = build_datasec_pointers(&text_concat, btf.as_ref(), &elf, §ion_bases);
tracing::debug!(
elapsed_us = datasec_t0.elapsed().as_micros() as u64,
datasec_pointers = datasec_pointers.len(),
"cast_analysis: build_datasec_pointers finished"
);
let alloc_seed_t0 = std::time::Instant::now();
let subprog_returns = build_subprog_returns(&text_concat, &elf, §ion_bases);
tracing::debug!(
elapsed_us = alloc_seed_t0.elapsed().as_micros() as u64,
subprog_returns = subprog_returns.len(),
"cast_analysis: build_subprog_returns finished"
);
let analyze_t0 = std::time::Instant::now();
let result = analyze_casts(
&text_concat,
btf.as_ref(),
&[],
&func_entries,
&datasec_pointers,
&subprog_returns,
);
tracing::debug!(
elapsed_ms = analyze_t0.elapsed().as_millis() as u64,
casts = result.len(),
"cast_analysis: analyze_casts inner pass finished"
);
let mut alloc_sizes: Vec<u64> = subprog_returns
.iter()
.filter_map(|sr| sr.alloc_size)
.collect();
alloc_sizes.sort_unstable();
alloc_sizes.dedup();
(result, Some(btf), alloc_sizes)
}
fn iter_text_relocs<'a, 'elf: 'a>(
elf: &'a goblin::elf::Elf<'elf>,
section_bases: &'a HashMap<u32, usize>,
) -> impl Iterator<Item = (usize, goblin::elf::Reloc)> + 'a {
elf.shdr_relocs
.iter()
.flat_map(move |(rel_section_idx, reloc_section)| {
let target_section_idx = elf.section_headers.get(*rel_section_idx).map(|h| h.sh_info);
let scope = target_section_idx.and_then(|idx| {
let base = *section_bases.get(&idx)?;
let sh = elf.section_headers.get(idx as usize)?;
Some((base, sh.sh_size as usize))
});
scope
.into_iter()
.flat_map(move |(base, section_byte_size)| {
reloc_section.iter().filter_map(move |reloc| {
let off = reloc.r_offset as usize;
if !off.is_multiple_of(BPF_INSN_SIZE) {
return None;
}
if off >= section_byte_size {
return None;
}
let insn_idx = base.saturating_add(off / BPF_INSN_SIZE);
Some((insn_idx, reloc))
})
})
})
}
const ALLOC_SUBPROG_NAMES: &[&str] = &[
"scx_alloc_internal",
"scx_static_alloc_internal",
];
fn build_subprog_returns(
text_concat: &[BpfInsn],
elf: &goblin::elf::Elf<'_>,
section_bases: &HashMap<u32, usize>,
) -> Vec<SubprogReturn> {
let mut out: Vec<SubprogReturn> = Vec::new();
for (insn_idx, reloc) in iter_text_relocs(elf, section_bases) {
let Some(insn) = text_concat.get(insn_idx) else {
continue;
};
if insn.code != cast_analysis_load_consts::BPF_JMP_CALL_CODE {
continue;
}
if insn.src_reg() != BPF_PSEUDO_CALL {
continue;
}
let Some(sym) = elf.syms.get(reloc.r_sym) else {
continue;
};
const STT_FUNC: u8 = goblin::elf::sym::STT_FUNC;
const SHN_UNDEF: usize = 0;
if sym.st_shndx == SHN_UNDEF {
continue;
}
if sym.st_type() != STT_FUNC {
continue;
}
let name = match elf.strtab.get_at(sym.st_name) {
Some(s) if !s.is_empty() => s,
_ => continue,
};
if !ALLOC_SUBPROG_NAMES.contains(&name) {
continue;
}
let alloc_size = if name == "scx_static_alloc_internal" {
recover_alloc_size_from_r1(text_concat, insn_idx)
} else {
None
};
out.push(SubprogReturn {
insn_offset: insn_idx,
alloc_size,
});
}
out
}
const ALLOC_SIZE_LOOKBACK: usize = 20;
const BPF_MOV64_IMM_CODE: u8 = (libbpf_rs::libbpf_sys::BPF_ALU64
| libbpf_rs::libbpf_sys::BPF_MOV
| libbpf_rs::libbpf_sys::BPF_K) as u8;
fn recover_alloc_size_from_r1(text: &[BpfInsn], call_pc: usize) -> Option<u64> {
if call_pc == 0 {
return None;
}
let start = call_pc.saturating_sub(ALLOC_SIZE_LOOKBACK);
let mut idx = call_pc;
while idx > start {
idx -= 1;
let insn = text.get(idx)?;
if insn.code == BPF_MOV64_IMM_CODE && insn.dst_reg() == 1 {
return Some(insn.imm as i64 as u64);
}
}
None
}
fn build_datasec_pointers(
text_concat: &[BpfInsn],
btf: &Btf,
elf: &goblin::elf::Elf<'_>,
section_bases: &HashMap<u32, usize>,
) -> Vec<DatasecPointer> {
const R_BPF_64_64: u32 = 1;
let bpf_ld_imm64_code: u8 = (libbpf_rs::libbpf_sys::BPF_LD
| libbpf_rs::libbpf_sys::BPF_DW
| libbpf_rs::libbpf_sys::BPF_IMM) as u8;
let mut out: Vec<DatasecPointer> = Vec::new();
for (insn_pc, reloc) in iter_text_relocs(elf, section_bases) {
if reloc.r_type != R_BPF_64_64 {
continue;
}
let Some(insn) = text_concat.get(insn_pc) else {
continue;
};
if insn.code != bpf_ld_imm64_code {
continue;
}
let Some(sym) = elf.syms.get(reloc.r_sym) else {
continue;
};
const SHN_UNDEF: usize = 0;
const SHN_ABS: usize = 0xFFF1;
const SHN_COMMON: usize = 0xFFF2;
if sym.st_shndx == SHN_UNDEF || sym.st_shndx == SHN_ABS || sym.st_shndx == SHN_COMMON {
continue;
}
let target_sec_idx = sym.st_shndx;
let target_sh_for_name = match elf.section_headers.get(target_sec_idx) {
Some(s) => s,
None => continue,
};
let sec_name = match elf.shdr_strtab.get_at(target_sh_for_name.sh_name) {
Some(s) if !s.is_empty() => s,
_ => continue,
};
let Some(datasec_id) = find_datasec_btf_id(btf, sec_name) else {
continue;
};
let imm_off = if insn.imm < 0 { 0 } else { insn.imm as u32 };
if sym.st_value > u32::MAX as u64 {
continue;
}
let sym_off = sym.st_value as u32;
let Some(base_offset) = imm_off.checked_add(sym_off) else {
continue;
};
out.push(DatasecPointer {
insn_offset: insn_pc,
datasec_type_id: datasec_id,
base_offset,
});
}
out
}
fn find_datasec_btf_id(btf: &Btf, name: &str) -> Option<u32> {
let ids = btf.resolve_ids_by_name(name).ok()?;
for id in ids {
let Ok(ty) = btf.resolve_type_by_id(id) else {
continue;
};
if let Type::Datasec(_) = ty {
return Some(id);
}
}
None
}
fn patch_kfunc_calls(
text_concat: &mut [BpfInsn],
btf: &Btf,
elf: &goblin::elf::Elf<'_>,
section_bases: &HashMap<u32, usize>,
) {
for (insn_idx, reloc) in iter_text_relocs(elf, section_bases) {
let Some(insn) = text_concat.get_mut(insn_idx) else {
continue;
};
if insn.code != cast_analysis_load_consts::BPF_JMP_CALL_CODE {
continue;
}
if insn.imm != -1 {
continue;
}
if insn.src_reg() != BPF_PSEUDO_CALL {
continue;
}
let Some(sym) = elf.syms.get(reloc.r_sym) else {
continue;
};
const STT_NOTYPE: u8 = goblin::elf::sym::STT_NOTYPE;
const STB_GLOBAL: u8 = goblin::elf::sym::STB_GLOBAL;
const STB_WEAK: u8 = goblin::elf::sym::STB_WEAK;
const SHN_UNDEF: usize = 0;
if sym.st_shndx != SHN_UNDEF {
continue;
}
if sym.st_type() != STT_NOTYPE {
continue;
}
let bind = sym.st_bind();
if bind != STB_GLOBAL && bind != STB_WEAK {
continue;
}
let name = match elf.strtab.get_at(sym.st_name) {
Some(s) if !s.is_empty() => s,
_ => continue,
};
let Some(func_btf_id) = find_extern_func_btf_id(btf, name) else {
continue;
};
insn.set_src_reg(BPF_PSEUDO_KFUNC_CALL);
insn.imm = func_btf_id as i32;
}
}
fn patch_subprog_calls(
text_concat: &mut [BpfInsn],
elf: &goblin::elf::Elf<'_>,
section_bases: &HashMap<u32, usize>,
) {
let text_len = text_concat.len();
for (call_pc, reloc) in iter_text_relocs(elf, section_bases) {
let Some(insn) = text_concat.get_mut(call_pc) else {
continue;
};
if insn.code != cast_analysis_load_consts::BPF_JMP_CALL_CODE {
continue;
}
if insn.imm != -1 {
continue;
}
if insn.src_reg() != BPF_PSEUDO_CALL {
continue;
}
let Some(sym) = elf.syms.get(reloc.r_sym) else {
continue;
};
const STT_FUNC: u8 = goblin::elf::sym::STT_FUNC;
const SHN_UNDEF: usize = 0;
if sym.st_shndx == SHN_UNDEF {
continue;
}
if sym.st_type() != STT_FUNC {
continue;
}
let callee_sec_idx = sym.st_shndx as u32;
let Some(&callee_section_base) = section_bases.get(&callee_sec_idx) else {
continue;
};
let Some(callee_section) = elf.section_headers.get(callee_sec_idx as usize) else {
continue;
};
let Some(sym_offset_bytes) = sym.st_value.checked_sub(callee_section.sh_addr) else {
continue;
};
let sym_offset_bytes = sym_offset_bytes as usize;
if !sym_offset_bytes.is_multiple_of(BPF_INSN_SIZE) {
continue;
}
let callee_pc = match callee_section_base.checked_add(sym_offset_bytes / BPF_INSN_SIZE) {
Some(p) => p,
None => continue,
};
if callee_pc >= text_len {
continue;
}
let call_pc_i64 = call_pc as i64;
let callee_pc_i64 = callee_pc as i64;
let new_imm = callee_pc_i64 - call_pc_i64 - 1;
if new_imm < i32::MIN as i64 || new_imm > i32::MAX as i64 {
continue;
}
insn.imm = new_imm as i32;
}
}
fn find_extern_func_btf_id(btf: &Btf, name: &str) -> Option<u32> {
let ids = btf.resolve_ids_by_name(name).ok()?;
for id in ids {
let Ok(ty) = btf.resolve_type_by_id(id) else {
continue;
};
if let Type::Func(f) = ty
&& f.is_extern()
{
return Some(id);
}
}
None
}
mod cast_analysis_load_consts {
use libbpf_rs::libbpf_sys as bs;
pub(super) const BPF_JMP_CALL_CODE: u8 = (bs::BPF_JMP | bs::BPF_CALL) as u8;
}
fn parse_btf_ext_func_entries(
data: &[u8],
btf_bytes: &[u8],
inner_elf: &goblin::elf::Elf<'_>,
section_bases: &HashMap<u32, usize>,
) -> Vec<FuncEntry> {
if data.len() < BTF_EXT_HEADER_MIN_LEN as usize {
return Vec::new();
}
let magic = u16::from_le_bytes([data[0], data[1]]);
if magic != BTF_MAGIC {
return Vec::new();
}
let hdr_len = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
let func_info_off = u32::from_le_bytes([data[8], data[9], data[10], data[11]]);
let func_info_len = u32::from_le_bytes([data[12], data[13], data[14], data[15]]);
if hdr_len < BTF_EXT_HEADER_MIN_LEN || (hdr_len as usize) > data.len() {
return Vec::new();
}
if func_info_len == 0 {
return Vec::new();
}
let info_start = (hdr_len as usize).checked_add(func_info_off as usize);
let info_end = info_start.and_then(|s| s.checked_add(func_info_len as usize));
let (info_start, info_end) = match (info_start, info_end) {
(Some(s), Some(e)) => (s, e),
_ => return Vec::new(),
};
if info_end > data.len() {
return Vec::new();
}
let info = &data[info_start..info_end];
if info.len() < 4 {
return Vec::new();
}
let record_size = u32::from_le_bytes([info[0], info[1], info[2], info[3]]) as usize;
if record_size < 8 {
return Vec::new();
}
let mut cursor = 4usize;
let mut out: Vec<FuncEntry> = Vec::new();
while cursor + 8 <= info.len() {
let sec_name_off = u32::from_le_bytes([
info[cursor],
info[cursor + 1],
info[cursor + 2],
info[cursor + 3],
]);
let num_info = u32::from_le_bytes([
info[cursor + 4],
info[cursor + 5],
info[cursor + 6],
info[cursor + 7],
]) as usize;
cursor += 8;
let records_bytes = num_info.saturating_mul(record_size);
match cursor.checked_add(records_bytes) {
Some(end) if end <= info.len() => {}
_ => break,
}
let sec_name = match btf_str_at(btf_bytes, sec_name_off) {
Some(s) => s,
None => {
cursor += records_bytes;
continue;
}
};
let sec_idx = match find_section(inner_elf, sec_name) {
Some(i) => i as u32,
None => {
cursor += records_bytes;
continue;
}
};
let base = match section_bases.get(&sec_idx) {
Some(b) => *b,
None => {
cursor += records_bytes;
continue;
}
};
for i in 0..num_info {
let rec_off = cursor + i * record_size;
let insn_off = u32::from_le_bytes([
info[rec_off],
info[rec_off + 1],
info[rec_off + 2],
info[rec_off + 3],
]) as usize;
let type_id = u32::from_le_bytes([
info[rec_off + 4],
info[rec_off + 5],
info[rec_off + 6],
info[rec_off + 7],
]);
if !insn_off.is_multiple_of(BPF_INSN_SIZE) {
continue;
}
let entry_idx = base.saturating_add(insn_off / BPF_INSN_SIZE);
out.push(FuncEntry {
insn_offset: entry_idx,
func_proto_id: type_id,
});
}
cursor += records_bytes;
}
out
}
fn find_section(elf: &goblin::elf::Elf<'_>, name: &str) -> Option<usize> {
for (i, sh) in elf.section_headers.iter().enumerate() {
if let Some(n) = elf.shdr_strtab.get_at(sh.sh_name)
&& n == name
{
return Some(i);
}
}
None
}
fn section_data<'a>(
elf: &goblin::elf::Elf<'_>,
file_bytes: &'a [u8],
idx: usize,
) -> Option<&'a [u8]> {
let sh = elf.section_headers.get(idx)?;
let start = sh.sh_offset as usize;
let end = start.checked_add(sh.sh_size as usize)?;
file_bytes.get(start..end)
}
mod persist;
#[cfg(test)]
mod tests;