use super::stack::StackFunction;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum RenderHint {
Decimal,
#[default]
Hex,
Bool,
Signed,
}
#[derive(Debug, Clone, Default)]
pub struct BtfParam {
pub name: String,
pub struct_name: Option<String>,
pub is_ptr: bool,
pub is_string_ptr: bool,
pub auto_fields: Vec<(String, String, RenderHint)>,
pub type_name: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct BtfFunc {
pub name: String,
pub params: Vec<BtfParam>,
pub is_variadic: bool,
}
pub const STRUCT_FIELDS: &[(&str, &[(&str, &str)])] = &[
(
"task_struct",
&[
("->pid", "pid"),
("->cpus_ptr->bits[0]", "cpumask_0"),
("->cpus_ptr->bits[1]", "cpumask_1"),
("->cpus_ptr->bits[2]", "cpumask_2"),
("->cpus_ptr->bits[3]", "cpumask_3"),
("->scx.ddsp_dsq_id", "dsq_id"),
("->scx.ddsp_enq_flags", "enq_flags"),
("->scx.slice", "slice"),
("->scx.dsq_vtime", "vtime"),
("->scx.weight", "weight"),
("->scx.sticky_cpu", "sticky_cpu"),
("->scx.flags", "scx_flags"),
],
),
("rq", &[("->cpu", "cpu")]),
("scx_dispatch_q", &[("->id", "dsq_id")]),
("scx_init_task_args", &[("->fork", "fork")]),
(
"scx_exit_info",
&[("->kind", "exit_kind"), ("->reason", "reason")],
),
("scx_cgroup_init_args", &[("->weight", "weight")]),
];
pub fn resolve_btf_path(kernel_dir: Option<&str>) -> Option<std::path::PathBuf> {
let dir = kernel_dir?;
let vmlinux = std::path::Path::new(dir).join("vmlinux");
if vmlinux.exists() {
tracing::debug!(path = %vmlinux.display(), "btf: using vmlinux from kernel_dir");
Some(vmlinux)
} else {
tracing::debug!(path = %vmlinux.display(), "btf: vmlinux not found, falling back to sysfs");
None
}
}
#[derive(Debug, Clone)]
pub struct FieldSpec {
pub param_idx: u32,
pub offset: u32,
pub size: u32,
pub field_idx: u32,
pub ptr_offset: u32,
}
pub fn resolve_field_specs(btf_func: &BtfFunc, vmlinux_path: Option<&str>) -> Vec<FieldSpec> {
use btf_rs::Btf;
let btf_path = vmlinux_path.unwrap_or("/sys/kernel/btf/vmlinux");
let btf = match Btf::from_file(btf_path) {
Ok(b) => b,
Err(e) => {
tracing::warn!(%e, path = btf_path, "resolve_field_specs: failed to parse BTF");
return Vec::new();
}
};
let mut specs = Vec::new();
let mut field_idx: u32 = 0;
let max_params = btf_func.params.len().min(6);
for (param_idx, param) in btf_func.params[..max_params].iter().enumerate() {
if let Some(ref struct_name) = param.struct_name {
let fields = match STRUCT_FIELDS
.iter()
.find(|(s, _)| *s == struct_name.as_str())
{
Some((_, f)) => *f,
None => continue,
};
let struct_type = match resolve_struct_type(&btf, struct_name) {
Some(s) => s,
None => {
field_idx += fields.len() as u32;
continue;
}
};
for (access, _key) in fields {
let access = access.trim_start_matches("->");
if access.contains("->") {
let (ptr_member, target) = access.split_once("->").unwrap();
let ptr_off_result = resolve_member_offset(&btf, &struct_type, ptr_member);
if let Some((ptr_off, _)) = ptr_off_result {
let pointed = resolve_pointed_struct(&btf, &struct_type, ptr_member);
if let Some(pointed_struct) = pointed
&& let Some((target_off, target_sz)) =
resolve_member_offset(&btf, &pointed_struct, target)
{
specs.push(FieldSpec {
param_idx: param_idx as u32,
offset: target_off,
size: target_sz,
field_idx,
ptr_offset: ptr_off,
});
}
} else {
tracing::debug!(
member = ptr_member,
"chained deref: member offset not found",
);
}
} else {
if let Some((offset, size)) = resolve_member_offset(&btf, &struct_type, access)
{
specs.push(FieldSpec {
param_idx: param_idx as u32,
offset,
size,
field_idx,
ptr_offset: 0,
});
}
}
field_idx += 1;
if field_idx >= 16 {
break;
}
}
} else if !param.auto_fields.is_empty() {
let sname = match param.type_name.as_deref() {
Some(n) => n,
None => {
field_idx += param.auto_fields.len() as u32;
continue;
}
};
let struct_type = match resolve_struct_type(&btf, sname) {
Some(s) => s,
None => {
field_idx += param.auto_fields.len() as u32;
continue;
}
};
let remaining = (16 - field_idx) as usize;
if param.auto_fields.len() > remaining {
tracing::warn!(
func = %btf_func.name,
struct_name = sname,
total = param.auto_fields.len(),
budget = remaining,
"auto-discovered fields truncated to MAX_FIELDS budget",
);
}
for (_fname, access, _hint) in ¶m.auto_fields {
if field_idx >= 16 {
break;
}
let access = access.trim_start_matches("->");
if access.contains("->") {
if let Some((ptr_member, target)) = access.split_once("->") {
let ptr_off_result = resolve_member_offset(&btf, &struct_type, ptr_member);
if let Some((ptr_off, _)) = ptr_off_result {
let pointed = resolve_pointed_struct(&btf, &struct_type, ptr_member);
if let Some(pointed_struct) = pointed
&& let Some((target_off, target_sz)) =
resolve_member_offset(&btf, &pointed_struct, target)
{
specs.push(FieldSpec {
param_idx: param_idx as u32,
offset: target_off,
size: target_sz,
field_idx,
ptr_offset: ptr_off,
});
}
}
}
} else if let Some((offset, size)) =
resolve_member_offset(&btf, &struct_type, access)
{
specs.push(FieldSpec {
param_idx: param_idx as u32,
offset,
size,
field_idx,
ptr_offset: 0,
});
}
field_idx += 1;
}
} else if !param.is_ptr {
field_idx += 1;
}
}
tracing::debug!(n = specs.len(), func = %btf_func.name, "resolve_field_specs");
specs
}
fn resolve_struct_type(btf: &btf_rs::Btf, name: &str) -> Option<btf_rs::Struct> {
let types = btf.resolve_types_by_name(name).ok()?;
for t in types {
if let btf_rs::Type::Struct(s) = t {
return Some(s);
}
}
None
}
fn resolve_member_offset(
btf: &btf_rs::Btf,
struct_type: &btf_rs::Struct,
access: &str,
) -> Option<(u32, u32)> {
let parts: Vec<&str> = access.split('.').collect();
let mut current_struct = struct_type.clone();
let mut total_offset: u32 = 0;
for (i, part) in parts.iter().enumerate() {
let (member_name, array_idx) = if let Some(bracket) = part.find('[') {
let name = &part[..bracket];
let idx_str = &part[bracket + 1..part.len() - 1];
let idx: u32 = idx_str.parse().unwrap_or(0);
(name, Some(idx))
} else {
(*part, None)
};
let member = current_struct.members.iter().find(|m| {
btf.resolve_name(*m)
.map(|n| n == member_name)
.unwrap_or(false)
})?;
let bit_off = member.bit_offset();
if bit_off % 8 != 0 {
return None;
}
total_offset += bit_off / 8;
if let Some(idx) = array_idx {
let elem_size = resolve_type_size(btf, member)?;
total_offset += idx * elem_size;
}
let is_last = i == parts.len() - 1;
if is_last {
let size = resolve_type_size(btf, member)?;
return Some((total_offset, size));
}
let member_type = follow_to_struct_or_union(btf, member)?;
current_struct = member_type;
}
None
}
fn resolve_type_size(btf: &btf_rs::Btf, member: &btf_rs::Member) -> Option<u32> {
use btf_rs::{BtfType, Type};
let tid = member.get_type_id().ok()?;
let mut t = btf.resolve_type_by_id(tid).ok()?;
for _ in 0..20 {
match t {
Type::Ptr(_) => return Some(8),
Type::Int(ref i) => return Some(i.size() as u32),
Type::Enum(ref e) => return Some(e.size() as u32),
Type::Enum64(_) => return Some(8),
Type::Struct(ref s) => return Some(s.size() as u32),
Type::Union(ref u) => return Some(u.size() as u32),
Type::Array(ref a) => {
let elem_tid = a.get_type_id().ok()?;
let elem = btf.resolve_type_by_id(elem_tid).ok()?;
match elem {
Type::Int(ref i) => return Some(i.size() as u32),
Type::Ptr(_) => return Some(8),
_ => return Some(8),
}
}
Type::Const(_)
| Type::Volatile(_)
| Type::Restrict(_)
| Type::Typedef(_)
| Type::TypeTag(_) => {
t = btf.resolve_chained_type(t.as_btf_type()?).ok()?;
}
_ => return None,
}
}
None
}
fn follow_to_struct_or_union(btf: &btf_rs::Btf, member: &btf_rs::Member) -> Option<btf_rs::Struct> {
use btf_rs::{BtfType, Type};
let tid = member.get_type_id().ok()?;
let mut t = btf.resolve_type_by_id(tid).ok()?;
for _ in 0..20 {
match t {
Type::Struct(s) | Type::Union(s) => return Some(s),
Type::Const(_)
| Type::Volatile(_)
| Type::Restrict(_)
| Type::Typedef(_)
| Type::TypeTag(_) => {
t = btf.resolve_chained_type(t.as_btf_type()?).ok()?;
}
_ => return None,
}
}
None
}
fn resolve_pointed_struct(
btf: &btf_rs::Btf,
struct_type: &btf_rs::Struct,
member_name: &str,
) -> Option<btf_rs::Struct> {
use btf_rs::BtfType;
let member_name = member_name.split('[').next().unwrap_or(member_name);
let member = struct_type.members.iter().find(|m| {
btf.resolve_name(*m)
.map(|n| n == member_name)
.unwrap_or(false)
})?;
let tid = member.get_type_id().ok()?;
crate::monitor::bpf_map::resolve_to_struct(btf, tid)
}
pub fn parse_btf_functions(func_names: &[&str], vmlinux_path: Option<&str>) -> Vec<BtfFunc> {
use btf_rs::{Btf, BtfType, Type};
let btf_path = vmlinux_path.unwrap_or("/sys/kernel/btf/vmlinux");
let btf = match Btf::from_file(btf_path) {
Ok(b) => b,
Err(e) => {
tracing::warn!(%e, path = btf_path, "btf: failed to parse");
return Vec::new();
}
};
let resolve_struct_name = |type_id: u32| -> Option<String> {
let mut t = match btf.resolve_type_by_id(type_id) {
Ok(t) => t,
Err(_) => return None,
};
for _ in 0..20 {
match &t {
Type::Ptr(_)
| Type::Const(_)
| Type::Volatile(_)
| Type::Typedef(_)
| Type::Restrict(_)
| Type::TypeTag(_) => {
t = match btf.resolve_chained_type(t.as_btf_type().unwrap()) {
Ok(next) => next,
Err(_) => return None,
};
}
Type::Struct(s) => {
return btf.resolve_name(s).ok();
}
Type::Union(u) => {
return btf.resolve_name(u).ok();
}
_ => return None,
}
}
None
};
let is_ptr =
|type_id: u32| -> bool { matches!(btf.resolve_type_by_id(type_id), Ok(Type::Ptr(_))) };
let is_str_ptr = |type_id: u32| -> bool {
let mut t = match btf.resolve_type_by_id(type_id) {
Ok(t) => t,
Err(_) => return false,
};
for _ in 0..20 {
match &t {
Type::Ptr(_)
| Type::Const(_)
| Type::Volatile(_)
| Type::Restrict(_)
| Type::Typedef(_)
| Type::TypeTag(_) => {
t = match btf.resolve_chained_type(t.as_btf_type().unwrap()) {
Ok(next) => next,
Err(_) => return false,
};
}
Type::Int(i) => return i.size() == 1,
_ => return false,
}
}
false
};
let mut results = Vec::new();
for func_name in func_names {
let types = match btf.resolve_types_by_name(func_name) {
Ok(t) => t,
Err(_) => continue,
};
for t in &types {
if let Type::Func(func) = t {
let proto = match btf.resolve_chained_type(func) {
Ok(Type::FuncProto(fp)) => fp,
_ => continue,
};
let variadic = proto
.parameters
.last()
.map(|p| p.is_variadic())
.unwrap_or(false);
let mut params = Vec::new();
for param in &proto.parameters {
if param.is_variadic() {
continue;
}
let name = btf.resolve_name(param).unwrap_or_default();
let tid = param.get_type_id().unwrap_or(0);
let all_struct_name = resolve_struct_name(tid);
let known_struct = all_struct_name
.as_ref()
.filter(|n| STRUCT_FIELDS.iter().any(|(s, _)| *s == n.as_str()))
.cloned();
let param_is_ptr = is_ptr(tid);
let (auto_fields, type_name) = if param_is_ptr && known_struct.is_none() {
if let Some(ref sname) = all_struct_name {
let fields = discover_vmlinux_struct_fields(&btf, tid);
(fields, Some(sname.clone()))
} else {
(Vec::new(), None)
}
} else {
(
Vec::new(),
all_struct_name.filter(|_| known_struct.is_none()),
)
};
params.push(BtfParam {
name,
struct_name: known_struct,
is_ptr: param_is_ptr,
is_string_ptr: is_str_ptr(tid),
auto_fields,
type_name,
});
}
results.push(BtfFunc {
name: func_name.to_string(),
params,
is_variadic: variadic,
});
break; }
}
}
tracing::debug!(n = results.len(), "btf: parsed function signatures");
results
}
pub fn discover_bpf_symbols(stack_names: &[&str]) -> Vec<StackFunction> {
use libbpf_rs::query::ProgInfoIter;
let mut seen = std::collections::HashSet::new();
let mut results = Vec::new();
for info in ProgInfoIter::default() {
let info_name = match info.name.to_str() {
Ok(n) if !n.is_empty() => n.to_string(),
_ => continue,
};
if info.ty == libbpf_rs::ProgramType::StructOps {
let full_name = if info_name.len() >= 15 {
resolve_bpf_prog_full_name(info.id).unwrap_or(info_name.clone())
} else {
info_name.clone()
};
if !seen.insert(full_name.clone()) {
continue;
}
results.push(StackFunction {
raw_name: format!("bpf_prog_{}_{full_name}", info.id),
display_name: full_name,
is_bpf: true,
bpf_prog_id: Some(info.id),
});
} else if !stack_names.is_empty() {
let matched_name = if stack_names.contains(&info_name.as_str()) {
Some(info_name.clone())
} else {
let candidate = stack_names
.iter()
.find(|sn| sn.len() > info_name.len() && sn.starts_with(&info_name));
if let Some(target) = candidate {
resolve_bpf_prog_full_name(info.id).filter(|full| full == *target)
} else {
None
}
};
if let Some(func_name) = matched_name {
if !seen.insert(func_name.clone()) {
continue;
}
tracing::debug!(
name = %func_name, id = info.id, ty = ?info.ty,
"discover_bpf_symbols: matched non-struct_ops program from stack",
);
results.push(StackFunction {
raw_name: format!("bpf_prog_{}_{func_name}", info.id),
display_name: func_name,
is_bpf: true,
bpf_prog_id: Some(info.id),
});
}
}
}
tracing::debug!(n = results.len(), "discover_bpf_symbols");
results
}
fn resolve_bpf_prog_full_name(prog_id: u32) -> Option<String> {
use libbpf_rs::AsRawLibbpf;
use libbpf_rs::libbpf_sys;
let prog_btf = libbpf_rs::btf::Btf::from_prog_id(prog_id).ok()?;
let btf_ptr = prog_btf.as_libbpf_object().as_ptr();
let fd = unsafe { libbpf_sys::bpf_prog_get_fd_by_id(prog_id) };
if fd < 0 {
return None;
}
let mut info = libbpf_sys::bpf_prog_info::default();
let mut info_len = std::mem::size_of::<libbpf_sys::bpf_prog_info>() as u32;
let ret = unsafe {
libbpf_sys::bpf_obj_get_info_by_fd(fd, &mut info as *mut _ as *mut _, &mut info_len)
};
if ret != 0 || info.nr_func_info == 0 {
unsafe { libc::close(fd) };
return None;
}
let fi_rec = info.func_info_rec_size as usize;
let mut fi_buf = vec![0u8; info.nr_func_info as usize * fi_rec];
let mut info2 = libbpf_sys::bpf_prog_info {
nr_func_info: info.nr_func_info,
func_info_rec_size: info.func_info_rec_size,
func_info: fi_buf.as_mut_ptr() as u64,
..Default::default()
};
let mut info2_len = std::mem::size_of::<libbpf_sys::bpf_prog_info>() as u32;
let ret = unsafe {
libbpf_sys::bpf_obj_get_info_by_fd(fd, &mut info2 as *mut _ as *mut _, &mut info2_len)
};
unsafe { libc::close(fd) };
if ret != 0 {
return None;
}
let fi = unsafe { &*(fi_buf.as_ptr() as *const libbpf_sys::bpf_func_info) };
let t = unsafe { libbpf_sys::btf__type_by_id(btf_ptr, fi.type_id) };
if t.is_null() {
return None;
}
let name_ptr = unsafe { libbpf_sys::btf__name_by_offset(btf_ptr, (*t).name_off) };
if name_ptr.is_null() {
return None;
}
let name = unsafe { std::ffi::CStr::from_ptr(name_ptr) }
.to_str()
.ok()?
.to_string();
if name.is_empty() { None } else { Some(name) }
}
pub fn resolve_bpf_source_locs(prog_ids: &[u32]) -> std::collections::HashMap<String, String> {
use libbpf_rs::AsRawLibbpf;
use libbpf_rs::libbpf_sys;
let mut locs = std::collections::HashMap::new();
for prog_id in prog_ids {
let prog_btf = match libbpf_rs::btf::Btf::from_prog_id(*prog_id) {
Ok(b) => b,
Err(_) => continue,
};
let btf_ptr = prog_btf.as_libbpf_object().as_ptr();
let fd = unsafe { libbpf_sys::bpf_prog_get_fd_by_id(*prog_id) };
if fd < 0 {
continue;
}
let mut info = libbpf_sys::bpf_prog_info::default();
let mut info_len = std::mem::size_of::<libbpf_sys::bpf_prog_info>() as u32;
let ret = unsafe {
libbpf_sys::bpf_obj_get_info_by_fd(fd, &mut info as *mut _ as *mut _, &mut info_len)
};
if ret != 0 || info.nr_func_info == 0 || info.nr_line_info == 0 {
unsafe { libc::close(fd) };
continue;
}
let nr_fi = info.nr_func_info as usize;
let nr_li = info.nr_line_info as usize;
let fi_rec = info.func_info_rec_size as usize;
let li_rec = info.line_info_rec_size as usize;
let mut fi_buf = vec![0u8; nr_fi * fi_rec];
let mut li_buf = vec![0u8; nr_li * li_rec];
let mut info2 = libbpf_sys::bpf_prog_info {
nr_func_info: nr_fi as u32,
func_info_rec_size: fi_rec as u32,
func_info: fi_buf.as_mut_ptr() as u64,
nr_line_info: nr_li as u32,
line_info_rec_size: li_rec as u32,
line_info: li_buf.as_mut_ptr() as u64,
..Default::default()
};
let mut info2_len = std::mem::size_of::<libbpf_sys::bpf_prog_info>() as u32;
let ret = unsafe {
libbpf_sys::bpf_obj_get_info_by_fd(fd, &mut info2 as *mut _ as *mut _, &mut info2_len)
};
unsafe { libc::close(fd) };
if ret != 0 {
continue;
}
for i in 0..nr_fi {
let fi =
unsafe { &*(fi_buf.as_ptr().add(i * fi_rec) as *const libbpf_sys::bpf_func_info) };
let t = unsafe { libbpf_sys::btf__type_by_id(btf_ptr, fi.type_id) };
if t.is_null() {
continue;
}
let name_ptr = unsafe { libbpf_sys::btf__name_by_offset(btf_ptr, (*t).name_off) };
if name_ptr.is_null() {
continue;
}
let fname = unsafe { std::ffi::CStr::from_ptr(name_ptr) }
.to_str()
.unwrap_or("")
.to_string();
if fname.is_empty() {
continue;
}
let mut best: Option<&libbpf_sys::bpf_line_info> = None;
for j in 0..nr_li {
let li = unsafe {
&*(li_buf.as_ptr().add(j * li_rec) as *const libbpf_sys::bpf_line_info)
};
if li.insn_off >= fi.insn_off && best.is_none_or(|b| li.insn_off < b.insn_off) {
best = Some(li);
}
}
if let Some(li) = best {
let file_ptr =
unsafe { libbpf_sys::btf__name_by_offset(btf_ptr, li.file_name_off) };
if !file_ptr.is_null() {
let file = unsafe { std::ffi::CStr::from_ptr(file_ptr) }
.to_str()
.unwrap_or("");
if !file.is_empty() {
let basename = file.rsplit('/').next().unwrap_or(file);
let line = li.line_col >> 10;
locs.insert(fname, format!("{basename}:{line}"));
}
}
}
}
}
tracing::debug!(n = locs.len(), "resolve_bpf_source_locs");
locs
}
pub fn parse_bpf_btf_functions(
func_names: &[(&str, u32)], ) -> Vec<BtfFunc> {
use libbpf_rs::btf;
let mut by_prog: std::collections::HashMap<u32, Vec<&str>> = std::collections::HashMap::new();
for (name, pid) in func_names {
by_prog.entry(*pid).or_default().push(name);
}
let vmlinux = match btf::Btf::from_vmlinux() {
Ok(b) => b,
Err(e) => {
tracing::warn!(%e, "parse_bpf_btf: failed to load vmlinux BTF");
return Vec::new();
}
};
let resolve_struct_name = |b: &btf::Btf<'_>, type_id: btf::TypeId| -> Option<String> {
let t = b.type_by_id::<btf::BtfType<'_>>(type_id)?;
let inner = t.skip_mods_and_typedefs();
let deref = if inner.kind() == btf::BtfKind::Ptr {
inner.next_type()?.skip_mods_and_typedefs()
} else {
inner
};
if deref.kind() == btf::BtfKind::Struct || deref.kind() == btf::BtfKind::Union {
Some(deref.name()?.to_str()?.to_string())
} else {
None
}
};
let mut results = Vec::new();
for (prog_id, names) in &by_prog {
let prog_btf = match btf::Btf::from_prog_id(*prog_id) {
Ok(b) => b,
Err(e) => {
tracing::debug!(prog_id, %e, "parse_bpf_btf: failed to load prog BTF");
continue;
}
};
for func_name in names {
let inner_name = format!("____{func_name}");
let (proto, skip_first_param) =
if let Some(f) = prog_btf.type_by_name::<btf::types::Func<'_>>(&inner_name) {
let bt: btf::BtfType<'_> = *f;
if let Some(pt) = bt
.next_type()
.filter(|t| t.kind() == btf::BtfKind::FuncProto)
{
if let Ok(p) = TryInto::<btf::types::FuncProto<'_>>::try_into(pt) {
(Some(p), true)
} else {
(None, false)
}
} else {
(None, false)
}
} else {
(None, false)
};
let ops_proto = if proto.is_none() {
let p = resolve_ops_callback_proto(&vmlinux, func_name);
tracing::debug!(
func = func_name,
ops_found = p.is_some(),
"bpf_btf: ____name not in BTF, trying ops fallback",
);
p
} else {
None
};
let (use_proto_from_ops, proto) = if let Some(ref p) = proto {
(false, p)
} else if let Some(ref p) = ops_proto {
(true, p)
} else {
let f = match prog_btf.type_by_name::<btf::types::Func<'_>>(func_name) {
Some(f) => f,
None => continue,
};
let bt: btf::BtfType<'_> = *f;
let _pt = match bt
.next_type()
.filter(|t| t.kind() == btf::BtfKind::FuncProto)
{
Some(t) => t,
None => continue,
};
results.push(BtfFunc {
name: func_name.to_string(),
params: vec![],
is_variadic: false,
});
continue;
};
let btf_for_params: &btf::Btf<'_> = if use_proto_from_ops {
&vmlinux
} else {
&prog_btf
};
let mut params = Vec::new();
let param_iter: Vec<_> = if skip_first_param && !use_proto_from_ops {
proto.iter().skip(1).collect()
} else {
proto.iter().collect()
};
for (param_pos, param) in param_iter.into_iter().enumerate() {
let mut name = param
.name
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
if name.is_empty() {
let sname = resolve_struct_name(btf_for_params, param.ty);
name = match sname.as_deref() {
Some("task_struct") => "p".into(),
Some("rq") => "rq".into(),
Some("scx_exit_info") => "ei".into(),
Some("scx_init_task_args") => "args".into(),
Some("scx_cgroup_init_args") => "args".into(),
Some("scx_dispatch_q") => "dsq".into(),
_ => {
infer_scalar_param_name(func_name, param_pos)
}
};
}
let all_struct_name = resolve_struct_name(btf_for_params, param.ty);
let known_struct = all_struct_name
.as_ref()
.filter(|n| STRUCT_FIELDS.iter().any(|(s, _)| *s == n.as_str()))
.cloned();
let is_ptr = btf_for_params
.type_by_id::<btf::BtfType<'_>>(param.ty)
.map(|t| t.skip_mods_and_typedefs().kind() == btf::BtfKind::Ptr)
.unwrap_or(false);
let (auto_fields, type_name) = if is_ptr && known_struct.is_none() {
if let Some(ref sname) = all_struct_name {
let is_vmlinux: Option<btf::types::Struct<'_>> =
vmlinux.type_by_name(sname);
if is_vmlinux.is_some() {
(Vec::new(), Some(sname.clone()))
} else {
let fields = discover_bpf_struct_fields(&prog_btf, param.ty);
(fields, Some(sname.clone()))
}
} else {
(Vec::new(), None)
}
} else {
(
Vec::new(),
all_struct_name.filter(|_| known_struct.is_none()),
)
};
tracing::debug!(
func = func_name, param = %name, struct_name = ?known_struct,
is_ptr, auto_fields = auto_fields.len(),
"bpf_btf: resolved param",
);
params.push(BtfParam {
name,
struct_name: known_struct,
is_ptr,
auto_fields,
type_name,
..Default::default()
});
}
results.push(BtfFunc {
name: func_name.to_string(),
params,
is_variadic: false,
});
}
}
tracing::debug!(
n = results.len(),
"parse_bpf_btf: parsed BPF function signatures"
);
results
}
fn infer_scalar_param_name(func_name: &str, param_pos: usize) -> String {
const OPS_SCALARS: &[(&str, &[&str])] = &[
("dispatch", &["cpu"]),
("select_cpu", &["", "prev_cpu", "wake_flags"]),
("set_weight", &["", "weight"]),
("update_idle", &["cpu", "idle"]),
("cpu_acquire", &["cpu"]),
("cpu_release", &["cpu"]),
("cpu_online", &["cpu"]),
("cpu_offline", &["cpu"]),
];
for (op, names) in OPS_SCALARS {
if func_name.ends_with(op)
&& let Some(name) = names.get(param_pos)
&& !name.is_empty()
{
return name.to_string();
}
}
format!("arg{param_pos}")
}
pub(super) fn resolve_ops_callback_proto<'a>(
vmlinux: &'a libbpf_rs::btf::Btf<'a>,
func_name: &str,
) -> Option<libbpf_rs::btf::types::FuncProto<'a>> {
use libbpf_rs::btf::{BtfKind, BtfType};
let ops: libbpf_rs::btf::types::Struct<'_> = vmlinux.type_by_name("sched_ext_ops")?;
for member in ops.iter() {
let member_name = member.name.and_then(|n| n.to_str()).unwrap_or("");
if member_name.is_empty() || !func_name.ends_with(member_name) {
continue;
}
let mut t = vmlinux.type_by_id::<BtfType<'_>>(member.ty)?;
for _ in 0..20 {
let inner = t.skip_mods_and_typedefs();
match inner.kind() {
BtfKind::Ptr => {
t = inner.next_type()?;
}
BtfKind::FuncProto => {
return inner.try_into().ok();
}
_ => break,
}
}
}
None
}
pub fn resolve_bpf_field_specs(btf_func: &BtfFunc, prog_id: u32) -> Vec<FieldSpec> {
use libbpf_rs::btf;
let prog_btf = match btf::Btf::from_prog_id(prog_id) {
Ok(b) => b,
Err(_) => return Vec::new(),
};
let mut specs = Vec::new();
let mut field_idx: u32 = 0;
let max_params = btf_func.params.len().min(6);
for (param_idx, param) in btf_func.params[..max_params].iter().enumerate() {
if param.struct_name.is_some() {
if let Some((_, fields)) = STRUCT_FIELDS
.iter()
.find(|(s, _)| Some(*s) == param.struct_name.as_deref())
{
field_idx += fields.len() as u32;
}
continue;
}
if !param.auto_fields.is_empty() {
let sname = match param.type_name.as_deref() {
Some(n) => n,
None => {
field_idx += param.auto_fields.len() as u32;
continue;
}
};
let struct_type: Option<btf::types::Struct<'_>> = prog_btf.type_by_name(sname);
let composite = match struct_type {
Some(s) => s,
None => {
field_idx += param.auto_fields.len() as u32;
continue;
}
};
for (_fname, access, _hint) in ¶m.auto_fields {
let access = access.trim_start_matches("->");
if !access.contains("->") {
let member_name = access.split('[').next().unwrap_or(access);
if let Some(offset) = resolve_bpf_member_offset(&composite, member_name) {
let size = resolve_bpf_member_size(&prog_btf, &composite, member_name)
.unwrap_or(8);
specs.push(FieldSpec {
param_idx: param_idx as u32,
offset,
size,
field_idx,
ptr_offset: 0,
});
}
} else if let Some((ptr_member, target)) = access.split_once("->") {
let ptr_member = ptr_member.split('[').next().unwrap_or(ptr_member);
if let Some(ptr_off) = resolve_bpf_member_offset(&composite, ptr_member) {
let target_stripped = target.split('[').next().unwrap_or(target);
let target_parts: Vec<&str> = target_stripped.split('.').collect();
let mut target_off = 0u32;
let mut target_sz = 8u32;
'resolve_target: for member in composite.iter() {
let name = member.name.and_then(|n| n.to_str()).unwrap_or("");
if name != ptr_member {
continue;
}
if let Some(pointed) =
prog_btf.type_by_id::<libbpf_rs::btf::BtfType<'_>>(member.ty)
{
let deref = pointed.skip_mods_and_typedefs();
if deref.kind() == libbpf_rs::btf::BtfKind::Ptr
&& let Some(inner) = deref.next_type()
{
let mut current = inner.skip_mods_and_typedefs();
let mut accumulated_off = 0u32;
for (i, part) in target_parts.iter().enumerate() {
if let Ok(cur_struct) =
TryInto::<libbpf_rs::btf::types::Struct<'_>>::try_into(
current,
)
{
if let Some(off) =
resolve_bpf_member_offset(&cur_struct, part)
{
accumulated_off += off;
}
if i == target_parts.len() - 1 {
if let Some(sz) = resolve_bpf_member_size(
&prog_btf,
&cur_struct,
part,
) {
target_sz = sz;
}
} else {
for m in cur_struct.iter() {
let mn = m
.name
.and_then(|n| n.to_str())
.unwrap_or("");
if mn == *part {
if let Some(mt) = prog_btf
.type_by_id::<libbpf_rs::btf::BtfType<
'_,
>>(
m.ty
) {
current = mt.skip_mods_and_typedefs();
}
break;
}
}
}
}
}
target_off = accumulated_off;
}
}
break 'resolve_target;
}
specs.push(FieldSpec {
param_idx: param_idx as u32,
offset: target_off,
size: target_sz,
field_idx,
ptr_offset: ptr_off,
});
}
}
field_idx += 1;
if field_idx >= 16 {
break;
}
}
} else if !param.is_ptr {
field_idx += 1;
}
}
tracing::debug!(
n = specs.len(),
func = %btf_func.name,
"resolve_bpf_field_specs",
);
specs
}
fn resolve_bpf_member_offset(
composite: &libbpf_rs::btf::types::Struct<'_>,
member_name: &str,
) -> Option<u32> {
use libbpf_rs::btf::types::MemberAttr;
for member in composite.iter() {
let name = member.name.and_then(|n| n.to_str()).unwrap_or("");
if name == member_name {
let bit_off = match member.attr {
MemberAttr::Normal { offset } => offset,
MemberAttr::BitField { offset, .. } => offset,
};
if bit_off % 8 != 0 {
return None; }
return Some(bit_off / 8);
}
}
None
}
fn resolve_bpf_member_size(
btf: &libbpf_rs::btf::Btf<'_>,
composite: &libbpf_rs::btf::types::Struct<'_>,
member_name: &str,
) -> Option<u32> {
use libbpf_rs::btf::{BtfKind, BtfType};
for member in composite.iter() {
let name = member.name.and_then(|n| n.to_str()).unwrap_or("");
if name != member_name {
continue;
}
let t = btf.type_by_id::<BtfType<'_>>(member.ty)?;
let inner = t.skip_mods_and_typedefs();
return match inner.kind() {
BtfKind::Int => {
let int_ty: Result<libbpf_rs::btf::types::Int<'_>, _> = inner.try_into();
Some(int_ty.map(|i| (i.bits / 8) as u32).unwrap_or(8))
}
BtfKind::Enum => Some(4),
BtfKind::Enum64 => Some(8),
BtfKind::Ptr => Some(8),
_ => Some(8),
};
}
None
}
fn discover_bpf_struct_fields(
btf: &libbpf_rs::btf::Btf<'_>,
type_id: libbpf_rs::btf::TypeId,
) -> Vec<(String, String, RenderHint)> {
use libbpf_rs::btf::{BtfKind, BtfType};
let t = match btf.type_by_id::<BtfType<'_>>(type_id) {
Some(t) => t.skip_mods_and_typedefs(),
None => return Vec::new(),
};
let inner = if t.kind() == BtfKind::Ptr {
match t.next_type() {
Some(t) => t.skip_mods_and_typedefs(),
None => return Vec::new(),
}
} else {
t
};
if inner.kind() != BtfKind::Struct && inner.kind() != BtfKind::Union {
return Vec::new();
}
let composite: libbpf_rs::btf::types::Struct<'_> = match inner.try_into() {
Ok(s) => s,
Err(_) => return Vec::new(),
};
let mut fields = Vec::new();
for member in composite.iter() {
let fname = match member.name.and_then(|n| n.to_str()) {
Some(n) if !n.is_empty() => n.to_string(),
_ => continue,
};
let member_type = match btf.type_by_id::<BtfType<'_>>(member.ty) {
Some(t) => t.skip_mods_and_typedefs(),
None => continue,
};
match member_type.kind() {
BtfKind::Int => {
let hint = if let Ok(int_ty) =
TryInto::<libbpf_rs::btf::types::Int<'_>>::try_into(member_type)
{
match int_ty.encoding {
libbpf_rs::btf::types::IntEncoding::Bool => RenderHint::Bool,
libbpf_rs::btf::types::IntEncoding::Signed => RenderHint::Signed,
_ => RenderHint::Hex,
}
} else {
RenderHint::Hex
};
fields.push((fname.clone(), format!("->{fname}"), hint));
}
BtfKind::Enum | BtfKind::Enum64 => {
fields.push((fname.clone(), format!("->{fname}"), RenderHint::Hex));
}
BtfKind::Ptr => {
let deref = member_type.next_type().map(|t| t.skip_mods_and_typedefs());
let pointed_name = deref
.as_ref()
.and_then(|t| t.name())
.and_then(|n| n.to_str());
match pointed_name {
Some("cpumask") => {
fields.push((
fname.clone(),
format!("->{fname}->bits[0]"),
RenderHint::Hex,
));
}
Some("bpf_cpumask") => {
fields.push((
fname.clone(),
format!("->{fname}->cpumask.bits[0]"),
RenderHint::Hex,
));
}
_ => {} }
}
_ => {}
}
}
fields
}
fn discover_vmlinux_struct_fields(
btf: &btf_rs::Btf,
type_id: u32,
) -> Vec<(String, String, RenderHint)> {
use btf_rs::{BtfType, Type};
let t = match btf.resolve_type_by_id(type_id) {
Ok(t) => t,
Err(_) => return Vec::new(),
};
let mut current = t;
let struct_type = loop {
match current {
Type::Ptr(_)
| Type::Const(_)
| Type::Volatile(_)
| Type::Typedef(_)
| Type::Restrict(_)
| Type::TypeTag(_) => {
current = match btf.resolve_chained_type(current.as_btf_type().unwrap()) {
Ok(next) => next,
Err(_) => return Vec::new(),
};
}
Type::Struct(s) | Type::Union(s) => break s,
_ => return Vec::new(),
}
};
let mut fields = Vec::new();
for member in &struct_type.members {
let fname = match btf.resolve_name(member) {
Ok(n) if !n.is_empty() => n,
_ => continue,
};
let member_tid = match member.get_type_id() {
Ok(tid) => tid,
Err(_) => continue,
};
let mut member_type = match btf.resolve_type_by_id(member_tid) {
Ok(t) => t,
Err(_) => continue,
};
for _ in 0..20 {
match &member_type {
Type::Const(_)
| Type::Volatile(_)
| Type::Restrict(_)
| Type::Typedef(_)
| Type::TypeTag(_) => {
member_type = match btf.resolve_chained_type(member_type.as_btf_type().unwrap())
{
Ok(next) => next,
Err(_) => break,
};
}
_ => break,
}
}
match &member_type {
Type::Int(i) => {
let hint = if i.is_bool() {
RenderHint::Bool
} else if i.is_signed() {
RenderHint::Signed
} else {
RenderHint::Hex
};
fields.push((fname.clone(), format!("->{fname}"), hint));
}
Type::Enum(_) | Type::Enum64(_) => {
fields.push((fname.clone(), format!("->{fname}"), RenderHint::Hex));
}
Type::Ptr(_) => {
let pointed = btf.resolve_chained_type(member_type.as_btf_type().unwrap());
if let Ok(pointed_type) = pointed {
let mut inner = pointed_type;
for _ in 0..20 {
match &inner {
Type::Const(_)
| Type::Volatile(_)
| Type::Restrict(_)
| Type::Typedef(_)
| Type::TypeTag(_) => {
inner = match btf.resolve_chained_type(inner.as_btf_type().unwrap())
{
Ok(next) => next,
Err(_) => break,
};
}
_ => break,
}
}
let pointed_name = match &inner {
Type::Struct(s) | Type::Union(s) => btf.resolve_name(s).ok(),
_ => None,
};
match pointed_name.as_deref() {
Some("cpumask") | Some("cpumask_t") => {
fields.push((
fname.clone(),
format!("->{fname}->bits[0]"),
RenderHint::Hex,
));
}
_ => {} }
}
}
_ => {}
}
}
fields
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn struct_fields_has_task_struct() {
let entry = STRUCT_FIELDS.iter().find(|(s, _)| *s == "task_struct");
assert!(entry.is_some());
let (_, fields) = entry.unwrap();
assert!(fields.iter().any(|(_, k)| *k == "pid"));
assert!(fields.iter().any(|(_, k)| *k == "dsq_id"));
assert!(fields.iter().any(|(_, k)| *k == "cpumask_0"));
}
#[test]
fn struct_fields_has_rq() {
let entry = STRUCT_FIELDS.iter().find(|(s, _)| *s == "rq");
assert!(entry.is_some());
let (_, fields) = entry.unwrap();
assert!(fields.iter().any(|(_, k)| *k == "cpu"));
}
#[test]
fn parse_btf_known_kernel_func() {
if !std::path::Path::new("/sys/kernel/btf/vmlinux").exists() {
return;
}
let funcs = parse_btf_functions(&["do_exit"], None);
if funcs.is_empty() {
return;
}
assert_eq!(funcs[0].name, "do_exit");
assert!(!funcs[0].params.is_empty());
}
#[test]
fn parse_btf_unknown_func_returns_empty() {
let funcs = parse_btf_functions(&["__totally_fake_function_name__"], None);
assert!(funcs.is_empty());
}
#[test]
fn discover_bpf_symbols_no_scheduler() {
let _ = discover_bpf_symbols(&[]);
}
#[test]
fn resolve_btf_path_none() {
assert!(resolve_btf_path(None).is_none());
}
#[test]
fn resolve_btf_path_nonexistent() {
assert!(resolve_btf_path(Some("/nonexistent/path")).is_none());
}
#[test]
fn resolve_btf_path_dir_without_vmlinux() {
let dir = std::env::temp_dir();
assert!(resolve_btf_path(Some(dir.to_str().unwrap())).is_none());
}
#[test]
fn struct_fields_all_entries_have_fields() {
for (name, fields) in STRUCT_FIELDS {
assert!(!fields.is_empty(), "struct {name} has no fields");
}
}
#[test]
fn struct_fields_no_duplicate_structs() {
let names: Vec<&str> = STRUCT_FIELDS.iter().map(|(n, _)| *n).collect();
let unique: std::collections::HashSet<&&str> = names.iter().collect();
assert_eq!(
names.len(),
unique.len(),
"duplicate struct names in STRUCT_FIELDS"
);
}
#[test]
fn struct_fields_no_duplicate_keys_per_struct() {
for (name, fields) in STRUCT_FIELDS {
let keys: Vec<&str> = fields.iter().map(|(_, k)| *k).collect();
let unique: std::collections::HashSet<&&str> = keys.iter().collect();
assert_eq!(
keys.len(),
unique.len(),
"struct {name} has duplicate field keys"
);
}
}
#[test]
fn struct_fields_has_scx_dispatch_q() {
let entry = STRUCT_FIELDS.iter().find(|(s, _)| *s == "scx_dispatch_q");
assert!(entry.is_some());
}
#[test]
fn struct_fields_has_scx_exit_info() {
let entry = STRUCT_FIELDS.iter().find(|(s, _)| *s == "scx_exit_info");
assert!(entry.is_some());
}
#[test]
fn struct_fields_has_scx_init_task_args() {
let entry = STRUCT_FIELDS
.iter()
.find(|(s, _)| *s == "scx_init_task_args");
assert!(entry.is_some());
}
#[test]
fn struct_fields_has_scx_cgroup_init_args() {
let entry = STRUCT_FIELDS
.iter()
.find(|(s, _)| *s == "scx_cgroup_init_args");
assert!(entry.is_some());
}
#[test]
fn btf_param_debug_display() {
let p = BtfParam {
name: "test".into(),
struct_name: Some("task_struct".into()),
is_ptr: true,
..Default::default()
};
let dbg = format!("{:?}", p);
assert!(dbg.contains("task_struct"));
assert!(dbg.contains("test"));
}
#[test]
fn btf_func_empty_params() {
let f = BtfFunc {
name: "empty".into(),
params: vec![],
is_variadic: false,
};
assert_eq!(f.name, "empty");
assert!(f.params.is_empty());
}
#[test]
fn btf_func_multiple_params() {
let f = BtfFunc {
name: "multi".into(),
params: vec![
BtfParam {
name: "a".into(),
struct_name: Some("task_struct".into()),
is_ptr: true,
..Default::default()
},
BtfParam {
name: "b".into(),
struct_name: Some("rq".into()),
is_ptr: true,
..Default::default()
},
BtfParam {
name: "c".into(),
struct_name: None,
is_ptr: false,
..Default::default()
},
],
is_variadic: false,
};
assert_eq!(f.params.len(), 3);
assert!(f.params[0].is_ptr);
assert!(f.params[2].struct_name.is_none());
}
#[test]
fn parse_btf_multiple_unknown_funcs() {
let funcs = parse_btf_functions(&["__fake_a__", "__fake_b__", "__fake_c__"], None);
assert!(funcs.is_empty());
}
#[test]
fn resolve_btf_path_real_kernel_dir() {
let _ = resolve_btf_path(Some("/usr/lib/debug/boot"));
}
#[test]
fn struct_fields_has_cpumask_words() {
let entry = STRUCT_FIELDS.iter().find(|(s, _)| *s == "task_struct");
let (_, fields) = entry.unwrap();
assert!(fields.iter().any(|(_, k)| *k == "cpumask_0"));
assert!(fields.iter().any(|(_, k)| *k == "cpumask_1"));
assert!(fields.iter().any(|(_, k)| *k == "cpumask_2"));
assert!(fields.iter().any(|(_, k)| *k == "cpumask_3"));
}
#[test]
fn struct_fields_cpumask_access_patterns() {
let entry = STRUCT_FIELDS.iter().find(|(s, _)| *s == "task_struct");
let (_, fields) = entry.unwrap();
assert!(fields.iter().any(|(a, _)| *a == "->cpus_ptr->bits[0]"));
assert!(fields.iter().any(|(a, _)| *a == "->cpus_ptr->bits[1]"));
assert!(fields.iter().any(|(a, _)| *a == "->cpus_ptr->bits[2]"));
assert!(fields.iter().any(|(a, _)| *a == "->cpus_ptr->bits[3]"));
}
#[test]
fn struct_fields_task_struct_field_count() {
let entry = STRUCT_FIELDS.iter().find(|(s, _)| *s == "task_struct");
let (_, fields) = entry.unwrap();
assert_eq!(fields.len(), 12);
}
#[test]
fn struct_fields_auto_discover_cpumask_pattern() {
let ts_fields = STRUCT_FIELDS
.iter()
.find(|(s, _)| *s == "task_struct")
.unwrap()
.1;
let cpumask_accesses: Vec<&&str> = ts_fields
.iter()
.filter(|(a, _)| a.contains("bits["))
.map(|(a, _)| a)
.collect();
assert_eq!(cpumask_accesses.len(), 4);
for (i, a) in cpumask_accesses.iter().enumerate() {
assert!(
a.contains(&format!("bits[{i}]")),
"expected bits[{i}] in {a}",
);
}
}
#[test]
fn struct_fields_access_patterns_start_with_arrow() {
for (name, fields) in STRUCT_FIELDS {
for (access, _key) in *fields {
assert!(
access.starts_with("->"),
"struct {name} field access '{access}' should start with '->'"
);
}
}
}
}