Skip to content

Commit

Permalink
Merge pull request #192 from amosproj/161-testing-ebpf
Browse files Browse the repository at this point in the history
161 testing ebpf
  • Loading branch information
der-whity authored Jan 8, 2025
2 parents 412aad2 + ebd3882 commit d4c1870
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 9 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ members = [
"shared",
"client",
"playground/sendmsg-demo",
"uniffi-bindgen",
"uniffi-bindgen", "backend/ebpf-test",
]
default-members = [
"xtask",
Expand Down Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions rust/backend/ebpf-test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# SPDX-FileCopyrightText: 2024 Felix Hilgers <[email protected]>
#
# 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"
162 changes: 162 additions & 0 deletions rust/backend/ebpf-test/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// SPDX-FileCopyrightText: 2024 Benedikt Zinn <[email protected]>
// SPDX-FileCopyrightText: 2024 Felix Hilgers <[email protected]>
// SPDX-FileCopyrightText: 2024 Luca Bretting <[email protected]>
//
// 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}"));
}
}
65 changes: 65 additions & 0 deletions rust/backend/ebpf-test/tests/prog_test_run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: 2025 Franz Schlicht <[email protected]>
//
// 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::<Vec<_>>();
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::<bpf_attr>() };

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::<bpf_attr>()) };

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() });
}
}
6 changes: 4 additions & 2 deletions rust/backend/ebpf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
23 changes: 18 additions & 5 deletions rust/backend/ebpf/src/sys_sigquit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,34 @@
//
// 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<u32, u64> = 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;
Expand Down

0 comments on commit d4c1870

Please sign in to comment.