diff --git a/aya-obj/src/btf/btf.rs b/aya-obj/src/btf/btf.rs index ff28b164..f1f511bc 100644 --- a/aya-obj/src/btf/btf.rs +++ b/aya-obj/src/btf/btf.rs @@ -24,6 +24,7 @@ use crate::{ info::{FuncSecInfo, LineSecInfo}, relocation::Relocation, }, + extern_types::ExternCollection, generated::{btf_ext_header, btf_header}, util::{HashMap, bytes_of}, }; @@ -264,6 +265,8 @@ pub struct Btf { strings: Vec, types: BtfTypes, _endianness: Endianness, + /// Extern functions parsed from ksyms section + pub(crate) externs: ExternCollection, } fn add_type(header: &mut btf_header, types: &mut BtfTypes, btf_type: BtfType) -> u32 { @@ -292,6 +295,7 @@ impl Btf { strings: vec![0], types: BtfTypes::default(), _endianness: Endianness::default(), + externs: ExternCollection::new(), } } @@ -364,6 +368,7 @@ impl Btf { strings, types, _endianness: endianness, + externs: ExternCollection::new(), }) } @@ -501,6 +506,9 @@ impl Btf { symbol_offsets: &HashMap, features: &BtfFeatures, ) -> Result<(), BtfError> { + if !self.externs.is_empty() { + self.fixup_ksyms_datasec(self.externs.datasec_id, self.externs.dummy_ksym_var_id)?; + } let enum64_placeholder_id = OnceCell::new(); let filler_var_id = OnceCell::new(); let mut types = mem::take(&mut self.types); @@ -812,6 +820,128 @@ 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: Option, + 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_id = datasec_id.ok_or(BtfError::InvalidDatasec)?; + + 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() { + match &self.types.types[type_id as usize] { + BtfType::Func(f) => { + let (func_name, proto_id) = + { (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}"); + } + BtfType::Var(v) => { + let var_name = { 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/btf/extern_types.rs b/aya-obj/src/btf/extern_types.rs new file mode 100644 index 00000000..e2b96275 --- /dev/null +++ b/aya-obj/src/btf/extern_types.rs @@ -0,0 +1,178 @@ +use alloc::vec::Vec; + +use log::debug; + +use crate::{ + KsymsError, Object, + btf::{Btf, BtfError, BtfType, DataSec, DataSecEntry}, + extern_types::{ExternDesc, ExternType}, + relocation::Symbol, + util::HashMap, +}; +impl Btf { + /// Creates a dummy global variable named "dummy_ksym" with a 4-byte int type for unresolved kernel symbols. + pub(crate) fn create_dummy_ksym_var(&mut self) -> Result { + let int_type_id = { + let mut found_id = None; + for (idx, t) in self.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 = self.add_string("int"); + self.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)) = self.type_by_id(int_type_id) { + debug!( + "Int type size: {}, encoding: {:?}", + int.size, + int.encoding() + ); + } + + let name_offset = self.add_string("dummy_ksym"); + let dummy_var_id = self.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)) = self.type_by_id(dummy_var_id) { + debug!("Dummy var points to type_id: {}", var.btf_type); + } + + Ok(dummy_var_id) + } + + /// Searches for the `.ksyms` datasec in BTF, returns it if found. + fn find_ksyms_datasec(&self) -> Result, BtfError> { + for (idx, btf_type) in self.types().enumerate() { + if let BtfType::DataSec(datasec) = btf_type { + let name = self.type_name(btf_type)?; + if name == ".ksyms" { + return Ok(Some((idx as u32, datasec.clone()))); + } + } + } + + Ok(None) + } + + /// Checks if datasec contains any functions. + pub(crate) fn datasec_has_functions(&self, datasec: &DataSec) -> bool { + datasec.entries.iter().any(|entry| { + self.type_by_id(entry.btf_type) + .map(|t| matches!(t, BtfType::Func(_))) + .unwrap_or(false) + }) + } + + /// Collects extern descriptors from datasec entries. + pub(crate) fn collect_extern_entries( + &self, + datasec: &DataSec, + symbol_table: &HashMap, + ) -> Result, BtfError> { + let mut result = Vec::new(); + + for entry in &datasec.entries { + let Some(extern_desc) = self.process_datasec_entry(entry, symbol_table)? 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, + entry: &DataSecEntry, + symbol_table: &HashMap, + ) -> Result, BtfError> { + let btf_type = self.type_by_id(entry.btf_type)?; + + let (name, is_func, resolved_type_id) = match btf_type { + BtfType::Func(func) => { + let name = self.string_at(func.name_offset)?.into_owned(); + let resolved_type = self.resolve_type(func.btf_type).ok(); + (name, true, resolved_type) + } + BtfType::Var(var) => { + let name = self.string_at(var.name_offset)?.into_owned(); + let resolved_type = self.resolve_type(var.btf_type).ok(); + (name, false, resolved_type) + } + _ => return Ok(None), + }; + + let symbol = find_symbol_by_name(symbol_table, &name).ok_or(BtfError::InvalidSymbolName)?; + + 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)) + } +} + +fn find_symbol_by_name<'a>( + symbol_table: &'a HashMap, + name: &str, +) -> Option<&'a Symbol> { + symbol_table + .values() + .find(|sym| sym.name.as_deref() == Some(name)) +} + +impl Object { + /// Collects extern kernel symbols from BTF datasec entries. + pub fn collect_ksyms_from_btf(&mut self) -> Result<(), KsymsError> { + let btf = self.btf.as_mut().ok_or(KsymsError::NoBtf)?; + let Some((datasec_id, datasec)) = btf.find_ksyms_datasec()? else { + return Ok(()); + }; + + if btf.datasec_has_functions(&datasec) { + let dummy_var_id = btf.create_dummy_ksym_var()?; + btf.externs.set_dummy_var_id(dummy_var_id); + } + + let collected = btf.collect_extern_entries(&datasec, &self.symbol_table)?; + + for extern_desc in collected { + btf.externs.insert(extern_desc.name.clone(), extern_desc); + } + + if !btf.externs.is_empty() { + btf.externs.datasec_id = Some(datasec_id); + } + + Ok(()) + } +} diff --git a/aya-obj/src/btf/mod.rs b/aya-obj/src/btf/mod.rs index a5faf817..3509a3b2 100644 --- a/aya-obj/src/btf/mod.rs +++ b/aya-obj/src/btf/mod.rs @@ -2,6 +2,7 @@ #[expect(clippy::module_inception)] mod btf; +mod extern_types; mod info; mod relocation; mod types; diff --git a/aya-obj/src/extern_types.rs b/aya-obj/src/extern_types.rs new file mode 100644 index 00000000..79036ded --- /dev/null +++ b/aya-obj/src/extern_types.rs @@ -0,0 +1,480 @@ +//! 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: &mut Btf, + ) -> core::result::Result<(), KsymsError> { + let mut resolutions = Vec::new(); + { + let obj_btf = self.btf.as_ref().ok_or(KsymsError::NoBtf)?; + + for (name, extern_desc) in obj_btf.externs.iter() { + if extern_desc.type_id.is_none() { + continue; + } + + 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, kernel_btf)? + } + BtfType::Var(_) => { + self.resolve_extern_variable_internal(name, extern_desc, kernel_btf)? + } + _ => { + return Err(KsymsError::InvalidExternType { name: name.clone() }); + } + }; + + if let Some(btf_id) = kernel_btf_id { + resolutions.push((name.clone(), btf_id)); + } + } + } + + let obj_mut = self.btf.as_mut().ok_or(KsymsError::NoBtf)?; + + for (name, kernel_btf_id) in resolutions { + if let Some(ext) = obj_mut.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)?; + + Ok(()) + } + + fn resolve_kallsyms_from_lines( + &mut self, + lines: &[String], + unresolved: &[String], + ) -> core::result::Result<(), KsymsError> { + let Some(obj_btf) = self.btf.as_mut() else { + return Err(KsymsError::NoBtf); + }; + + 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) = obj_btf.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 obj_btf.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 { + let Some(obj_btf) = self.btf.as_ref() else { + return Vec::new(); + }; + + obj_btf + .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, + 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 obj_btf = self.btf.as_ref().ok_or(KsymsError::NoBtf)?; + 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, + 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 obj_btf = self.btf.as_ref().ok_or(KsymsError::NoBtf)?; + 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, + + /// Index ID of `.ksyms`datasec entry in BTF types. + pub(crate) datasec_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..bff6ba9f 100644 --- a/aya-obj/src/obj.rs +++ b/aya-obj/src/obj.rs @@ -18,6 +18,7 @@ use object::{ }; use crate::{ + KsymsError, btf::{ Array, Btf, BtfError, BtfExt, BtfFeatures, BtfType, DataSecEntry, FuncSecInfo, LineSecInfo, }, @@ -473,6 +474,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() { @@ -496,6 +498,8 @@ impl Object { if let Some(s) = obj.section_by_name(".BTF.ext") { bpf_obj.parse_section(Section::try_from(&s)?)?; } + + bpf_obj.collect_ksyms_from_btf()?; } for s in obj.sections() { @@ -504,7 +508,6 @@ impl Object { continue; } } - bpf_obj.parse_section(Section::try_from(&s)?)?; } @@ -936,6 +939,9 @@ pub enum ParseError { #[error("error parsing ELF data")] ElfError(object::read::Error), + #[error("error collecting Externs: {0}")] + KsymsError(#[from] KsymsError), + /// Error parsing BTF object #[error("BTF error")] BtfError(#[from] BtfError), @@ -998,6 +1004,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 +1107,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 +1533,7 @@ mod tests { size, is_definition: false, kind: SymbolKind::Text, + is_weak: false, }, ); obj.symbols_by_section @@ -2666,6 +2691,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..25801a75 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,35 @@ impl Object { Ok(()) } + /// Relocates extern ksym references after BTF resolution + pub fn relocate_externs(&mut self) -> Result<(), EbpfRelocationError> { + if let Some(obj_btf) = self.btf.as_mut() { + for (name, extern_desc) in &obj_btf.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(), + &obj_btf.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 +244,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 +526,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 +674,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..aef15c45 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, @@ -511,6 +511,12 @@ impl<'a> EbpfLoader<'a> { if let Some(btf) = &btf { obj.relocate_btf(btf)?; } + + if let Some(kernel_btf) = self.btf.as_mut() { + obj.resolve_typed_externs(kernel_btf.to_mut())?; + 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 +582,9 @@ 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 +1182,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-ebpf/Cargo.toml b/test/integration-ebpf/Cargo.toml index cb52e728..60872a2e 100644 --- a/test/integration-ebpf/Cargo.toml +++ b/test/integration-ebpf/Cargo.toml @@ -64,6 +64,10 @@ path = "src/raw_tracepoint.rs" name = "redirect" path = "src/redirect.rs" +[[bin]] +name = "ksyms" +path = "src/ksyms.rs" + [[bin]] name = "relocations" path = "src/relocations.rs" diff --git a/test/integration-ebpf/src/ksyms.rs b/test/integration-ebpf/src/ksyms.rs new file mode 100644 index 00000000..a530b110 --- /dev/null +++ b/test/integration-ebpf/src/ksyms.rs @@ -0,0 +1,80 @@ +#![no_std] +#![no_main] +#![expect(unused_crate_dependencies, reason = "used in other bins")] +use aya_ebpf::{ + macros::{map, tracepoint}, + maps::Array, + programs::TracePointContext, +}; + +#[cfg(not(test))] +extern crate ebpf_panic; + +#[repr(C)] +struct TestData { + counter: u64, + read_value: u64, +} + +#[map] +static DATA_MAP: Array = Array::with_max_entries(1, 0); + +// Opaque type for dynptr +#[repr(C)] +struct bpf_dynptr { + _opaque: [u64; 2], +} + +unsafe extern "C" { + // Kfunc with multiple arguments: (data, size, flags, dynptr) + fn bpf_dynptr_from_mem(data: *mut u8, size: u32, flags: u64, ptr: *mut bpf_dynptr) -> i32; + + // Kfunc with multiple arguments: (dynptr, offset, dst, len) + fn bpf_dynptr_read(ptr: *const bpf_dynptr, offset: u32, dst: *mut u8, len: u32) -> i32; +} + +#[tracepoint] +fn sys_enter(ctx: TracePointContext) -> u32 { + match try_sys_enter(&ctx) { + Ok(_) => 0, + Err(_) => 1, + } +} + +fn try_sys_enter(_ctx: &TracePointContext) -> Result<(), ()> { + unsafe { + if let Some(data) = DATA_MAP.get_ptr_mut(0) { + // Test data + let mut test_val: u64 = 0x1234567890ABCDEF; + let mut dynptr: bpf_dynptr = core::mem::zeroed(); + + // Test kfunc with 4 arguments + let ret = bpf_dynptr_from_mem( + &mut test_val as *mut u64 as *mut u8, + 8, // size + 0, // flags + &mut dynptr, + ); + + if ret == 0 { + let mut read_buf: u64 = 0; + + // Test another kfunc with 4 arguments + let ret = bpf_dynptr_read( + &dynptr, + 0, // offset + &mut read_buf as *mut u64 as *mut u8, + 8, // len + ); + + if ret == 0 { + (*data).read_value = read_buf; + } + } + + (*data).counter = (*data).counter.wrapping_add(1); + } + } + + Ok(()) +} diff --git a/test/integration-test/bpf/ksyms.bpf.c b/test/integration-test/bpf/ksyms.bpf.c new file mode 100644 index 00000000..2b40b401 --- /dev/null +++ b/test/integration-test/bpf/ksyms.bpf.c @@ -0,0 +1,32 @@ +#include "vmlinux.h" +#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 881cdd57..a4423ab8 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..cdf3d13f 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", @@ -58,6 +59,7 @@ bpf_file!( TWO_PROGS => "two_progs", XDP_SEC => "xdp_sec", UPROBE_COOKIE => "uprobe_cookie", + KSYMS_RS => "ksyms", ); #[cfg(test)] 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..bba8ca3d --- /dev/null +++ b/test/integration-test/src/tests/ksyms.rs @@ -0,0 +1,22 @@ +use aya::{ + Btf, Ebpf, + programs::{BtfTracePoint, TracePoint, Xdp}, +}; +use test_log::test; + +#[test] +fn test_ksym_btf_tracepoint() { + 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(); +} + +#[test] +fn test_ksym_tracepoint() { + let mut ebpf = Ebpf::load(crate::KSYMS_RS).unwrap(); + let prog: &mut TracePoint = ebpf.program_mut("sys_enter").unwrap().try_into().unwrap(); + prog.load().unwrap(); + prog.attach("sched", "sched_switch").unwrap(); +}