Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

161 testing ebpf #192

Merged
merged 3 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading