diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5330a3762..9da39b474 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,9 +77,11 @@ jobs: run: | set -euxo pipefail cargo +nightly hack miri test --all-targets --feature-powerset \ + --exclude aya-common \ --exclude aya-ebpf \ --exclude aya-ebpf-bindings \ --exclude aya-log-ebpf \ + --exclude integration-common \ --exclude integration-ebpf \ --exclude integration-test \ --workspace @@ -129,9 +131,11 @@ jobs: run: | set -euxo pipefail cargo hack build --all-targets --feature-powerset \ + --exclude aya-common \ --exclude aya-ebpf \ --exclude aya-ebpf-bindings \ --exclude aya-log-ebpf \ + --exclude integration-common \ --exclude integration-ebpf \ --exclude xtask \ --workspace @@ -142,9 +146,11 @@ jobs: run: | set -euxo pipefail cargo hack test --all-targets \ + --exclude aya-common \ --exclude aya-ebpf \ --exclude aya-ebpf-bindings \ --exclude aya-log-ebpf \ + --exclude integration-common \ --exclude integration-ebpf \ --exclude integration-test \ --feature-powerset @@ -155,9 +161,11 @@ jobs: run: | set -euxo pipefail cargo hack test --doc \ + --exclude aya-common \ --exclude aya-ebpf \ --exclude aya-ebpf-bindings \ --exclude aya-log-ebpf \ + --exclude integration-common \ --exclude integration-ebpf \ --exclude integration-test \ --feature-powerset diff --git a/Cargo.toml b/Cargo.toml index ebad698d6..0f99b3dd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "aya", "aya-build", + "aya-common", "aya-log", "aya-log-common", "aya-log-parser", diff --git a/aya-common/Cargo.toml b/aya-common/Cargo.toml new file mode 100644 index 000000000..5f667997e --- /dev/null +++ b/aya-common/Cargo.toml @@ -0,0 +1,22 @@ +[package] +description = "Library shared across eBPF and user-space" +documentation = "https://docs.rs/aya-common" +keywords = ["bpf", "ebpf", "common"] +name = "aya-common" +version = "0.1.0" + +authors.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +aya-ebpf-bindings = { version = "^0.1.2", path = "../ebpf/aya-ebpf-bindings" } + +[features] +user = ["aya-ebpf-bindings/user"] diff --git a/aya-common/src/lib.rs b/aya-common/src/lib.rs new file mode 100644 index 000000000..9125f5a0f --- /dev/null +++ b/aya-common/src/lib.rs @@ -0,0 +1,9 @@ +#![cfg_attr( + target_arch = "bpf", + expect(unused_crate_dependencies, reason = "compiler_builtins") +)] +#![no_std] + +mod spin_lock; + +pub use spin_lock::SpinLock; diff --git a/aya-common/src/spin_lock.rs b/aya-common/src/spin_lock.rs new file mode 100644 index 000000000..d826dd49e --- /dev/null +++ b/aya-common/src/spin_lock.rs @@ -0,0 +1,2 @@ +/// A spin lock that can be used to protect shared data in eBPF maps +pub type SpinLock = aya_ebpf_bindings::bindings::bpf_spin_lock; diff --git a/ebpf/aya-ebpf-bindings/Cargo.toml b/ebpf/aya-ebpf-bindings/Cargo.toml index f906ab5e7..f1208e45d 100644 --- a/ebpf/aya-ebpf-bindings/Cargo.toml +++ b/ebpf/aya-ebpf-bindings/Cargo.toml @@ -13,7 +13,11 @@ repository.workspace = true workspace = true [dependencies] +aya = { path = "../../aya", version = "^0.13.1", optional = true } aya-ebpf-cty = { version = "^0.2.3", path = "../aya-ebpf-cty" } [build-dependencies] aya-build = { version = "^0.1.3", path = "../../aya-build" } + +[features] +user = ["dep:aya"] diff --git a/ebpf/aya-ebpf-bindings/src/lib.rs b/ebpf/aya-ebpf-bindings/src/lib.rs index 4576684b8..5985f6efc 100644 --- a/ebpf/aya-ebpf-bindings/src/lib.rs +++ b/ebpf/aya-ebpf-bindings/src/lib.rs @@ -118,4 +118,7 @@ pub mod bindings { pub id: ::aya_ebpf_cty::c_uint, pub pinning: ::aya_ebpf_cty::c_uint, } + + #[cfg(feature = "user")] + unsafe impl aya::Pod for bpf_spin_lock {} } diff --git a/ebpf/aya-ebpf/Cargo.toml b/ebpf/aya-ebpf/Cargo.toml index b597f21f1..42137fa84 100644 --- a/ebpf/aya-ebpf/Cargo.toml +++ b/ebpf/aya-ebpf/Cargo.toml @@ -14,6 +14,7 @@ rust-version.workspace = true workspace = true [dependencies] +aya-common = { version = "^0.1.0", path = "../../aya-common" } aya-ebpf-bindings = { version = "^0.1.2", path = "../aya-ebpf-bindings" } aya-ebpf-cty = { version = "^0.2.3", path = "../aya-ebpf-cty" } aya-ebpf-macros = { version = "^0.1.2", path = "../../aya-ebpf-macros" } diff --git a/ebpf/aya-ebpf/src/lib.rs b/ebpf/aya-ebpf/src/lib.rs index 9a32e2c0a..cdfa64655 100644 --- a/ebpf/aya-ebpf/src/lib.rs +++ b/ebpf/aya-ebpf/src/lib.rs @@ -45,6 +45,7 @@ pub mod btf_maps; pub mod helpers; pub mod maps; pub mod programs; +pub mod spin_lock; use core::{ mem::MaybeUninit, diff --git a/ebpf/aya-ebpf/src/spin_lock.rs b/ebpf/aya-ebpf/src/spin_lock.rs new file mode 100644 index 000000000..6a4a42d3c --- /dev/null +++ b/ebpf/aya-ebpf/src/spin_lock.rs @@ -0,0 +1,56 @@ +pub use aya_common::SpinLock; + +use crate::helpers; + +/// An RAII implementation of a scope of a spin lock. When this structure is +/// dropped (falls out of scope), the lock will be unlocked. +#[must_use = "if unused the spin lock will immediately unlock"] +pub struct SpinLockGuard<'a> { + spin_lock: &'a mut SpinLock, +} + +impl Drop for SpinLockGuard<'_> { + fn drop(&mut self) { + // SAFETY: Call to an eBPF helper. `self.spin_lock` is always + // initialized. + unsafe { + helpers::bpf_spin_unlock(core::ptr::from_mut(self.spin_lock)); + } + } +} + +mod sealed { + use super::{SpinLock, SpinLockGuard, helpers}; + + pub trait EbpfSpinLock { + fn lock(&mut self) -> SpinLockGuard<'_>; + } + + impl EbpfSpinLock for SpinLock { + fn lock(&mut self) -> SpinLockGuard<'_> { + // SAFETY: Call to an eBPF helper. `self` is always initialized. + unsafe { + helpers::bpf_spin_lock(core::ptr::from_mut(self)); + } + SpinLockGuard { spin_lock: self } + } + } +} + +/// Extension trait for [`SpinLock`] exposing eBPF-only helpers. These helpers +/// are not available in user-space. +pub trait EbpfSpinLockExt: sealed::EbpfSpinLock { + fn lock(&mut self) -> SpinLockGuard<'_>; +} + +impl EbpfSpinLockExt for T +where + T: sealed::EbpfSpinLock, +{ + /// Acquires a spin lock and returns a [`SpinLockGuard`]. The lock is + /// acquired as long as the guard is alive. + #[inline] + fn lock(&mut self) -> SpinLockGuard<'_> { + sealed::EbpfSpinLock::lock(self) + } +} diff --git a/test/integration-common/Cargo.toml b/test/integration-common/Cargo.toml index 437f87e91..1b731db26 100644 --- a/test/integration-common/Cargo.toml +++ b/test/integration-common/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [dependencies] aya = { path = "../../aya", optional = true } +aya-common = { path = "../../aya-common" } [features] user = ["aya"] diff --git a/test/integration-common/src/lib.rs b/test/integration-common/src/lib.rs index 7a93a07d9..b0164efb3 100644 --- a/test/integration-common/src/lib.rs +++ b/test/integration-common/src/lib.rs @@ -1,3 +1,7 @@ +#![cfg_attr( + target_arch = "bpf", + expect(unused_crate_dependencies, reason = "compiler_builtins") +)] #![no_std] pub mod array { @@ -82,6 +86,20 @@ pub mod ring_buf { unsafe impl aya::Pod for Registers {} } +pub mod spin_lock { + use aya_common::SpinLock; + + #[derive(Copy, Clone)] + #[repr(C)] + pub struct Counter { + pub count: u32, + pub spin_lock: SpinLock, + } + + #[cfg(feature = "user")] + unsafe impl aya::Pod for Counter {} +} + pub mod strncmp { #[derive(Copy, Clone)] #[repr(C)] diff --git a/test/integration-ebpf/Cargo.toml b/test/integration-ebpf/Cargo.toml index 185c37c39..d8e745fd6 100644 --- a/test/integration-ebpf/Cargo.toml +++ b/test/integration-ebpf/Cargo.toml @@ -88,6 +88,10 @@ path = "src/sk_storage.rs" name = "simple_prog" path = "src/simple_prog.rs" +[[bin]] +name = "spin_lock" +path = "src/spin_lock.rs" + [[bin]] name = "strncmp" path = "src/strncmp.rs" diff --git a/test/integration-ebpf/src/spin_lock.rs b/test/integration-ebpf/src/spin_lock.rs new file mode 100644 index 000000000..720572941 --- /dev/null +++ b/test/integration-ebpf/src/spin_lock.rs @@ -0,0 +1,32 @@ +#![no_std] +#![no_main] +#![expect(unused_crate_dependencies, reason = "used in other bins")] + +#[cfg(not(test))] +extern crate ebpf_panic; + +use aya_ebpf::{ + bindings::xdp_action, + btf_maps::Array, + macros::{btf_map, xdp}, + programs::XdpContext, + spin_lock::EbpfSpinLock as _, +}; +use integration_common::spin_lock::Counter; + +#[btf_map] +static COUNTER: Array = Array::new(); + +#[xdp] +fn packet_counter(_ctx: XdpContext) -> u32 { + let Some(counter) = COUNTER.get_ptr_mut(0) else { + return xdp_action::XDP_PASS; + }; + let counter = unsafe { &mut *counter }; + { + let _guard = counter.spin_lock.lock(); + counter.count = counter.count.saturating_add(1); + } + + xdp_action::XDP_PASS +} diff --git a/test/integration-test/src/lib.rs b/test/integration-test/src/lib.rs index 702c71908..5bcc44a11 100644 --- a/test/integration-test/src/lib.rs +++ b/test/integration-test/src/lib.rs @@ -56,6 +56,7 @@ bpf_file!( RING_BUF => "ring_buf", SIMPLE_PROG => "simple_prog", SK_STORAGE => "sk_storage", + SPIN_LOCK => "spin_lock", STRNCMP => "strncmp", TCX => "tcx", TEST => "test", diff --git a/test/integration-test/src/tests.rs b/test/integration-test/src/tests.rs index 098c79551..6104d0fdd 100644 --- a/test/integration-test/src/tests.rs +++ b/test/integration-test/src/tests.rs @@ -30,6 +30,7 @@ mod relocations; mod ring_buf; mod sk_storage; mod smoke; +mod spin_lock; mod strncmp; mod tcx; mod uprobe_cookie; diff --git a/test/integration-test/src/tests/spin_lock.rs b/test/integration-test/src/tests/spin_lock.rs new file mode 100644 index 000000000..ed06b1e74 --- /dev/null +++ b/test/integration-test/src/tests/spin_lock.rs @@ -0,0 +1,51 @@ +use std::{net::UdpSocket, time::Duration}; + +use aya::{ + EbpfLoader, + maps::Array, + programs::{Xdp, XdpFlags}, +}; +use integration_common::spin_lock::Counter; + +use crate::utils::NetNsGuard; + +#[test_log::test] +fn test_spin_lock() { + let _netns = NetNsGuard::new(); + + let mut ebpf = EbpfLoader::new().load(crate::SPIN_LOCK).unwrap(); + + let prog: &mut Xdp = ebpf + .program_mut("packet_counter") + .unwrap() + .try_into() + .unwrap(); + prog.load().unwrap(); + prog.attach("lo", XdpFlags::default()).unwrap(); + + const PAYLOAD: &str = "hello counter"; + + let sock = UdpSocket::bind("127.0.0.1:0").unwrap(); + let addr = sock.local_addr().unwrap(); + sock.set_read_timeout(Some(Duration::from_secs(60))) + .unwrap(); + + let num_packets = 10; + for _ in 0..num_packets { + sock.send_to(PAYLOAD.as_bytes(), addr).unwrap(); + } + + // Read back the packets to ensure it went through the entire network stack, + // including the XDP program. + let mut buf = [0u8; PAYLOAD.len() + 1]; + for _ in 0..num_packets { + let n = sock.recv(&mut buf).unwrap(); + assert_eq!(n, PAYLOAD.len()); + assert_eq!(&buf[..n], PAYLOAD.as_bytes()); + } + + let counter_map = ebpf.map("COUNTER").unwrap(); + let counter_map = Array::<_, Counter>::try_from(counter_map).unwrap(); + let Counter { count, .. } = counter_map.get(&0, 0).unwrap(); + assert_eq!(count, num_packets); +} diff --git a/xtask/public-api/aya-common.txt b/xtask/public-api/aya-common.txt new file mode 100644 index 000000000..7243a5ea1 --- /dev/null +++ b/xtask/public-api/aya-common.txt @@ -0,0 +1,2 @@ +pub mod aya_common +pub type aya_common::SpinLock = aya_ebpf_bindings::x86_64::bindings::bpf_spin_lock diff --git a/xtask/public-api/aya-ebpf.txt b/xtask/public-api/aya-ebpf.txt index a5d1de27b..4e39f7fa3 100644 --- a/xtask/public-api/aya-ebpf.txt +++ b/xtask/public-api/aya-ebpf.txt @@ -3061,6 +3061,37 @@ impl core::borrow::BorrowMut for aya_ebpf::programs::xdp::XdpContext where pub fn aya_ebpf::programs::xdp::XdpContext::borrow_mut(&mut self) -> &mut T impl core::convert::From for aya_ebpf::programs::xdp::XdpContext pub fn aya_ebpf::programs::xdp::XdpContext::from(t: T) -> T +pub mod aya_ebpf::spin_lock +pub use aya_ebpf::spin_lock::SpinLock +pub struct aya_ebpf::spin_lock::SpinLockGuard<'a> +impl core::ops::drop::Drop for aya_ebpf::spin_lock::SpinLockGuard<'_> +pub fn aya_ebpf::spin_lock::SpinLockGuard<'_>::drop(&mut self) +impl<'a> core::marker::Freeze for aya_ebpf::spin_lock::SpinLockGuard<'a> +impl<'a> core::marker::Send for aya_ebpf::spin_lock::SpinLockGuard<'a> +impl<'a> core::marker::Sync for aya_ebpf::spin_lock::SpinLockGuard<'a> +impl<'a> core::marker::Unpin for aya_ebpf::spin_lock::SpinLockGuard<'a> +impl<'a> core::panic::unwind_safe::RefUnwindSafe for aya_ebpf::spin_lock::SpinLockGuard<'a> +impl<'a> !core::panic::unwind_safe::UnwindSafe for aya_ebpf::spin_lock::SpinLockGuard<'a> +impl core::convert::Into for aya_ebpf::spin_lock::SpinLockGuard<'a> where U: core::convert::From +pub fn aya_ebpf::spin_lock::SpinLockGuard<'a>::into(self) -> U +impl core::convert::TryFrom for aya_ebpf::spin_lock::SpinLockGuard<'a> where U: core::convert::Into +pub type aya_ebpf::spin_lock::SpinLockGuard<'a>::Error = core::convert::Infallible +pub fn aya_ebpf::spin_lock::SpinLockGuard<'a>::try_from(value: U) -> core::result::Result>::Error> +impl core::convert::TryInto for aya_ebpf::spin_lock::SpinLockGuard<'a> where U: core::convert::TryFrom +pub type aya_ebpf::spin_lock::SpinLockGuard<'a>::Error = >::Error +pub fn aya_ebpf::spin_lock::SpinLockGuard<'a>::try_into(self) -> core::result::Result>::Error> +impl core::any::Any for aya_ebpf::spin_lock::SpinLockGuard<'a> where T: 'static + ?core::marker::Sized +pub fn aya_ebpf::spin_lock::SpinLockGuard<'a>::type_id(&self) -> core::any::TypeId +impl core::borrow::Borrow for aya_ebpf::spin_lock::SpinLockGuard<'a> where T: ?core::marker::Sized +pub fn aya_ebpf::spin_lock::SpinLockGuard<'a>::borrow(&self) -> &T +impl core::borrow::BorrowMut for aya_ebpf::spin_lock::SpinLockGuard<'a> where T: ?core::marker::Sized +pub fn aya_ebpf::spin_lock::SpinLockGuard<'a>::borrow_mut(&mut self) -> &mut T +impl core::convert::From for aya_ebpf::spin_lock::SpinLockGuard<'a> +pub fn aya_ebpf::spin_lock::SpinLockGuard<'a>::from(t: T) -> T +pub trait aya_ebpf::spin_lock::EbpfSpinLock: aya_ebpf::spin_lock::sealed::Sealed +pub fn aya_ebpf::spin_lock::EbpfSpinLock::lock(&mut self) -> aya_ebpf::spin_lock::SpinLockGuard<'_> +impl aya_ebpf::spin_lock::EbpfSpinLock for aya_common::spin_lock::SpinLock +pub fn aya_common::spin_lock::SpinLock::lock(&mut self) -> aya_ebpf::spin_lock::SpinLockGuard<'_> pub macro aya_ebpf::bpf_printk! #[repr(transparent)] pub struct aya_ebpf::EbpfGlobal impl aya_ebpf::EbpfGlobal where T: core::marker::Copy