From 4cd24e430b534f0a8aaefb9021f80bf530d44cd5 Mon Sep 17 00:00:00 2001 From: altug bozkurt Date: Wed, 22 Oct 2025 15:40:26 +0300 Subject: [PATCH] aya: Support loading programs with ksyms --- aya-obj/src/btf/btf.rs | 135 +++++++ aya-obj/src/extern_types.rs | 470 +++++++++++++++++++++++ aya-obj/src/lib.rs | 2 + aya-obj/src/obj.rs | 204 +++++++++- aya-obj/src/relocation.rs | 160 +++++++- aya/src/bpf.rs | 29 +- test/integration-test/bpf/ksyms.bpf.c | 32 ++ test/integration-test/build.rs | 1 + test/integration-test/src/lib.rs | 1 + test/integration-test/src/tests.rs | 1 + test/integration-test/src/tests/ksyms.rs | 14 + 11 files changed, 1045 insertions(+), 4 deletions(-) create mode 100644 aya-obj/src/extern_types.rs create mode 100644 test/integration-test/bpf/ksyms.bpf.c create mode 100644 test/integration-test/src/tests/ksyms.rs diff --git a/aya-obj/src/btf/btf.rs b/aya-obj/src/btf/btf.rs index ff28b164..5a94a606 100644 --- a/aya-obj/src/btf/btf.rs +++ b/aya-obj/src/btf/btf.rs @@ -812,6 +812,141 @@ impl Btf { self.types = types; Ok(()) } + + /// Fixes up BTF for `.ksyms` datasec entries containing extern kernel symbol and + /// makes it acceptable by the kernel: + /// + /// * Changes linkage of extern functions to `GLOBAL`, fixes parameter names, injects + /// a dummy variable representing them in datasec. + /// * Changes linkage of extern variables to `GLOBAL_ALLOCATED`, replaces their type + /// with `int`. + pub(crate) fn fixup_ksyms_datasec( + &mut self, + datasec_id: u32, + dummy_var_id: Option, + ) -> Result<(), BtfError> { + // Extract dummy variable's name offset and type ID for patching func_proto names and datasec entries. + // If dummy var exists: use its name_offset (for string table patching) and btf_type (underlying int type). + // If no dummy var (fallback): search for a 4-byte int type directly, with no name_offset. + // Both paths provide an int_btf_id to ensure type consistency in datasec variable entries. + let (dummy_var_name_offset, int_btf_id) = if let Some(dummy_id) = dummy_var_id { + let dummy_type = &self.types.types[dummy_id as usize]; + if let BtfType::Var(v) = dummy_type { + (Some(v.name_offset), v.btf_type) + } else { + return Err(BtfError::InvalidDatasec); + } + } else { + let int_id = self + .types + .types + .iter() + .enumerate() + .find_map(|(idx, t)| { + if let BtfType::Int(int_type) = t { + (int_type.size == 4).then_some(idx as u32) + } else { + None + } + }) + .ok_or(BtfError::InvalidDatasec)?; + + (None, int_id) + }; + + let datasec_name = { + let datasec = &self.types.types[datasec_id as usize]; + let BtfType::DataSec(d) = datasec else { + return Err(BtfError::InvalidDatasec); + }; + self.string_at(d.name_offset)?.into_owned() + }; + + debug!("DATASEC {datasec_name}: fixing up extern ksyms"); + + let entry_type_ids: Vec = { + let BtfType::DataSec(d) = &self.types.types[datasec_id as usize] else { + return Err(BtfError::InvalidDatasec); + }; + d.entries.iter().map(|e| e.btf_type).collect() + }; + + let mut offset = 0u32; + let size = mem::size_of::() as u32; + + for (i, &type_id) in entry_type_ids.iter().enumerate() { + let type_kind = match &self.types.types[type_id as usize] { + BtfType::Func(_) => "func", + BtfType::Var(_) => "var", + _ => return Err(BtfError::InvalidDatasec), + }; + + match type_kind { + "func" => { + let (func_name, proto_id) = { + let BtfType::Func(f) = &self.types.types[type_id as usize] else { + return Err(BtfError::InvalidDatasec); + }; + (self.string_at(f.name_offset)?.into_owned(), f.btf_type) + }; + + if let BtfType::Func(f) = &mut self.types.types[type_id as usize] { + f.set_linkage(FuncLinkage::Global); + } + + if let Some(dummy_name_off) = dummy_var_name_offset { + if let BtfType::FuncProto(func_proto) = + &mut self.types.types[proto_id as usize] + { + for param in &mut func_proto.params { + if param.btf_type != 0 && param.name_offset == 0 { + param.name_offset = dummy_name_off; + } + } + } + } + + if let (Some(dummy_id), BtfType::DataSec(d)) = + (dummy_var_id, &mut self.types.types[datasec_id as usize]) + { + d.entries[i].btf_type = dummy_id; + } + + debug!("DATASEC {datasec_name}: FUNC {func_name}: fixup offset {offset}"); + } + "var" => { + let var_name = { + let BtfType::Var(v) = &self.types.types[type_id as usize] else { + return Err(BtfError::InvalidDatasec); + }; + self.string_at(v.name_offset)?.into_owned() + }; + + if let BtfType::Var(v) = &mut self.types.types[type_id as usize] { + v.linkage = VarLinkage::Global; + v.btf_type = int_btf_id; + } + + debug!("DATASEC {datasec_name}: VAR {var_name}: fixup offset {offset}"); + } + _ => unreachable!(), + } + + if let BtfType::DataSec(d) = &mut self.types.types[datasec_id as usize] { + d.entries[i].offset = offset; + d.entries[i].size = size; + } + + offset += size; + } + + if let BtfType::DataSec(d) = &mut self.types.types[datasec_id as usize] { + d.size = offset; + debug!("DATASEC {datasec_name}: fixup size to {offset}"); + } + + Ok(()) + } } impl Default for Btf { diff --git a/aya-obj/src/extern_types.rs b/aya-obj/src/extern_types.rs new file mode 100644 index 00000000..18a42bdc --- /dev/null +++ b/aya-obj/src/extern_types.rs @@ -0,0 +1,470 @@ +//! Extern type resolution, relocation +use alloc::{ + string::{String, ToString as _}, + vec::Vec, +}; + +use crate::{ + Object, + btf::{Btf, BtfError, BtfKind, BtfType}, + util::HashMap, +}; + +impl Object { + /// Resolves typed externs through kernel `BTF`. + pub fn resolve_typed_externs( + &mut self, + kernel_btf: &Btf, + ) -> core::result::Result<(), KsymsError> { + if self.externs.externs.is_empty() { + return Ok(()); + } + + let obj_btf = self.btf.as_ref().ok_or(KsymsError::NoBtf)?; + + let mut resolutions = Vec::new(); + + for (name, extern_desc) in self.externs.iter() { + // Skip if extern has no type_id (typeless ksyms will be resolved through kallsyms) + if extern_desc.type_id.is_none() { + continue; + } + + // Dispatch to appropriate resolver + let btf_type = obj_btf.type_by_id(extern_desc.btf_id)?; + let kernel_btf_id = match btf_type { + BtfType::Func(_) => { + self.resolve_extern_function_internal(name, extern_desc, obj_btf, kernel_btf)? + } + BtfType::Var(_) => { + self.resolve_extern_variable_internal(name, extern_desc, obj_btf, kernel_btf)? + } + _ => { + return Err(KsymsError::InvalidExternType { name: name.clone() }); + } + }; + + if let Some(btf_id) = kernel_btf_id { + resolutions.push((name.clone(), btf_id)); + } + } + + for (name, kernel_btf_id) in resolutions { + if let Some(ext) = self.externs.get_mut(&name) { + ext.kernel_btf_id = Some(kernel_btf_id); + ext.is_resolved = true; + } + } + + Ok(()) + } + + /// Resolves typless extern vars found in `.ksyms` section through `kallsyms`. + #[cfg(feature = "std")] + pub fn resolve_typeless_externs(&mut self) -> core::result::Result<(), KsymsError> { + use std::{fs::File, io::BufRead as _}; + + let unresolved: Vec = self.get_unresolved_ksym_vars(); + + if unresolved.is_empty() { + return Ok(()); + } + + let file = File::open("/proc/kallsyms")?; + + let reader = std::io::BufReader::new(file); + let lines: Vec = reader.lines().collect::>()?; + + self.resolve_kallsyms_from_lines(&lines, &unresolved) + } + + fn resolve_kallsyms_from_lines( + &mut self, + lines: &[String], + unresolved: &[String], + ) -> core::result::Result<(), KsymsError> { + let mut resolved_count = 0; + + for line in lines { + // Parse: [] + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() < 3 { + continue; + } + + let addr_str = parts[0]; + let sym_name = parts[2]; + + if !unresolved.iter().any(|s| s.as_str() == sym_name) { + continue; + } + + let addr = u64::from_str_radix(addr_str, 16).map_err(|_| { + KsymsError::KallsymsParseError(alloc::format!("invalid address: {}", addr_str)) + })?; + + if let Some(ext) = self.externs.get_mut(sym_name) { + if ext.is_resolved { + if let Some(existing_addr) = ext.ksym_addr { + if existing_addr != addr { + return Err(KsymsError::AmbiguousResolution { + name: sym_name.to_string(), + first_addr: existing_addr, + second_addr: addr, + }); + } + } + } + + ext.ksym_addr = Some(addr); + ext.is_resolved = true; + resolved_count += 1; + } + + if resolved_count == unresolved.len() { + break; + } + } + + // Check for unresolved non-weak symbols + for (name, ext) in self.externs.externs.iter() { + if ext.extern_type == ExternType::Ksym && !ext.is_resolved && !ext.is_weak { + return Err(KsymsError::UnresolvedExtern { name: name.clone() }); + } + } + + Ok(()) + } + + /// Gets unresolved ksyms variables from [`ExternCollection`]. + fn get_unresolved_ksym_vars(&self) -> Vec { + self.externs + .externs + .iter() + .filter(|(_, ext)| { + ext.extern_type == ExternType::Ksym && !ext.is_func && !ext.is_resolved + }) + .map(|(name, _)| name.clone()) + .collect() + } + + /// Resolves a single exterm function. Returns BTF ID if found, otherwise + /// returns `None`. + fn resolve_extern_function_internal( + &self, + name: &str, + extern_desc: &ExternDesc, + obj_btf: &Btf, + kernel_btf: &Btf, + ) -> core::result::Result, KsymsError> { + let lookup_name = extern_desc.essent_name.as_deref().unwrap_or(name); + let kernel_func_id = match kernel_btf.id_by_type_name_kind(lookup_name, BtfKind::Func) { + Ok(id) => id, + Err(_) => { + if extern_desc.is_weak { + return Ok(None); + } + + return Err(KsymsError::FunctionNotFound { + name: lookup_name.to_string(), + }); + } + }; + + let kernel_func_type = kernel_btf.type_by_id(kernel_func_id)?; + let kernel_proto_id = match kernel_func_type { + BtfType::Func(func) => func.btf_type, + _ => { + return Err(KsymsError::BtfError(BtfError::UnexpectedBtfType { + type_id: kernel_func_id, + })); + } + }; + + let local_proto_id = + extern_desc + .type_id + .ok_or(KsymsError::BtfError(BtfError::UnknownBtfType { + type_id: 0, + }))?; + + let compatible = + crate::btf::types_are_compatible(obj_btf, local_proto_id, kernel_btf, kernel_proto_id)?; + + if !compatible { + if extern_desc.is_weak { + return Ok(None); + } + return Err(KsymsError::IncompatibleFunctionSignature { + name: lookup_name.to_string(), + }); + } + + Ok(Some(kernel_func_id)) + } + + /// Resolves a single exterm variable. Returns BTF ID if found, otherwise + /// returns `None`. + fn resolve_extern_variable_internal( + &self, + name: &str, + extern_desc: &ExternDesc, + obj_btf: &Btf, + kernel_btf: &Btf, + ) -> core::result::Result, KsymsError> { + let kernel_var_id = match kernel_btf.id_by_type_name_kind(name, BtfKind::Var) { + Ok(id) => id, + Err(_) => { + if extern_desc.is_weak { + return Ok(None); + } + return Err(KsymsError::VariableNotFound { + name: name.to_string(), + }); + } + }; + + let kernel_var_type = kernel_btf.type_by_id(kernel_var_id)?; + let kernel_type_id = match kernel_var_type { + BtfType::Var(var) => var.btf_type, + _ => { + return Err(KsymsError::BtfError(BtfError::UnexpectedBtfType { + type_id: kernel_var_id, + })); + } + }; + + let local_type_id = + extern_desc + .type_id + .ok_or(KsymsError::BtfError(BtfError::UnknownBtfType { + type_id: 0, + }))?; + + let compatible = + crate::btf::types_are_compatible(obj_btf, local_type_id, kernel_btf, kernel_type_id)?; + + if !compatible { + return Err(KsymsError::IncompatibleVariableType { + name: name.to_string(), + }); + } + + Ok(Some(kernel_var_id)) + } +} + +/// Errors that can occur during `ksyms`` operation fails +#[derive(Debug, thiserror::Error)] +pub enum KsymsError { + /// A non-weak extern variable was not found in kernel BTF + #[error("kernel variable '{name}' not found in kernel BTF or kallsyms")] + VariableNotFound { + /// The name of the variable that was not found + name: String, + }, + + /// The extern function's signature is incompatible with the kernel function + #[error("kernel function '{name}' has incompatible signature")] + IncompatibleFunctionSignature { + /// The name of the function with incompatible signature + name: String, + }, + + /// The extern variable's type is incompatible with the kernel variable + #[error("kernel variable '{name}' has incompatible type")] + IncompatibleVariableType { + /// The name of the variable with incompatible type + name: String, + }, + + /// The extern symbol has an invalid BTF type (neither Func nor Var) + #[error("extern '{name}' has invalid BTF type (neither Func nor Var)")] + InvalidExternType { + /// The name of the extern with invalid type + name: String, + }, + + /// An error occurred while working with BTF data + #[error("BTF error: {0}")] + BtfError(#[from] BtfError), + + /// The object file has no BTF information + #[error("object has no BTF information")] + NoBtf, + + /// Resolved Extern's kallsyms address does not match agains the loaded kallsyms + #[error("extern (ksym) '{name}': resolution is ambiguous: {first_addr:#x} or {second_addr:#x}")] + AmbiguousResolution { + /// The name of the symbol with ambiguous resolution + name: String, + /// The first address found + first_addr: u64, + /// The second (conflicting) address found + second_addr: u64, + }, + + /// Could not read kallsyms entries + #[cfg(feature = "std")] + #[error("failed to read /proc/kallsyms: {0}")] + KallsymsReadError(#[from] std::io::Error), + + /// Failed to parse kallsyms data + #[error("failed to parse kallsyms: {0}")] + KallsymsParseError(String), + + /// An unresolved non-weak extern was encountered during patching + #[error( + "extern symbol '{name}' is not resolved (non-weak externs must be resolved before patching)" + )] + UnresolvedExtern { + /// The name of the unresolved extern + name: String, + }, + + /// Function not found when trying to patch its instructions + #[error("function '{name}' not found")] + FunctionNotFound { + /// The section index + name: String, + }, +} + +/// Type of extern symbol +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExternType { + /// Kernel configuration variable (.kconfig section) + Kconfig, + /// Kernel symbol - variable or function (.ksyms section) + Ksym, +} + +/// Descriptor for an extern symbol +#[derive(Debug, Clone)] +pub(crate) struct ExternDesc { + /// Symbol name + pub(crate) name: String, + + /// Type of extern (Kconfig or Ksym) + pub(crate) extern_type: ExternType, + + /// BTF type ID in local (program) BTF + pub(crate) btf_id: u32, + + /// Whether this is a weak symbol + pub(crate) is_weak: bool, + + /// Whether this is a function (vs variable) + pub(crate) is_func: bool, + + /// Whether extern has been resolved + pub(crate) is_resolved: bool, + + /// For ksym: kernel BTF ID (after resolution) + pub(crate) kernel_btf_id: Option, + + /// For ksym variables: resolved kernel address + pub(crate) ksym_addr: Option, + + /// For ksym: resolved type ID (after skipping modifiers/typedefs) + pub(crate) type_id: Option, + + /// For names with flavors: stripped essential name + pub(crate) essent_name: Option, +} + +/// Given 'some_struct_name___with_flavor' return the length of a name prefix +/// before last triple underscore. Struct name part after last triple +/// underscore is ignored by BPF CO-RE relocation during relocation matching. +fn essential_name_len(name: &str) -> usize { + let n = name.len(); + if n < 5 { + return n; + } + + for i in (0..=n - 5).rev() { + if is_flavor_sep(name, i) { + return i + 1; + } + } + + n +} + +fn is_flavor_sep(s: &str, pos: usize) -> bool { + let bytes = s.as_bytes(); + if pos + 4 >= bytes.len() { + return false; + } + bytes[pos] != b'_' + && bytes[pos + 1] == b'_' + && bytes[pos + 2] == b'_' + && bytes[pos + 3] == b'_' + && bytes[pos + 4] != b'_' +} + +impl ExternDesc { + pub(crate) fn new( + name: String, + extern_type: ExternType, + btf_id: u32, + is_weak: bool, + is_func: bool, + ) -> Self { + let essent_len = essential_name_len(&name); + let essent_name = if essent_len != name.len() { + Some(name[..essent_len].to_string()) + } else { + None + }; + + Self { + name: name.clone(), + extern_type, + btf_id, + is_weak, + is_func, + is_resolved: false, + kernel_btf_id: None, + ksym_addr: None, + type_id: None, + essent_name, + } + } +} + +/// Collection of extern symbols +#[derive(Debug, Default, Clone)] +pub struct ExternCollection { + /// Map of extern descriptors by name + pub(crate) externs: HashMap, + + /// BTF ID of dummy ksym variable (if created) + pub(crate) dummy_ksym_var_id: Option, +} + +impl ExternCollection { + pub(crate) fn new() -> Self { + Self::default() + } + + pub(crate) fn insert(&mut self, name: String, desc: ExternDesc) { + self.externs.insert(name, desc); + } + + pub(crate) fn get_mut(&mut self, name: &str) -> Option<&mut ExternDesc> { + self.externs.get_mut(name) + } + + pub(crate) fn iter(&self) -> impl Iterator { + self.externs.iter() + } + + pub(crate) fn set_dummy_var_id(&mut self, id: u32) { + self.dummy_ksym_var_id = Some(id); + } + + pub(crate) fn is_empty(&self) -> bool { + self.externs.is_empty() + } +} diff --git a/aya-obj/src/lib.rs b/aya-obj/src/lib.rs index 6a6f4479..6c2c0023 100644 --- a/aya-obj/src/lib.rs +++ b/aya-obj/src/lib.rs @@ -76,6 +76,7 @@ extern crate alloc; extern crate std; pub mod btf; +pub mod extern_types; #[expect( clippy::all, clippy::cast_lossless, @@ -97,6 +98,7 @@ pub mod programs; pub mod relocation; mod util; +pub use extern_types::KsymsError; pub use maps::Map; pub use obj::*; diff --git a/aya-obj/src/obj.rs b/aya-obj/src/obj.rs index b451595a..02b4cf40 100644 --- a/aya-obj/src/obj.rs +++ b/aya-obj/src/obj.rs @@ -19,8 +19,10 @@ use object::{ use crate::{ btf::{ - Array, Btf, BtfError, BtfExt, BtfFeatures, BtfType, DataSecEntry, FuncSecInfo, LineSecInfo, + Array, Btf, BtfError, BtfExt, BtfFeatures, BtfType, DataSec, DataSecEntry, FuncSecInfo, + LineSecInfo, }, + extern_types::{ExternCollection, ExternDesc, ExternType}, generated::{ BPF_CALL, BPF_F_RDONLY_PROG, BPF_JMP, BPF_K, bpf_func_id::*, bpf_insn, bpf_map_info, bpf_map_type::BPF_MAP_TYPE_ARRAY, @@ -141,6 +143,9 @@ pub struct Object { pub(crate) symbol_table: HashMap, pub(crate) symbols_by_section: HashMap>, pub(crate) section_infos: HashMap, + /// Extern functions parsed from ksyms section + pub(crate) externs: ExternCollection, + // symbol_offset_by_name caches symbols that could be referenced from a // BTF VAR type so the offsets can be fixed up pub(crate) symbol_offset_by_name: HashMap, @@ -473,6 +478,7 @@ impl Object { size: symbol.size(), is_definition: symbol.is_definition(), kind: symbol.kind(), + is_weak: symbol.is_weak(), }; bpf_obj.symbol_table.insert(symbol.index().0, sym); if let Some(section_idx) = symbol.section().index() { @@ -493,6 +499,8 @@ impl Object { // when parsing program sections if let Some(s) = obj.section_by_name(".BTF") { bpf_obj.parse_section(Section::try_from(&s)?)?; + + bpf_obj.collect_ksyms_from_btf()?; if let Some(s) = obj.section_by_name(".BTF.ext") { bpf_obj.parse_section(Section::try_from(&s)?)?; } @@ -526,6 +534,7 @@ impl Object { symbols_by_section: HashMap::new(), section_infos: HashMap::new(), symbol_offset_by_name: HashMap::new(), + externs: ExternCollection::new(), } } @@ -831,6 +840,179 @@ impl Object { Ok(()) } + fn create_dummy_ksym_var(&mut self) -> Result { + let btf = self.btf.as_mut().ok_or(ParseError::NoBTF)?; + + let int_type_id = { + let mut found_id = None; + for (idx, t) in btf.types().enumerate() { + if let BtfType::Int(int) = t { + if int.size == 4 { + found_id = Some((idx) as u32); + break; + } + } + } + found_id + }; + + let int_type_id = if let Some(id) = int_type_id { + id + } else { + let name_offset = btf.add_string("int"); + btf.add_type(BtfType::Int(crate::btf::Int::new( + name_offset, + 4, + crate::btf::IntEncoding::Signed, + 0, + ))) + }; + + debug!("Found/created int type_id: {}", int_type_id); + if let Ok(BtfType::Int(int)) = btf.type_by_id(int_type_id) { + debug!( + "Int type size: {}, encoding: {:?}", + int.size, + int.encoding() + ); + } + + let name_offset = btf.add_string("dummy_ksym"); + let dummy_var_id = btf.add_type(BtfType::Var(crate::btf::Var::new( + name_offset, + int_type_id, + crate::btf::VarLinkage::Global, + ))); + + debug!("Created dummy_var type_id: {}", dummy_var_id); + if let Ok(BtfType::Var(var)) = btf.type_by_id(dummy_var_id) { + debug!("Dummy var points to type_id: {}", var.btf_type); + } + + Ok(dummy_var_id) + } + + /// Collects extern kernel symbols from BTF datasec entries. + fn collect_ksyms_from_btf(&mut self) -> Result<(), ParseError> { + let Some((datasec_id, datasec)) = self.find_ksyms_datasec()? else { + return Ok(()); + }; + + if Self::datasec_has_functions(self.btf.as_ref().unwrap(), &datasec) { + let dummy_var_id = self.create_dummy_ksym_var()?; + self.externs.set_dummy_var_id(dummy_var_id); + } + + let collected = self.collect_extern_entries(&datasec)?; + + for extern_desc in collected { + self.externs.insert(extern_desc.name.clone(), extern_desc); + } + + if !self.externs.is_empty() { + self.fixup_ksyms_btf(datasec_id)?; + } + + Ok(()) + } + + /// Searches for the `.ksyms` datasec in BTF, returns it if found. + fn find_ksyms_datasec(&self) -> Result, ParseError> { + let Some(btf) = self.btf.as_ref() else { + return Ok(None); + }; + + for (idx, btf_type) in btf.types().enumerate() { + if let BtfType::DataSec(datasec) = btf_type { + let name = btf.type_name(btf_type).map_err(|_| ParseError::NoBTF)?; + if name == ".ksyms" { + return Ok(Some((idx as u32, datasec.clone()))); + } + } + } + + Ok(None) + } + + /// Checks if datasec contains any functions. + fn datasec_has_functions(btf: &Btf, datasec: &DataSec) -> bool { + datasec.entries.iter().any(|entry| { + btf.type_by_id(entry.btf_type) + .map(|t| matches!(t, BtfType::Func(_))) + .unwrap_or(false) + }) + } + + /// Collects extern descriptors from datasec entries. + fn collect_extern_entries(&self, datasec: &DataSec) -> Result, ParseError> { + let btf = self.btf.as_ref().ok_or(ParseError::NoBTF)?; + let mut result = Vec::new(); + + for entry in &datasec.entries { + let Some(extern_desc) = self.process_datasec_entry(btf, entry)? else { + continue; + }; + + result.push(extern_desc); + } + + Ok(result) + } + + /// Processes a single datasec entry, returns [`ExternDesc`] if it's an extern. + fn process_datasec_entry( + &self, + btf: &Btf, + entry: &DataSecEntry, + ) -> Result, ParseError> { + let btf_type = btf.type_by_id(entry.btf_type)?; + + let (name, is_func, resolved_type_id) = match btf_type { + BtfType::Func(func) => { + let name = btf.string_at(func.name_offset)?.into_owned(); + let resolved_type = btf.resolve_type(func.btf_type).ok(); + (name, true, resolved_type) + } + BtfType::Var(var) => { + let name = btf.string_at(var.name_offset)?.into_owned(); + let resolved_type = btf.resolve_type(var.btf_type).ok(); + (name, false, resolved_type) + } + _ => return Ok(None), + }; + + let symbol = self + .find_symbol_by_name(&name) + .ok_or(ParseError::SymbolNotFound { name: name.clone() })?; + + let mut extern_desc = ExternDesc::new( + name, + ExternType::Ksym, + entry.btf_type, + symbol.is_weak, + is_func, + ); + + extern_desc.type_id = resolved_type_id; + + Ok(Some(extern_desc)) + } + + /// Applies BTF fixups for `.ksyms` datasec. + fn fixup_ksyms_btf(&mut self, datasec_id: u32) -> Result<(), ParseError> { + let btf = self.btf.as_mut().ok_or(ParseError::NoBTF)?; + let dummy_var_id = self.externs.dummy_ksym_var_id; + + btf.fixup_ksyms_datasec(datasec_id, dummy_var_id) + .map_err(ParseError::BtfError) + } + /// Returns a symbol with the given `name` from the symbol table. + fn find_symbol_by_name(&self, name: &str) -> Option<&Symbol> { + self.symbol_table + .values() + .find(|sym| sym.name.as_deref() == Some(name)) + } + fn parse_section(&mut self, section: Section<'_>) -> Result<(), ParseError> { self.section_infos .insert(section.name.to_owned(), (section.index, section.size)); @@ -998,6 +1180,23 @@ pub enum ParseError { /// No BTF parsed for object #[error("no BTF parsed for object")] NoBTF, + + /// Extern weak function not supported + #[error("extern weak function '{name}' is not supported in this implementation")] + WeakFuncNotSupported { name: String }, + + /// Extern function in kconfig section not supported + #[error( + "extern function '{name}' cannot be used in {section} section (only variables allowed)" + )] + FuncInKconfig { name: String, section: String }, + + #[error("duplicate extern symbol '{name}' found in extern section")] + DuplicateExtern { name: String }, + + /// Invalid kconfig variable size + #[error("extern kconfig variable '{name}' has invalid or zero size")] + InvalidKconfigSize { name: String }, } /// Invalid bindings to the bpf type from the parsed/received value. @@ -1084,6 +1283,7 @@ impl<'a> TryFrom<&'a ObjSection<'_, '_>> for Section<'a> { error, }; let name = section.name().map_err(map_err)?; + //println!("Section name altug got: {}", name); let kind = match EbpfSectionKind::from_name(name) { EbpfSectionKind::Undefined => { if section.kind() == SectionKind::Text && section.size() > 0 { @@ -1509,6 +1709,7 @@ mod tests { size, is_definition: false, kind: SymbolKind::Text, + is_weak: false, }, ); obj.symbols_by_section @@ -2666,6 +2867,7 @@ mod tests { size: 3, is_definition: true, kind: SymbolKind::Data, + is_weak: false, }, ); diff --git a/aya-obj/src/relocation.rs b/aya-obj/src/relocation.rs index 850eaaa3..998c522a 100644 --- a/aya-obj/src/relocation.rs +++ b/aya-obj/src/relocation.rs @@ -8,6 +8,7 @@ use object::{SectionIndex, SymbolKind}; use crate::{ EbpfSectionKind, + extern_types::ExternDesc, generated::{ BPF_CALL, BPF_JMP, BPF_K, BPF_PSEUDO_CALL, BPF_PSEUDO_FUNC, BPF_PSEUDO_MAP_FD, BPF_PSEUDO_MAP_VALUE, bpf_insn, @@ -23,6 +24,8 @@ type RawFd = std::os::fd::RawFd; type RawFd = core::ffi::c_int; pub(crate) const INS_SIZE: usize = mem::size_of::(); +pub(crate) const BPF_PSEUDO_KFUNC_CALL: u32 = 2; +pub(crate) const BPF_PSEUDO_BTF_ID: u32 = 3; /// The error type returned by [`Object::relocate_maps`] and [`Object::relocate_calls`] #[derive(thiserror::Error, Debug)] @@ -85,6 +88,26 @@ pub enum RelocationError { /// The relocation number relocation_number: usize, }, + /// Extern not found + #[error("extern `{name}` not found")] + ExternNotFound { + /// Name of the extern symbol + name: String, + }, + + /// Unresolved extern + #[error("extern `{name}` was not resolved against kernel BTF")] + UnresolvedExtern { + /// Name of the extern symbol + name: String, + }, + + /// Missing kernel BTF ID + #[error("extern `{name}` is missing kernel BTF ID")] + MissingKallsymsAddr { + /// Name of the extern symbol + name: String, + }, } #[derive(Debug, Copy, Clone)] @@ -105,6 +128,17 @@ pub(crate) struct Symbol { pub(crate) size: u64, pub(crate) is_definition: bool, pub(crate) kind: SymbolKind, + pub(crate) is_weak: bool, +} + +impl Symbol { + /// Returns true if this symbol is an extern (undefined) symbol + pub(crate) fn is_extern(&self) -> bool { + self.section_index.is_none() + && self.name.is_some() + && !self.is_definition + && self.kind == SymbolKind::Unknown + } } impl Object { @@ -143,6 +177,32 @@ impl Object { Ok(()) } + /// Relocates extern ksym references after BTF resolution + pub fn relocate_externs(&mut self) -> Result<(), EbpfRelocationError> { + for (name, extern_desc) in &self.externs.externs { + debug!( + "[DEBUG] Extern '{}': resolved={}, btf_id={:?}", + name, extern_desc.is_resolved, extern_desc.kernel_btf_id + ); + } + + for function in self.functions.values_mut() { + if let Some(relocations) = self.relocations.get(&function.section_index) { + relocate_externs( + function, + relocations.values(), + &self.externs.externs, + &self.symbol_table, + ) + .map_err(|error| EbpfRelocationError { + function: function.name.clone(), + error, + })?; + } + } + Ok(()) + } + /// Relocates function calls pub fn relocate_calls( &mut self, @@ -181,6 +241,100 @@ impl Object { } } +fn relocate_externs<'a, I: Iterator>( + fun: &mut Function, + relocations: I, + externs: &HashMap, + symbol_table: &HashMap, +) -> Result<(), RelocationError> { + let section_offset = fun.section_offset; + let instructions = &mut fun.instructions; + let function_size = instructions.len() * INS_SIZE; + + for (rel_n, rel) in relocations.enumerate() { + let rel_offset = rel.offset as usize; + if rel_offset < section_offset || rel_offset >= section_offset + function_size { + continue; + } + + let ins_offset = rel_offset - section_offset; + if ins_offset % INS_SIZE != 0 { + return Err(RelocationError::InvalidRelocationOffset { + offset: rel.offset, + relocation_number: rel_n, + }); + } + let ins_index = ins_offset / INS_SIZE; + + let sym = symbol_table + .get(&rel.symbol_index) + .ok_or(RelocationError::UnknownSymbol { + index: rel.symbol_index, + })?; + + // Only process extern symbols + if !sym.is_extern() { + continue; + } + + let extern_name = sym.name.as_ref().unwrap(); + let extern_desc = externs + .get(extern_name) + .ok_or(RelocationError::ExternNotFound { + name: extern_name.clone(), + })?; + + let ins = &mut instructions[ins_index]; + let is_call = insn_is_call(ins); + + if is_call { + ins.set_src_reg(BPF_PSEUDO_KFUNC_CALL as u8); + if extern_desc.is_resolved { + let kernel_btf_id = extern_desc.kernel_btf_id.unwrap(); + ins.imm = kernel_btf_id as i32; + ins.off = 0; // btf_fd_idx, typically 0 for vmlinux + } else { + // Unresolved weak kfunc call + poison_kfunc_call(ins, rel.symbol_index); + } + } else { + // Variable references - check if typed and resolved + if extern_desc.type_id.is_some() && extern_desc.is_resolved { + // Typed ksyms - use BTF ID + let kernel_btf_id = extern_desc.kernel_btf_id.unwrap(); + ins.set_src_reg(BPF_PSEUDO_BTF_ID as u8); + ins.imm = kernel_btf_id as i32; + // Note: For typed ksyms, insn[1].imm would be kernel_btf_obj_fd + // but that's typically 0 for vmlinux symbols + instructions[ins_index + 1].imm = 0; + } else { + // typeless ksyms or unresolved typed ksyms + let addr = extern_desc + .ksym_addr + .ok_or(RelocationError::MissingKallsymsAddr { + name: extern_name.clone(), + })?; + + // Split 64-bit address across two instructions + ins.imm = (addr & 0xFFFFFFFF) as i32; + instructions[ins_index + 1].imm = (addr >> 32) as i32; + } + } + } + + Ok(()) +} + +const POISON_CALL_KFUNC_BASE: i32 = 2002000000; + +fn poison_kfunc_call(ins: &mut bpf_insn, ext_idx: usize) { + ins.code = (BPF_JMP | BPF_CALL) as u8; + ins.set_dst_reg(0); + ins.set_src_reg(0); + ins.off = 0; + ins.imm = POISON_CALL_KFUNC_BASE + ext_idx as i32; +} + fn relocate_maps<'a, I: Iterator>( fun: &mut Function, relocations: I, @@ -369,8 +523,9 @@ impl<'a> FunctionLinker<'a> { .map(|sym| (rel, sym)) }) .filter(|(_rel, sym)| { - // only consider text relocations, data relocations are - // relocated in relocate_maps() + if sym.is_extern() { + return false; + } sym.kind == SymbolKind::Text || sym .section_index @@ -516,6 +671,7 @@ mod test { size, is_definition: false, kind: SymbolKind::Data, + is_weak: false, } } diff --git a/aya/src/bpf.rs b/aya/src/bpf.rs index 609ae553..4740a6af 100644 --- a/aya/src/bpf.rs +++ b/aya/src/bpf.rs @@ -8,7 +8,7 @@ use std::{ }; use aya_obj::{ - EbpfSectionKind, Features, Object, ParseError, ProgramSection, + EbpfSectionKind, Features, KsymsError, Object, ParseError, ProgramSection, btf::{Btf, BtfError, BtfFeatures, BtfRelocationError}, generated::{ BPF_F_SLEEPABLE, BPF_F_XDP_HAS_FRAGS, @@ -94,6 +94,22 @@ pub fn features() -> &'static Features { &FEATURES } +pub(crate) static KERNEL_BTF: LazyLock> = LazyLock::new(|| match Btf::from_sys_fs() { + Ok(btf) => { + debug!("Loaded kernel BTF"); + Some(btf) + } + Err(e) => { + debug!("Failed to load kernel BTF: {}", e); + None + } +}); + +/// Returns a reference to the kernel BTF if available. +pub fn kernel_btf() -> Option<&'static Btf> { + KERNEL_BTF.as_ref() +} + /// Builder style API for advanced loading of eBPF programs. /// /// Loading eBPF code involves a few steps, including loading maps and applying @@ -511,6 +527,12 @@ impl<'a> EbpfLoader<'a> { if let Some(btf) = &btf { obj.relocate_btf(btf)?; } + + if let Some(kernel_btf) = kernel_btf() { + obj.resolve_typed_externs(kernel_btf)?; + obj.resolve_typeless_externs()?; + } + let mut maps = HashMap::new(); for (name, mut obj) in obj.maps.drain() { if let (false, EbpfSectionKind::Bss | EbpfSectionKind::Data | EbpfSectionKind::Rodata) = @@ -576,6 +598,7 @@ impl<'a> EbpfLoader<'a> { .map(|(s, data)| (s.as_str(), data.fd().as_fd().as_raw_fd(), data.obj())), &text_sections, )?; + obj.relocate_externs()?; obj.relocate_calls(&text_sections)?; obj.sanitize_functions(&FEATURES); @@ -1173,6 +1196,10 @@ pub enum EbpfError { #[error("error relocating section")] BtfRelocationError(#[from] BtfRelocationError), + /// Error patching extern kernel symbol instructions + #[error("kernel symbol instruction patching error: {0}")] + KsymsError(#[from] KsymsError), + /// No BTF parsed for object #[error("no BTF parsed for object")] NoBTF, diff --git a/test/integration-test/bpf/ksyms.bpf.c b/test/integration-test/bpf/ksyms.bpf.c new file mode 100644 index 00000000..10a71966 --- /dev/null +++ b/test/integration-test/bpf/ksyms.bpf.c @@ -0,0 +1,32 @@ +#include +#include +#include +#include + +char _license[] SEC("license") = "GPL"; + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, 1); + __type(key, __u32); + __type(value, __u64); +} counter_map SEC(".maps"); + +extern void bpf_rcu_read_lock(void) __attribute__((section(".ksyms"))); +extern void bpf_rcu_read_unlock(void) __attribute__((section(".ksyms"))); + +SEC("tp_btf/sys_enter") +int BPF_PROG(sys_enter, struct pt_regs *regs, long id) { + __u32 key = 0; + __u64 *count; + + bpf_rcu_read_lock(); + + count = bpf_map_lookup_elem(&counter_map, &key); + if (count) { + __sync_fetch_and_add(count, 1); + } + + bpf_rcu_read_unlock(); + return 0; +} \ No newline at end of file diff --git a/test/integration-test/build.rs b/test/integration-test/build.rs index 44a4783a..4bd4751b 100644 --- a/test/integration-test/build.rs +++ b/test/integration-test/build.rs @@ -84,6 +84,7 @@ fn main() -> Result<()> { ("struct_flavors_reloc.bpf.c", true), ("text_64_64_reloc.c", false), ("variables_reloc.bpf.c", false), + ("ksyms.bpf.c", true), ]; const C_BPF_HEADERS: &[&str] = &["reloc.h", "struct_with_scalars.h"]; diff --git a/test/integration-test/src/lib.rs b/test/integration-test/src/lib.rs index 64f37d18..e3e2212f 100644 --- a/test/integration-test/src/lib.rs +++ b/test/integration-test/src/lib.rs @@ -11,6 +11,7 @@ bpf_file!( ITER_TASK => "iter.bpf.o", MAIN => "main.bpf.o", MULTIMAP_BTF => "multimap-btf.bpf.o", + KSYMS => "ksyms.bpf.o", ENUM_SIGNED_32_RELOC_BPF => "enum_signed_32_reloc.bpf.o", ENUM_SIGNED_32_RELOC_BTF => "enum_signed_32_reloc.bpf.target.o", diff --git a/test/integration-test/src/tests.rs b/test/integration-test/src/tests.rs index b7d4d492..dc7a1aa3 100644 --- a/test/integration-test/src/tests.rs +++ b/test/integration-test/src/tests.rs @@ -5,6 +5,7 @@ mod elf; mod feature_probe; mod info; mod iter; +mod ksyms; mod linear_data_structures; mod load; mod log; diff --git a/test/integration-test/src/tests/ksyms.rs b/test/integration-test/src/tests/ksyms.rs new file mode 100644 index 00000000..feaba483 --- /dev/null +++ b/test/integration-test/src/tests/ksyms.rs @@ -0,0 +1,14 @@ +use aya::{Btf, Ebpf, programs::BtfTracePoint}; +use test_log::test; + +#[test] +fn test_ksym() { + let env = env!("OUT_DIR"); + println!("out dir {}", env); + let mut ebpf = Ebpf::load(crate::KSYMS).unwrap(); + + let prog: &mut BtfTracePoint = ebpf.program_mut("sys_enter").unwrap().try_into().unwrap(); + let btf = Btf::from_sys_fs().unwrap(); + prog.load("sys_enter", &btf).unwrap(); + prog.attach().unwrap(); +}