From 4ed596bbc8ab9fd56bff6360fbad0f1fd8b3f84b Mon Sep 17 00:00:00 2001 From: Felix Hilgers Date: Wed, 8 Jan 2025 10:12:57 +0100 Subject: [PATCH 1/3] test: make sys_sigquit testable Signed-off-by: Felix Hilgers --- rust/backend/ebpf/Cargo.toml | 6 ++++-- rust/backend/ebpf/src/sys_sigquit.rs | 23 ++++++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/rust/backend/ebpf/Cargo.toml b/rust/backend/ebpf/Cargo.toml index cb63ae50..809d403e 100644 --- a/rust/backend/ebpf/Cargo.toml +++ b/rust/backend/ebpf/Cargo.toml @@ -13,14 +13,16 @@ edition.workspace = true [dependencies] backend-common = { workspace = true } - aya-ebpf = { workspace = true } aya-log-ebpf = { workspace = true } +[features] +prog-test = [] + [build-dependencies] which = { workspace = true } xtask = { workspace = true } [[bin]] name = "backend-ebpf" -path = "src/main.rs" +path = "src/main.rs" \ No newline at end of file diff --git a/rust/backend/ebpf/src/sys_sigquit.rs b/rust/backend/ebpf/src/sys_sigquit.rs index 0dddf53c..30eca69c 100644 --- a/rust/backend/ebpf/src/sys_sigquit.rs +++ b/rust/backend/ebpf/src/sys_sigquit.rs @@ -2,10 +2,10 @@ // // SPDX-License-Identifier: MIT -use aya_ebpf::{macros::{tracepoint, map}, maps::{RingBuf}, programs::{TracePointContext}, EbpfContext, helpers::gen::bpf_ktime_get_ns}; +use aya_ebpf::{helpers::gen::bpf_ktime_get_ns, macros::map, maps::RingBuf, programs::TracePointContext, EbpfContext}; use aya_ebpf::maps::HashMap; use aya_log_ebpf::error; -use backend_common::{SysSigquitCall}; +use backend_common::SysSigquitCall; #[map(name = "SYS_SIGQUIT_PIDS")] static SYS_SIGQUIT_PIDS: HashMap = HashMap::pinned(4096, 0); @@ -13,10 +13,23 @@ static SYS_SIGQUIT_PIDS: HashMap = HashMap::pinned(4096, 0); #[map(name = "SYS_SIGQUIT_EVENTS")] pub static SYS_SIGQUIT_EVENTS: RingBuf = RingBuf::pinned(1024, 0); -#[tracepoint] -pub fn sys_sigquit(ctx: TracePointContext) -> u32 { - let pid = ctx.pid(); +// Disclaimer: +// We have to swap here, because BPF_PROG_TEST_RUN does not support Tracepoints +// For testing we can set the prog-test flag and interpret it as TracepointContext, because we can set whatever we want +// For an example see backend/daemon/src/prog_test_run.rs +#[cfg(feature = "prog-test")] +type Arg = aya_ebpf::programs::RawTracePointContext; + +#[cfg(not(feature = "prog-test"))] +type Arg = aya_ebpf::programs::TracePointContext; + +#[cfg_attr(feature = "prog-test", aya_ebpf::macros::raw_tracepoint)] +#[cfg_attr(not(feature = "prog-test"), aya_ebpf::macros::tracepoint)] +pub fn sys_sigquit(ctx: Arg) -> u32 { + let ctx = TracePointContext::new(ctx.as_ptr()); + let pid = ctx.pid(); + if unsafe { SYS_SIGQUIT_PIDS.get(&pid).is_none() } { // ignore signals from this pid return 0; From 7a290c0fc444b506ea73c512c93c7a343fc4d43b Mon Sep 17 00:00:00 2001 From: Felix Hilgers Date: Wed, 8 Jan 2025 10:13:52 +0100 Subject: [PATCH 2/3] test: write example BPF_PROG_TEST_RUN test case Signed-off-by: Felix Hilgers --- rust/Cargo.lock | 14 ++ rust/Cargo.toml | 3 +- rust/backend/ebpf-test/Cargo.toml | 27 +++ rust/backend/ebpf-test/build.rs | 162 ++++++++++++++++++ rust/backend/ebpf-test/tests/prog_test_run.rs | 65 +++++++ 5 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 rust/backend/ebpf-test/Cargo.toml create mode 100644 rust/backend/ebpf-test/build.rs create mode 100644 rust/backend/ebpf-test/tests/prog_test_run.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a6939483..fc646d31 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -440,6 +440,20 @@ dependencies = [ "xtask", ] +[[package]] +name = "backend-ebpf-test" +version = "0.1.0" +dependencies = [ + "aya", + "aya-ebpf", + "aya-log-ebpf", + "aya-obj", + "backend-common", + "cargo_metadata 0.19.1", + "libc", + "which", +] + [[package]] name = "backtrace" version = "0.3.74" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index dfda1755..8802f3ff 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -15,7 +15,7 @@ members = [ "shared", "client", "playground/sendmsg-demo", - "uniffi-bindgen", + "uniffi-bindgen", "backend/ebpf-test", ] default-members = [ "xtask", @@ -72,6 +72,7 @@ object = "0.36.5" bytemuck = { version = "1.20.0" } crossbeam = "0.8.4" ractor = { version = "0.13.4", default-features = false } +aya-obj = "0.2.1" [profile.release.package.backend-ebpf] debug = 2 diff --git a/rust/backend/ebpf-test/Cargo.toml b/rust/backend/ebpf-test/Cargo.toml new file mode 100644 index 00000000..8dc77fdd --- /dev/null +++ b/rust/backend/ebpf-test/Cargo.toml @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2024 Felix Hilgers +# +# SPDX-License-Identifier: MIT + +[package] +name = "backend-ebpf-test" +version = "0.1.0" +license.workspace = true +repository.workspace = true +edition.workspace = true + +[dependencies] +backend-common = { workspace = true } +aya-ebpf = { workspace = true } +aya-log-ebpf = { workspace = true } + +aya = { workspace = true } +libc = { workspace = true } +aya-obj = { workspace = true } + +[build-dependencies] +which = { workspace = true } +cargo_metadata = { workspace = true } + +[[test]] +name = "prog-test-run" +path = "tests/prog_test_run.rs" \ No newline at end of file diff --git a/rust/backend/ebpf-test/build.rs b/rust/backend/ebpf-test/build.rs new file mode 100644 index 00000000..91a9402d --- /dev/null +++ b/rust/backend/ebpf-test/build.rs @@ -0,0 +1,162 @@ +// SPDX-FileCopyrightText: 2024 Benedikt Zinn +// SPDX-FileCopyrightText: 2024 Felix Hilgers +// SPDX-FileCopyrightText: 2024 Luca Bretting +// +// SPDX-License-Identifier: MIT +// + +// TODO: this is a verbatim copy of backend-daemon/build.rs with just another build variant of the ebpf program + +use std::{ + env, fs, + io::{BufRead as _, BufReader}, + path::PathBuf, + process::{Child, Command, Stdio}, +}; + +use cargo_metadata::{ + Artifact, CompilerMessage, Message, Metadata, MetadataCommand, Package, Target, TargetKind, +}; + +/// This crate has a runtime dependency on artifacts produced by the `example-ebpf` crate. +/// This would be better expressed as one or more [artifact-dependencies][bindeps] but issues such +/// as: +/// +/// * https://github.com/rust-lang/cargo/issues/12374 +/// * https://github.com/rust-lang/cargo/issues/12375 +/// * https://github.com/rust-lang/cargo/issues/12385 +/// +/// prevent their use for the time being. +/// +/// This file, along with the xtask crate, allows analysis tools such as `cargo check`, `cargo +/// clippy`, and even `cargo build` to work as users expect. Prior to this file's existence, this +/// crate's undeclared dependency on artifacts from `example-ebpf` would cause build (and +/// `cargo check`, and `cargo clippy`) failures until the user ran certain other commands in the +/// workspace. Conversely, those same tools (e.g. cargo test --no-run) would produce stale results +/// if run naively because they'd make use of artifacts from a previous build of +/// `example-ebpf`. +/// +/// Note that this solution is imperfect: in particular it has to balance correctness with +/// performance; an environment variable is used to replace true builds of `example-ebpf` +/// with stubs to preserve the property that code generation and linking (in +/// `example-ebpf`) do not occur on metadata-only actions such as `cargo check` or `cargo +/// clippy` of this crate. This means that naively attempting to `cargo test --no-run` this crate +/// will produce binaries that fail at runtime because the stubs are inadequate for actually running +/// the tests. +/// +/// [bindeps]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html?highlight=feature#artifact-dependencies +fn main() { + let Metadata { packages, .. } = MetadataCommand::new().no_deps().exec().unwrap(); + let ebpf_package = packages + .into_iter() + .find(|Package { name, .. }| name == "backend-ebpf") + .unwrap(); + + let out_dir = env::var_os("OUT_DIR").unwrap(); + let out_dir = PathBuf::from(out_dir); + + let endian = env::var_os("CARGO_CFG_TARGET_ENDIAN").unwrap(); + let target = if endian == "big" { + "bpfeb" + } else if endian == "little" { + "bpfel" + } else { + panic!("unsupported endian={:?}", endian) + }; + + let arch = env::var_os("CARGO_CFG_TARGET_ARCH").unwrap(); + + let target = format!("{target}-unknown-none"); + + let Package { manifest_path, .. } = ebpf_package; + let ebpf_dir = manifest_path.parent().unwrap(); + + // We have a build-dependency on `example-ebpf`, so cargo will automatically rebuild us + // if `example-ebpf`'s *library* target or any of its dependencies change. Since we + // depend on `example-ebpf`'s *binary* targets, that only gets us half of the way. This + // stanza ensures cargo will rebuild us on changes to the binaries too, which gets us the + // rest of the way. + println!("cargo:rerun-if-changed={}", ebpf_dir.as_str()); + + let mut cmd = Command::new("cargo"); + cmd.args([ + "build", + "-Z", + "build-std=core", + "--bins", + "--features", "prog-test", + "--message-format=json", + "--release", + "--target", + &target, + ]); + + cmd.env("CARGO_CFG_BPF_TARGET_ARCH", arch); + + // Workaround to make sure that the rust-toolchain.toml is respected. + for key in ["RUSTUP_TOOLCHAIN", "RUSTC"] { + cmd.env_remove(key); + } + cmd.current_dir(ebpf_dir); + + // Workaround for https://github.com/rust-lang/cargo/issues/6412 where cargo flocks itself. + let ebpf_target_dir = out_dir.join("backend/ebpf"); + cmd.arg("--target-dir").arg(&ebpf_target_dir); + + let mut child = cmd + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap_or_else(|err| panic!("failed to spawn {cmd:?}: {err}")); + let Child { stdout, stderr, .. } = &mut child; + + // Trampoline stdout to cargo warnings. + let stderr = stderr.take().unwrap(); + let stderr = BufReader::new(stderr); + let stderr = std::thread::spawn(move || { + for line in stderr.lines() { + let line = line.unwrap(); + println!("cargo:warning={line}"); + } + }); + + let stdout = stdout.take().unwrap(); + let stdout = BufReader::new(stdout); + let mut executables = Vec::new(); + for message in Message::parse_stream(stdout) { + #[allow(clippy::collapsible_match)] + match message.expect("valid JSON") { + Message::CompilerArtifact(Artifact { + executable, + target: Target { name, .. }, + .. + }) => { + if let Some(executable) = executable { + executables.push((name, executable.into_std_path_buf())); + } + } + Message::CompilerMessage(CompilerMessage { message, .. }) => { + for line in message.rendered.unwrap_or_default().split('\n') { + println!("cargo:warning={line}"); + } + } + Message::TextLine(line) => { + println!("cargo:warning={line}"); + } + _ => {} + } + } + + let status = child + .wait() + .unwrap_or_else(|err| panic!("failed to wait for {cmd:?}: {err}")); + assert_eq!(status.code(), Some(0), "{cmd:?} failed: {status:?}"); + + stderr.join().map_err(std::panic::resume_unwind).unwrap(); + + for (name, binary) in executables { + let dst = out_dir.join(name); + let _: u64 = fs::copy(&binary, &dst) + .unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}")); + } +} diff --git a/rust/backend/ebpf-test/tests/prog_test_run.rs b/rust/backend/ebpf-test/tests/prog_test_run.rs new file mode 100644 index 00000000..181db8e8 --- /dev/null +++ b/rust/backend/ebpf-test/tests/prog_test_run.rs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2025 Franz Schlicht +// +// SPDX-License-Identifier: MIT + +use std::{io, mem, os::fd::{AsFd, AsRawFd}}; + +use aya::{maps::{HashMap, RingBuf}, programs::RawTracePoint, EbpfLoader}; +use aya_obj::generated::{bpf_attr, bpf_cmd}; +use backend_common::{SysSigquitCall, TryFromRaw}; +use libc::{getpid, gettid, syscall, SYS_bpf}; + + +#[test] +fn prog_test_run_example() { + let mut ebpf = EbpfLoader::default() + .load(aya::include_bytes_aligned!(concat!( + env!("OUT_DIR"), + "/backend-ebpf" + ))) + .unwrap(); + + let p: &mut RawTracePoint = ebpf.program_mut("sys_sigquit").unwrap().try_into().unwrap(); + p.load().unwrap(); + p.attach("sys_enter").unwrap(); + + let fd = p.fd().unwrap().as_fd().as_raw_fd(); + + let mut pids: HashMap<_, u32, u64> = ebpf.take_map("SYS_SIGQUIT_PIDS").unwrap().try_into().unwrap(); + let old = pids.iter().filter_map(Result::ok).map(|x| x.0).collect::>(); + for old in old { + pids.remove(&old).unwrap(); + } + // Pid of the program seems to always be the next pid + pids.insert(unsafe { gettid() as u32 }, 0, 0).unwrap(); + let mut events: RingBuf<_> = ebpf.take_map("SYS_SIGQUIT_EVENTS").unwrap().try_into().unwrap(); + + let target_pid = 1111; + let signal = 3; // sigquit + let args = [0u64, 0u64, target_pid, signal]; + + let mut attr = unsafe { mem::zeroed::() }; + + attr.test.prog_fd = fd as u32; + attr.test.ctx_in = args.as_ptr() as u64; + attr.test.ctx_size_in = args.len() as u32 * 8; + + let _ = { + let ret = unsafe { syscall(SYS_bpf, bpf_cmd::BPF_PROG_TEST_RUN, &mut attr, size_of::()) }; + + match ret { + 0.. => Ok(ret), + ret => Err((ret, io::Error::last_os_error())), + } + }.unwrap(); + + println!("{:?}", unsafe { attr.test }); + + let first = events.next().unwrap().to_vec(); + + for next in [first] { + println!("{next:?}"); + println!("{:?}", SysSigquitCall::try_from_raw(&*next)); + println!("{} {}", unsafe { gettid() }, unsafe { getpid() }); + } +} \ No newline at end of file From ebd38824336eb2cc94334409eb10cb443667f856 Mon Sep 17 00:00:00 2001 From: Felix Hilgers Date: Wed, 8 Jan 2025 10:14:26 +0100 Subject: [PATCH 3/3] ci: exclude ebpf test Signed-off-by: Felix Hilgers --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index c1455048..fc7d17de 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -67,7 +67,7 @@ jobs: - name: Cargo test run: | cd rust - nix develop --command cargo test --workspace --all-targets --all-features --exclude client + nix develop --command cargo test --workspace --all-targets --all-features --exclude client --exclude backend-ebpf-test rust-build: name: Rust Build