Skip to content

Commit 05d5269

Browse files
swanananvadorovsky
authored andcommitted
aya: lazily load custom BTF sources
Custom BTF currently has to be read and parsed before configuring an EbpfLoader, even when the object does not require target BTF. Add btf_source(), which accepts a callback invoked only for objects with CO-RE relocations or typed kernel symbols. The callback receives a parser configured for the object endianness, allowing callers to parse borrowed data such as a subslice of an mmap without an intermediate allocation. Cache the first successfully parsed BTF for reuse by the loader, while keeping btf(&Btf) for callers that share one parsed value across multiple loaders.
1 parent 50f78e4 commit 05d5269

3 files changed

Lines changed: 179 additions & 20 deletions

File tree

aya/src/bpf.rs

Lines changed: 132 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ pub fn features() -> &'static Features {
113113
/// ```
114114
#[derive(Debug)]
115115
pub struct EbpfLoader<'a> {
116-
btf: Option<Cow<'a, Btf>>,
116+
btf: TargetBtf<'a>,
117117
default_map_pin_directory: Option<PathBuf>,
118118
globals: HashMap<&'a str, (&'a [u8], bool)>,
119119
// Max entries overrides the max_entries field of the map that matches the provided name
@@ -128,6 +128,34 @@ pub struct EbpfLoader<'a> {
128128
allow_unsupported_maps: bool,
129129
}
130130

