Skip to content

Commit 72c4b28

Browse files
committed
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 a reader factory that is invoked only for CO-RE relocations or typed kernel symbols, and cache the BTF after the first successful parse. Keep btf(&Btf) for callers that share one parsed value across multiple loaders.
1 parent 412fe81 commit 72c4b28

3 files changed

Lines changed: 173 additions & 22 deletions

File tree

aya/src/bpf.rs

Lines changed: 123 additions & 20 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,38 @@ 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+
struct BtfSource {
139+
read: Box<
140+
dyn Fn() -> io::Result<Vec<u8>>
141+
// Preserve `EbpfLoader`'s existing auto traits.
142+
+ Send
143+
+ Sync
144+
+ std::panic::RefUnwindSafe
145+
+ std::panic::UnwindSafe
146+
// Keep captured state independent of `EbpfLoader`'s borrowed configuration.
147+
+ 'static,
148+
>,
149+
}
150+
151+
impl std::fmt::Debug for BtfSource {
152+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153+
f.debug_struct("BtfSource").finish_non_exhaustive()
154+
}
155+
}
156+
157+
impl BtfSource {
158+
fn read(&self) -> io::Result<Vec<u8>> {
159+
(self.read)()
160+
}
161+
}
162+
131163
/// Builder style API for advanced loading of eBPF programs.
132164
#[deprecated(since = "0.13.0", note = "use `EbpfLoader` instead")]
133165
pub type BpfLoader<'a> = EbpfLoader<'a>;
@@ -157,7 +189,7 @@ impl<'a> EbpfLoader<'a> {
157189
/// Creates a new loader instance.
158190
pub fn new() -> Self {
159191
Self {
160-
btf: None,
192+
btf: TargetBtf::System,
161193
default_map_pin_directory: None,
162194
globals: HashMap::new(),
163195
max_entries: HashMap::new(),
@@ -174,6 +206,10 @@ impl<'a> EbpfLoader<'a> {
174206
/// the object contains CO-RE relocations or typed kernel symbols. Use this
175207
/// method to load `BTF` from a custom location.
176208
///
209+
/// The parsed `BTF` can be shared across multiple loaders, avoiding repeated
210+
/// parsing. To lazily read and parse target `BTF`, use
211+
/// [`EbpfLoader::btf_source`].
212+
///
177213
/// # Example
178214
///
179215
/// ```no_run
@@ -190,7 +226,48 @@ impl<'a> EbpfLoader<'a> {
190226
/// # Ok::<(), aya::EbpfError>(())
191227
/// ```
192228
pub fn btf(&mut self, btf: &'a Btf) -> &mut Self {
193-
self.btf = Some(Cow::Borrowed(btf));
229+
self.btf = TargetBtf::Parsed(Cow::Borrowed(btf));
230+
self
231+
}
232+
233+
/// Sets a factory for lazily reading the target [BTF](Btf) info.
234+
///
235+
/// The factory is called only when the object contains CO-RE relocations or
236+
/// typed kernel symbols. Once parsed, the target `BTF` is cached and reused
237+
/// by this loader. To share one parsed value across multiple loaders, use
238+
/// [`EbpfLoader::btf`] instead.
239+
///
240+
/// # Example
241+
///
242+
/// ```no_run
243+
/// use std::fs::File;
244+
///
245+
/// use aya::EbpfLoader;
246+
///
247+
/// let bpf = EbpfLoader::new()
248+
/// .btf_source(|| File::open("/custom_btf_file"))
249+
/// .load_file("file.o")?;
250+
///
251+
/// # Ok::<(), aya::EbpfError>(())
252+
/// ```
253+
pub fn btf_source<F, R>(&mut self, source: F) -> &mut Self
254+
where
255+
F: Fn() -> io::Result<R>
256+
+ Send
257+
+ Sync
258+
+ std::panic::RefUnwindSafe
259+
+ std::panic::UnwindSafe
260+
+ 'static,
261+
R: io::Read,
262+
{
263+
self.btf = TargetBtf::Source(BtfSource {
264+
read: Box::new(move || {
265+
let mut reader = source()?;
266+
let mut data = Vec::new();
267+
reader.read_to_end(&mut data)?;
268+
Ok(data)
269+
}),
270+
});
194271
self
195272
}
196273

@@ -493,29 +570,51 @@ impl<'a> EbpfLoader<'a> {
493570
None
494571
};
495572

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)),
499-
Err(err) => {
500-
// CO-RE relocations and strong typed ksyms cannot be resolved without kernel
501-
// BTF, so preserve the original loading error.
502-
if obj.has_btf_relocations() || obj.has_strong_typed_ksyms() {
503-
return Err(err.into());
504-
}
573+
let target_btf: Option<&Btf> = if obj.has_btf_relocations() || obj.has_typed_ksyms() {
574+
let load_result = match btf {
575+
TargetBtf::System => Some(Btf::from_sys_fs().map_err(EbpfError::from)),
576+
TargetBtf::Source(source) => Some(
577+
source
578+
.read()
579+
.map_err(EbpfError::BtfSourceError)
580+
.and_then(|data| {
581+
Btf::parse(&data, obj.endianness).map_err(EbpfError::BtfError)
582+
}),
583+
),
584+
TargetBtf::Parsed(_) => None,
585+
};
586+
if let Some(load_result) = load_result {
587+
match load_result {
588+
Ok(target_btf) => *btf = TargetBtf::Parsed(Cow::Owned(target_btf)),
589+
Err(err) => {
590+
// CO-RE relocations and strong typed ksyms cannot be resolved without
591+
// target BTF, so preserve the original loading error.
592+
if obj.has_btf_relocations() || obj.has_strong_typed_ksyms() {
593+
return Err(err);
594+
}
505595

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}")
596+
// Unresolved weak typed ksyms are allowed and handled during extern
597+
// relocation.
598+
warn!(
599+
"target BTF is unavailable; weak typed ksyms will be unresolved: {err}"
600+
)
601+
}
509602
}
510603
}
511-
}
512604

513-
let btf = btf.as_deref();
514-
if let Some(btf) = btf {
515-
obj.relocate_btf(btf)?;
605+
match btf {
606+
TargetBtf::Parsed(btf) => Some(btf),
607+
_ => None,
608+
}
609+
} else {
610+
None
611+
};
612+
613+
if let Some(target_btf) = target_btf {
614+
obj.relocate_btf(target_btf)?;
516615
}
517616

518-
obj.resolve_externs(btf)?;
617+
obj.resolve_externs(target_btf)?;
519618

520619
const fn is_map_of_maps(map_type: bpf_map_type) -> bool {
521620
matches!(
@@ -1244,6 +1343,10 @@ pub enum EbpfError {
12441343
error: io::Error,
12451344
},
12461345

1346+
/// Error reading target BTF
1347+
#[error("error reading BTF source")]
1348+
BtfSourceError(#[source] io::Error),
1349+
12471350
/// Unexpected pinning type
12481351
#[error("unexpected pinning type {name}")]
12491352
UnexpectedPinningType {

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

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1+
use std::{
2+
io::Cursor,
3+
sync::{
4+
Arc,
5+
atomic::{AtomicUsize, Ordering},
6+
},
7+
};
8+
19
use aya::{
2-
EbpfLoader, Endianness,
10+
Ebpf, EbpfLoader, Endianness,
311
maps::Array,
412
programs::{UProbe, uprobe::UProbeScope},
513
};
@@ -54,8 +62,46 @@ use rstest::rstest;
5462
fn relocation_tests(#[case] bpf: &[u8], #[case] btf: &[u8], #[case] expected: u64) {
5563
let btf = Btf::parse(btf, Endianness::default()).unwrap();
5664

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

104+
fn assert_relocation(mut bpf: Ebpf, expected: u64) {
59105
let program: &mut UProbe = bpf.program_mut("program").unwrap().try_into().unwrap();
60106
program.load().unwrap();
61107
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, R>(&mut self, F) -> &mut Self where F: core::ops::function::Fn() -> core::io::error::Result<R> + core::marker::Send + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe + core::panic::unwind_safe::UnwindSafe + 'static, R: alloc::io::read::Read
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)