diff --git a/aya-log-ebpf-macros/src/expand.rs b/aya-log-ebpf-macros/src/expand.rs index 890ffdc7..6f7dfca1 100644 --- a/aya-log-ebpf-macros/src/expand.rs +++ b/aya-log-ebpf-macros/src/expand.rs @@ -151,32 +151,34 @@ pub(crate) fn log(args: LogArgs, level: Option) -> Result {}, - Some(::aya_log_ebpf::macro_support::LogBuf { buf: #buf }) => { - // Silence unused variable warning; we may need ctx in the future. - let _ = #ctx; - let _: Option<()> = (|| { - let #size = ::aya_log_ebpf::macro_support::write_record_header( - #buf, - #target, - #level, - module_path!(), - file!(), - line!(), - #num_args, - )?; - let mut #size = #size.get(); - #( - { - let #buf = #buf.get_mut(#size..)?; - let #len = ::aya_log_ebpf::macro_support::WriteToBuf::write(#values_iter, #buf)?; - #size += #len.get(); - } - )* - let #record = #buf.get(..#size)?; - Result::<_, i64>::ok(::aya_log_ebpf::macro_support::AYA_LOGS.output(#record, 0)) - })(); + if ::aya_log_ebpf::macro_support::level_enabled(#level) { + match ::aya_log_ebpf::macro_support::AYA_LOG_BUF.get_ptr_mut(0).and_then(|ptr| unsafe { ptr.as_mut() }) { + None => {}, + Some(::aya_log_ebpf::macro_support::LogBuf { buf: #buf }) => { + // Silence unused variable warning; we may need ctx in the future. + let _ = #ctx; + let _: Option<()> = (|| { + let #size = ::aya_log_ebpf::macro_support::write_record_header( + #buf, + #target, + #level, + module_path!(), + file!(), + line!(), + #num_args, + )?; + let mut #size = #size.get(); + #( + { + let #buf = #buf.get_mut(#size..)?; + let #len = ::aya_log_ebpf::macro_support::WriteToBuf::write(#values_iter, #buf)?; + #size += #len.get(); + } + )* + let #record = #buf.get(..#size)?; + Result::<_, i64>::ok(::aya_log_ebpf::macro_support::AYA_LOGS.output(#record, 0)) + })(); + } } } }) diff --git a/aya-log/CHANGELOG.md b/aya-log/CHANGELOG.md index 5eb0e76d..55955ef7 100644 --- a/aya-log/CHANGELOG.md +++ b/aya-log/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### New Features + +- Add eBPF-side log level mask `AYA_LOG_LEVEL_MASK` and loader API `EbpfLoader::set_log_level_mask()` + allowing selective disabling of log levels at load-time. Disabled levels are + eliminated by the verifier, reducing instruction count and avoiding program + size limits when extensive logging is present. + ### Breaking Changes - The implementation is now backed by a ring buffer rather than a perf event array. This should diff --git a/aya-log/README.md b/aya-log/README.md index 765247c9..c084c62a 100644 --- a/aya-log/README.md +++ b/aya-log/README.md @@ -69,3 +69,37 @@ fn try_xdp_firewall(ctx: XdpContext) -> Result { [aya]: https://github.com/aya-rs/aya [log]: https://docs.rs/log [env_logger]: https://docs.rs/env_logger + +## Disabling log levels at load-time + +eBPF instruction budgets are tight. Even if a log statement never executes at +runtime, the verifier must still evaluate its instructions unless it can prove +they're unreachable. `aya-log` now exposes a global bitmask `AYA_LOG_LEVEL_MASK` +inside the eBPF object allowing you to selectively enable levels before the +program is loaded. + +Bits (least-significant first) map to levels: Error=bit0, Warn=bit1, Info=bit2, +Debug=bit3, Trace=bit4. + +By default all bits are set (all logging enabled). To disable all logging: + +```rust +use aya::EbpfLoader; + +let mut bpf = EbpfLoader::new() + .set_log_level_mask(0) // disable all levels + .load_file("prog.bpf.o")?; +# Ok::<(), aya::EbpfError>(()) +``` + +Enable only Error and Warn: + +```rust +let mut bpf = EbpfLoader::new() + .set_log_level_mask(0b11) + .load_file("prog.bpf.o")?; +``` + +Because the mask is placed in global read-only data, the verifier sees the +disabled branch as unreachable and prunes the logging instructions, reducing +overall instruction count and avoiding potential instruction limit issues. diff --git a/aya/src/bpf.rs b/aya/src/bpf.rs index c1b67955..a5f648c0 100644 --- a/aya/src/bpf.rs +++ b/aya/src/bpf.rs @@ -341,6 +341,21 @@ impl<'a> EbpfLoader<'a> { self } + /// Sets the aya-log eBPF log level mask before loading programs. + /// + /// This is a convenience wrapper around [`EbpfLoader::set_global`] that patches the + /// `AYA_LOG_LEVEL_MASK` symbol exposed by `aya-log-ebpf`. + /// + /// Each bit enables a level where bit0=Error, bit1=Warn, bit2=Info, bit3=Debug, bit4=Trace. + /// For example to enable only Error and Warn pass `0b0000_0011` (3). To disable all logging + /// pass 0. + /// + /// If the symbol does not exist in the object (e.g., logging not linked) no error is raised. + pub fn set_log_level_mask(&mut self, mask: &'a u32) -> &mut Self { + // Best-effort: don't require symbol to exist. + self.set_global("AYA_LOG_LEVEL_MASK", mask, false) + } + /// Loads eBPF bytecode from a file. /// /// # Examples diff --git a/ebpf/aya-log-ebpf/src/lib.rs b/ebpf/aya-log-ebpf/src/lib.rs index 0a3f1421..3dbfa035 100644 --- a/ebpf/aya-log-ebpf/src/lib.rs +++ b/ebpf/aya-log-ebpf/src/lib.rs @@ -34,4 +34,22 @@ pub mod macro_support { // test/integration-test/Cargo.toml. #[cfg_attr(target_arch = "bpf", map)] pub static AYA_LOGS: RingBuf = RingBuf::with_byte_size((LOG_BUF_CAPACITY as u32) << 4, 0); + + /// Global log level mask controlling which log statements are active. + /// Bits correspond to variants of `Level` (1-based discriminant) shifted into a mask. + /// By default all bits set (enable all levels). Userspace may patch this symbol before load + /// via `EbpfLoader::set_log_level_mask` (to be implemented) using global data patching. + /// When a bit is unset, verifier will see the guarded block as unreachable and prune it. + #[unsafe(no_mangle)] + pub static mut AYA_LOG_LEVEL_MASK: u32 = 0xFFFF_FFFF; + + /// Returns `true` if the provided level is enabled according to `AYA_LOG_LEVEL_MASK`. + #[inline(always)] + pub fn level_enabled(lvl: Level) -> bool { + // SAFETY: Mask lives in .rodata for BPF (declared mutable for patching), we only read it. + #[expect(static_mut_refs)] + let mask = unsafe { core::ptr::read_volatile(&AYA_LOG_LEVEL_MASK) }; + let bit: u32 = 1u32 << (lvl as u32 - 1); + (mask & bit) != 0 + } } diff --git a/test/integration-test/src/tests/log.rs b/test/integration-test/src/tests/log.rs index 987ec23b..58055fcf 100644 --- a/test/integration-test/src/tests/log.rs +++ b/test/integration-test/src/tests/log.rs @@ -2,6 +2,7 @@ use std::{borrow::Cow, sync::Mutex}; use aya::{Ebpf, programs::UProbe}; use aya_log::EbpfLogger; +use aya::EbpfLoader; use log::{Level, Log, Record}; #[unsafe(no_mangle)] @@ -173,3 +174,48 @@ fn log() { assert_eq!(records.next(), None); } + +#[test_log::test] +fn log_level_mask_only_error_warn() { + // Enable only Error (bit0) and Warn (bit1) + let mut bpf = EbpfLoader::new() + .set_log_level_mask(0b11) + .load(crate::LOG) + .unwrap(); + + let mut captured_logs = Vec::new(); + let logger = TestingLogger { + log: Mutex::new(|record: &Record| { + captured_logs.push(CapturedLog { + body: format!("{}", record.args()).into(), + level: record.level(), + target: record.target().to_string().into(), + }); + }), + }; + let mut logger = EbpfLogger::init_with_logger(&mut bpf, &logger).unwrap(); + + let prog: &mut UProbe = bpf.program_mut("test_log").unwrap().try_into().unwrap(); + prog.load().unwrap(); + prog.attach("trigger_ebpf_program", "/proc/self/exe", None, None) + .unwrap(); + + trigger_ebpf_program(); + logger.flush(); + + // We expect only the Error and Warn records to be present, in program order. + let expected: Vec> = vec![ + CapturedLog { + body: "69, 420, wao, 77616f".into(), + level: Level::Error, + target: "log".into(), + }, + CapturedLog { + body: "hex lc: 2f, hex uc: 2F".into(), + level: Level::Warn, + target: "log".into(), + }, + ]; + + assert_eq!(captured_logs, expected, "unexpected log records when mask filters levels"); +} diff --git a/xtask/public-api/aya.txt b/xtask/public-api/aya.txt index 25480b40..d6f5277d 100644 --- a/xtask/public-api/aya.txt +++ b/xtask/public-api/aya.txt @@ -10370,6 +10370,7 @@ pub fn aya::EbpfLoader<'a>::load_file>( pub fn aya::EbpfLoader<'a>::map_pin_path>(&mut self, path: P) -> &mut Self pub fn aya::EbpfLoader<'a>::new() -> Self pub fn aya::EbpfLoader<'a>::set_global>>(&mut self, name: &'a str, value: T, must_exist: bool) -> &mut Self +pub fn aya::EbpfLoader<'a>::set_log_level_mask(&mut self, mask: &'a u32) -> &mut Self pub fn aya::EbpfLoader<'a>::set_max_entries(&mut self, name: &'a str, size: u32) -> &mut Self pub fn aya::EbpfLoader<'a>::verifier_log_level(&mut self, level: aya::VerifierLogLevel) -> &mut Self impl core::default::Default for aya::EbpfLoader<'_>