131+
#[derive(Debug)]
132+
enum TargetBtf<'a> {
133+
System,
134+
Parsed(Cow<'a, Btf>),
135+
Source(BtfSource),
136+
}
137+
138+
type BtfParser = dyn Fn(&[u8]) -> Result<Btf, BtfError>;
139+
// Use `Fn` instead of `FnOnce`: when target BTF loading fails for an object with
140+
// only weak typed ksyms, loading continues, so a reused loader must be able to
141+
// retry the source.
142+
type BtfSourceFn = dyn Fn(&BtfParser) -> Result<Btf, EbpfError>
143+
// Preserve `EbpfLoader`'s existing auto traits.
144+
+ Send
145+
+ Sync
146+
+ std::panic::RefUnwindSafe
147+
+ std::panic::UnwindSafe
148+
// Keep captured state independent of `EbpfLoader`'s borrowed configuration.
149+
+ 'static;
150+
151+
struct BtfSource(Box<BtfSourceFn>);
152+
153+
impl std::fmt::Debug for BtfSource {
154+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155+
f.debug_struct("BtfSource").finish_non_exhaustive()
156+
}
157+
}
158+
131159
/// Builder style API for advanced loading of eBPF programs.
132160
#[deprecated(since = "0.13.0", note = "use `EbpfLoader` instead")]
133161
pub type BpfLoader<'a> = EbpfLoader<'a>;
@@ -157,7 +185,7 @@ impl<'a> EbpfLoader<'a> {
157185
/// Creates a new loader instance.
158186
pub fn new() -> Self {
159187
Self {
160-
btf: None,
188+
btf: TargetBtf::System,
161189
default_map_pin_directory: None,
162190
globals: HashMap::new(),
163191
max_entries: HashMap::new(),
@@ -174,6 +202,10 @@ impl<'a> EbpfLoader<'a> {
174202
/// the object contains CO-RE relocations or typed kernel symbols. Use this
175203
/// method to load `BTF` from a custom location.
176204
///
205+
/// The parsed `BTF` can be shared across multiple loaders, avoiding repeated
206+
/// parsing. To lazily read and parse target `BTF`, use
207+
/// [`EbpfLoader::btf_source`].
208+
///
177209
/// # Example
178210
///
179211
/// ```no_run
@@ -190,7 +222,45 @@ impl<'a> EbpfLoader<'a> {
190222
/// # Ok::<(), aya::EbpfError>(())
191223
/// ```
192224
pub fn btf(&mut self, btf: &'a Btf) -> &mut Self {
193-
self.btf = Some(Cow::Borrowed(btf));
225+
self.btf = TargetBtf::Parsed(Cow::Borrowed(btf));
226+
self
227+
}
228+
229+
/// Sets a callback for lazily loading the target [BTF](Btf) info.
230+
///
231+
/// The callback is called only when the object contains CO-RE relocations or
232+
/// typed kernel symbols. It receives a parser configured for the object's
233+
/// endianness, allowing BTF to be parsed from a borrowed byte slice. Once
234+
/// parsed, the target `BTF` is cached and reused by this loader. To share one
235+
/// parsed value across multiple loaders, use [`EbpfLoader::btf`] instead.
236+
///
237+
/// # Example
238+
///
239+
/// ```no_run
240+
/// use std::fs;
241+
///
242+
/// use aya::{EbpfError, EbpfLoader};
243+
///
244+
/// let bpf = EbpfLoader::new()
245+
/// .btf_source(|parse| {
246+
/// let data =
247+
/// fs::read("/custom_btf_file").map_err(EbpfError::BtfSourceError)?;
248+
/// Ok(parse(&data)?)
249+
/// })
250+
/// .load_file("file.o")?;
251+
///
252+
/// # Ok::<(), aya::EbpfError>(())
253+
/// ```
254+
pub fn btf_source<F>(&mut self, source: F) -> &mut Self
255+
where
256+
F: Fn(&BtfParser) -> Result<Btf, EbpfError>
257+
+ Send
258+
+ Sync
259+
+ std::panic::RefUnwindSafe
260+
+ std::panic::UnwindSafe
261+
+ 'static,
262+
{
263+
self.btf = TargetBtf::Source(BtfSource(Box::new(source)));
194264
self
195265
}
196266

@@ -493,30 +563,48 @@ impl<'a> EbpfLoader<'a> {
493563
None
494564
};
495565

496-
if btf.is_none() && (obj.has_btf_relocations() || obj.has_typed_ksyms()) {
497-
match Btf::from_sys_fs() {
498-
Ok(kernel_btf) => *btf = Some(Cow::Owned(kernel_btf)),
566+
if obj.has_btf_relocations() || obj.has_typed_ksyms() {
567+
let endianness = obj.endianness;
568+
let target_btf: Result<Cow<'_, Btf>, EbpfError> = match btf {
569+
TargetBtf::System => Btf::from_sys_fs().map(Cow::Owned).map_err(EbpfError::from),
570+
TargetBtf::Parsed(btf) => Ok(Cow::Borrowed(btf)),
571+
TargetBtf::Source(BtfSource(source)) => {
572+
source(&move |data| Btf::parse(data, endianness)).map(Cow::Owned)
573+
}
574+
};
575+
576+
match target_btf {
577+
Ok(target_btf) => {
578+
let result = obj
579+
.relocate_btf(&target_btf)
580+
.map_err(EbpfError::from)
581+
.and_then(|()| {
582+
obj.resolve_externs(Some(&target_btf))
583+
.map_err(EbpfError::from)
584+
});
585+
586+
if let Cow::Owned(target_btf) = target_btf {
587+
*btf = TargetBtf::Parsed(Cow::Owned(target_btf));
588+
}
589+
590+
result?;
591+
}
499592
Err(err) => {
500-
// CO-RE relocations and strong typed ksyms cannot be resolved without kernel
593+
// CO-RE relocations and strong typed ksyms cannot be resolved without target
501594
// BTF, so preserve the original loading error.
502595
if obj.has_btf_relocations() || obj.has_strong_typed_ksyms() {
503-
return Err(err.into());
596+
return Err(err);
504597
}
505598

506-
// Unresolved weak typed ksyms are allowed and handled during extern
507-
// relocation.
508-
warn!("kernel BTF is unavailable; weak typed ksyms will be unresolved: {err}")
599+
// Unresolved weak typed ksyms are allowed and handled during extern relocation.
600+
warn!("target BTF is unavailable; weak typed ksyms will be unresolved: {err}");
601+
obj.resolve_externs(None)?;
509602
}
510603
}
604+
} else {
605+
obj.resolve_externs(None)?;
511606
}
512607

513-
let btf = btf.as_deref();
514-
if let Some(btf) = btf {
515-
obj.relocate_btf(btf)?;
516-
}
517-
518-
obj.resolve_externs(btf)?;
519-
520608
const fn is_map_of_maps(map_type: bpf_map_type) -> bool {
521609
matches!(
522610
map_type,
@@ -928,6 +1016,8 @@ const fn adjust_to_page_size(byte_size: u32, page_size: u32) -> u32 {
9281016
mod tests {
9291017
use aya_obj::generated::bpf_map_type::*;
9301018

1019+
use super::{Btf, BtfSource, EbpfLoader, TargetBtf};
1020+
9311021
const PAGE_SIZE: u32 = 4096;
9321022
const NUM_CPUS: u32 = 4;
9331023

@@ -975,6 +1065,26 @@ mod tests {
9751065
);
9761066
}
9771067
}
1068+
1069+
#[test]
1070+
fn test_btf_source_can_parse_borrowed_subslice() {
1071+
let mut loader = EbpfLoader::new();
1072+
loader.btf_source(|parse| {
1073+
// This models an mmap opened by the callback: its backing storage
1074+
// can be dropped once the borrowed subslice has been parsed.
1075+
let data = [0, 1, 2, 3];
1076+
Ok(parse(&data[1..3])?)
1077+
});
1078+
1079+
let TargetBtf::Source(BtfSource(source)) = &loader.btf else {
1080+
panic!("expected a BTF source");
1081+
};
1082+
source(&|data| {
1083+
assert_eq!(data, [1, 2]);
1084+
Ok(Btf::new())
1085+
})
1086+
.unwrap();
1087+
}
9781088
}
9791089

9801090
impl Default for EbpfLoader<'_> {
@@ -1244,6 +1354,10 @@ pub enum EbpfError {
12441354
error: io::Error,
12451355
},
12461356

1357+
/// Error reading target BTF
1358+
#[error("error reading BTF source")]
1359+
BtfSourceError(#[source] io::Error),
1360+
12471361
/// Unexpected pinning type
12481362
#[error("unexpected pinning type {name}")]
12491363
UnexpectedPinningType {

test/integration-test/src/tests/btf_relocations.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
use std::sync::{
2+
Arc,
3+
atomic::{AtomicUsize, Ordering},
4+
};
5+
16
use aya::{
2-
EbpfLoader, Endianness,
7+
Ebpf, EbpfLoader, Endianness,
38
maps::Array,
49
programs::{UProbe, uprobe::UProbeScope},
510
};
@@ -54,8 +59,46 @@ use rstest::rstest;
5459
fn relocation_tests(#[case] bpf: &[u8], #[case] btf: &[u8], #[case] expected: u64) {
5560
let btf = Btf::parse(btf, Endianness::default()).unwrap();
5661

57-
let mut bpf = EbpfLoader::new().btf(&btf).load(bpf).unwrap();
62+
let bpf = EbpfLoader::new().btf(&btf).load(bpf).unwrap();
63+
64+
assert_relocation(bpf, expected);
65+
}
66+
67+
#[test_log::test]
68+
fn lazy_btf_source_is_cached() {
69+
let calls = Arc::new(AtomicUsize::new(0));
70+
let source_calls = Arc::clone(&calls);
71+
let mut loader = EbpfLoader::new();
72+
loader.btf_source(move |parse| {
73+
source_calls.fetch_add(1, Ordering::Relaxed);
74+
Ok(parse(crate::FIELD_RELOC_BTF)?)
75+
});
76+
77+
let bpf = loader.load(crate::FIELD_RELOC_BPF).unwrap();
78+
let _second_bpf = loader.load(crate::FIELD_RELOC_BPF).unwrap();
79+
80+
assert_eq!(calls.load(Ordering::Relaxed), 1);
81+
assert_relocation(bpf, 1);
82+
}
83+
84+
#[test_log::test]
85+
fn btf_source_is_not_called_when_unneeded() {
86+
let calls = Arc::new(AtomicUsize::new(0));
87+
let source_calls = Arc::clone(&calls);
88+
let mut loader = EbpfLoader::new();
89+
loader.btf_source(move |parse| {
90+
source_calls.fetch_add(1, Ordering::Relaxed);
91+
Ok(parse(crate::FIELD_RELOC_BTF)?)
92+
});
93+
94+
// RINGBUF_BTF contains BTF map definitions but no CO-RE relocations or typed ksyms. If that
95+
// changes, replace it with an object that does not require target BTF.
96+
let _bpf = loader.load(crate::RINGBUF_BTF).unwrap();
97+
98+
assert_eq!(calls.load(Ordering::Relaxed), 0);
99+
}
58100

101+
fn assert_relocation(mut bpf: Ebpf, expected: u64) {
59102
let program: &mut UProbe = bpf.program_mut("program").unwrap().try_into().unwrap();
60103
program.load().unwrap();
61104
program

xtask/public-api/aya.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7840,6 +7840,7 @@ pub macro aya::include_bytes_aligned!
78407840
pub enum aya::EbpfError
78417841
pub aya::EbpfError::BtfError(aya_obj::btf::btf::BtfError)
78427842
pub aya::EbpfError::BtfRelocationError(aya_obj::btf::relocation::BtfRelocationError)
7843+
pub aya::EbpfError::BtfSourceError(core::io::error::Error)
78437844
pub aya::EbpfError::FileError
78447845
pub aya::EbpfError::FileError::error: core::io::error::Error
78457846
pub aya::EbpfError::FileError::path: std::path::PathBuf
@@ -7905,6 +7906,7 @@ pub struct aya::EbpfLoader<'a>
79057906
impl<'a> aya::EbpfLoader<'a>
79067907
pub const fn aya::EbpfLoader<'a>::allow_unsupported_maps(&mut self) -> &mut Self
79077908
pub fn aya::EbpfLoader<'a>::btf(&mut self, &'a aya_obj::btf::btf::Btf) -> &mut Self
7909+
pub fn aya::EbpfLoader<'a>::btf_source<F>(&mut self, F) -> &mut Self where F: core::ops::function::Fn(&dyn core::ops::function::Fn(&[u8]) -> core::result::Result<aya_obj::btf::btf::Btf, aya_obj::btf::btf::BtfError>) -> core::result::Result<aya_obj::btf::btf::Btf, aya::EbpfError> + core::marker::Send + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe + core::panic::unwind_safe::UnwindSafe + 'static
79087910
pub fn aya::EbpfLoader<'a>::default_map_pin_directory<P: core::convert::AsRef<std::path::Path>>(&mut self, P) -> &mut Self
79097911
pub fn aya::EbpfLoader<'a>::extension(&mut self, &'a str) -> &mut Self
79107912
pub fn aya::EbpfLoader<'a>::load(&mut self, &[u8]) -> core::result::Result<aya::Ebpf, aya::EbpfError>

0 commit comments

Comments
 (0)