mirror of https://github.com/aya-rs/aya
Merge pull request #467 from MatteoNardi/relocation_tests
Add integration tests for BTF relocationspull/482/head
commit
4cc0ea09e0
@ -1,74 +1,21 @@
|
||||
use log::info;
|
||||
use libtest_mimic::{Arguments, Trial};
|
||||
|
||||
mod tests;
|
||||
use tests::IntegrationTest;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
#[clap(propagate_version = true)]
|
||||
pub struct RunOptions {
|
||||
#[clap(short, long, value_parser)]
|
||||
tests: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Cli {
|
||||
#[clap(subcommand)]
|
||||
command: Option<Command>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
enum Command {
|
||||
/// Run one or more tests: ... -- run -t test1 -t test2
|
||||
Run(RunOptions),
|
||||
/// List all the tests: ... -- list
|
||||
List,
|
||||
}
|
||||
|
||||
macro_rules! exec_test {
|
||||
($test:expr) => {{
|
||||
info!("Running {}", $test.name);
|
||||
($test.test_fn)();
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! exec_all_tests {
|
||||
() => {{
|
||||
for t in inventory::iter::<IntegrationTest> {
|
||||
exec_test!(t)
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
match &cli.command {
|
||||
Some(Command::Run(opts)) => match &opts.tests {
|
||||
Some(tests) => {
|
||||
for t in inventory::iter::<IntegrationTest> {
|
||||
if tests.contains(&t.name.into()) {
|
||||
exec_test!(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
exec_all_tests!()
|
||||
}
|
||||
},
|
||||
Some(Command::List) => {
|
||||
for t in inventory::iter::<IntegrationTest> {
|
||||
info!("{}", t.name);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
exec_all_tests!()
|
||||
}
|
||||
}
|
||||
|
||||
let mut args = Arguments::from_args();
|
||||
// Force to run single-threaded
|
||||
args.test_threads = Some(1);
|
||||
let tests = inventory::iter::<IntegrationTest>
|
||||
.into_iter()
|
||||
.map(|test| {
|
||||
Trial::test(test.name, move || {
|
||||
(test.test_fn)();
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
libtest_mimic::run(&args, tests).exit();
|
||||
}
|
||||
|
@ -0,0 +1,256 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::{path::PathBuf, process::Command, thread::sleep, time::Duration};
|
||||
use tempfile::TempDir;
|
||||
|
||||
use aya::{maps::Array, programs::TracePoint, BpfLoader, Btf, Endianness};
|
||||
|
||||
use super::{integration_test, IntegrationTest};
|
||||
|
||||
#[integration_test]
|
||||
fn relocate_field() {
|
||||
let test = RelocationTest {
|
||||
local_definition: r#"
|
||||
struct foo {
|
||||
__u8 a;
|
||||
__u8 b;
|
||||
__u8 c;
|
||||
__u8 d;
|
||||
};
|
||||
"#,
|
||||
target_btf: r#"
|
||||
struct foo {
|
||||
__u8 a;
|
||||
__u8 c;
|
||||
__u8 b;
|
||||
__u8 d;
|
||||
} s1;
|
||||
"#,
|
||||
relocation_code: r#"
|
||||
__u8 memory[] = {1, 2, 3, 4};
|
||||
__u32 value = BPF_CORE_READ((struct foo *)&memory, c);
|
||||
"#,
|
||||
}
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(test.run().unwrap(), 2);
|
||||
assert_eq!(test.run_no_btf().unwrap(), 3);
|
||||
}
|
||||
|
||||
#[integration_test]
|
||||
fn relocate_enum() {
|
||||
let test = RelocationTest {
|
||||
local_definition: r#"
|
||||
enum foo { D = 1 };
|
||||
"#,
|
||||
target_btf: r#"
|
||||
enum foo { D = 4 } e1;
|
||||
"#,
|
||||
relocation_code: r#"
|
||||
__u32 value = bpf_core_enum_value(enum foo, D);
|
||||
"#,
|
||||
}
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(test.run().unwrap(), 4);
|
||||
assert_eq!(test.run_no_btf().unwrap(), 1);
|
||||
}
|
||||
|
||||
#[integration_test]
|
||||
fn relocate_pointer() {
|
||||
let test = RelocationTest {
|
||||
local_definition: r#"
|
||||
struct foo {};
|
||||
struct bar { struct foo *f; };
|
||||
"#,
|
||||
target_btf: r#"
|
||||
struct foo {};
|
||||
struct bar { struct foo *f; };
|
||||
"#,
|
||||
relocation_code: r#"
|
||||
__u8 memory[] = {42, 0, 0, 0, 0, 0, 0, 0};
|
||||
struct foo *f = BPF_CORE_READ((struct bar *)&memory, f);
|
||||
__u32 value = ((__u64) f);
|
||||
"#,
|
||||
}
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(test.run().unwrap(), 42);
|
||||
assert_eq!(test.run_no_btf().unwrap(), 42);
|
||||
}
|
||||
|
||||
/// Utility code for running relocation tests:
|
||||
/// - Generates the eBPF program using probided local definition and relocation code
|
||||
/// - Generates the BTF from the target btf code
|
||||
struct RelocationTest {
|
||||
/// Data structure definition, local to the eBPF program and embedded in the eBPF bytecode
|
||||
local_definition: &'static str,
|
||||
/// Target data structure definition. What the vmlinux would actually contain.
|
||||
target_btf: &'static str,
|
||||
/// Code executed by the eBPF program to test the relocation.
|
||||
/// The format should be:
|
||||
// __u8 memory[] = { ... };
|
||||
// __u32 value = BPF_CORE_READ((struct foo *)&memory, ...);
|
||||
//
|
||||
// The generated code will be executed by attaching a tracepoint to sched_switch
|
||||
// and emitting `__u32 value` an a map. See the code template below for more details.
|
||||
relocation_code: &'static str,
|
||||
}
|
||||
|
||||
impl RelocationTest {
|
||||
/// Build a RelocationTestRunner
|
||||
fn build(&self) -> Result<RelocationTestRunner> {
|
||||
Ok(RelocationTestRunner {
|
||||
ebpf: self.build_ebpf()?,
|
||||
btf: self.build_btf()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// - Generate the source eBPF filling a template
|
||||
/// - Compile it with clang
|
||||
fn build_ebpf(&self) -> Result<Vec<u8>> {
|
||||
let local_definition = self.local_definition;
|
||||
let relocation_code = self.relocation_code;
|
||||
let (_tmp_dir, compiled_file) = compile(&format!(
|
||||
r#"
|
||||
#include <linux/bpf.h>
|
||||
|
||||
#include <bpf/bpf_core_read.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
|
||||
{local_definition}
|
||||
|
||||
struct {{
|
||||
__uint(type, BPF_MAP_TYPE_ARRAY);
|
||||
__type(key, __u32);
|
||||
__type(value, __u32);
|
||||
__uint(max_entries, 1);
|
||||
}} output_map SEC(".maps");
|
||||
|
||||
SEC("tracepoint/bpf_prog") int bpf_prog(void *ctx) {{
|
||||
__u32 key = 0;
|
||||
{relocation_code}
|
||||
bpf_map_update_elem(&output_map, &key, &value, BPF_ANY);
|
||||
return 0;
|
||||
}}
|
||||
|
||||
char _license[] SEC("license") = "GPL";
|
||||
"#
|
||||
))
|
||||
.context("Failed to compile eBPF program")?;
|
||||
let bytecode =
|
||||
std::fs::read(compiled_file).context("Error reading compiled eBPF program")?;
|
||||
Ok(bytecode)
|
||||
}
|
||||
|
||||
/// - Generate the target BTF source with a mock main()
|
||||
/// - Compile it with clang
|
||||
/// - Extract the BTF with llvm-objcopy
|
||||
fn build_btf(&self) -> Result<Btf> {
|
||||
let target_btf = self.target_btf;
|
||||
let relocation_code = self.relocation_code;
|
||||
// BTF files can be generated and inspected with these commands:
|
||||
// $ clang -c -g -O2 -target bpf target.c
|
||||
// $ pahole --btf_encode_detached=target.btf -V target.o
|
||||
// $ bpftool btf dump file ./target.btf format c
|
||||
let (tmp_dir, compiled_file) = compile(&format!(
|
||||
r#"
|
||||
#include <linux/bpf.h>
|
||||
|
||||
#include <bpf/bpf_core_read.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
|
||||
{target_btf}
|
||||
int main() {{
|
||||
// This is needed to make sure to emit BTF for the defined types,
|
||||
// it could be dead code eliminated if we don't.
|
||||
{relocation_code};
|
||||
return value;
|
||||
}}
|
||||
"#
|
||||
))
|
||||
.context("Failed to compile BTF")?;
|
||||
Command::new("llvm-objcopy")
|
||||
.current_dir(tmp_dir.path())
|
||||
.args(["--dump-section", ".BTF=target.btf"])
|
||||
.arg(compiled_file)
|
||||
.status()
|
||||
.context("Failed to run llvm-objcopy")?
|
||||
.success()
|
||||
.then_some(())
|
||||
.context("Failed to extract BTF")?;
|
||||
let btf = Btf::parse_file(tmp_dir.path().join("target.btf"), Endianness::default())
|
||||
.context("Error parsing generated BTF")?;
|
||||
Ok(btf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Compile an eBPF program and return the path of the compiled object.
|
||||
/// Also returns a TempDir handler, dropping it will clear the created dicretory.
|
||||
fn compile(source_code: &str) -> Result<(TempDir, PathBuf)> {
|
||||
let tmp_dir = tempfile::tempdir().context("Error making temp dir")?;
|
||||
let source = tmp_dir.path().join("source.c");
|
||||
std::fs::write(&source, source_code).context("Writing bpf program failed")?;
|
||||
Command::new("clang")
|
||||
.current_dir(&tmp_dir)
|
||||
.args(["-c", "-g", "-O2", "-target", "bpf"])
|
||||
// NOTE: these tests depend on libbpf, LIBBPF_INCLUDE must point its headers.
|
||||
// This is set automatically by the integration-test xtask.
|
||||
.args([
|
||||
"-I",
|
||||
&std::env::var("LIBBPF_INCLUDE").context("LIBBPF_INCLUDE not set")?,
|
||||
])
|
||||
.arg(&source)
|
||||
.status()
|
||||
.context("Failed to run clang")?
|
||||
.success()
|
||||
.then_some(())
|
||||
.context("Failed to compile eBPF source")?;
|
||||
Ok((tmp_dir, source.with_extension("o")))
|
||||
}
|
||||
|
||||
struct RelocationTestRunner {
|
||||
ebpf: Vec<u8>,
|
||||
btf: Btf,
|
||||
}
|
||||
|
||||
impl RelocationTestRunner {
|
||||
/// Run test and return the output value
|
||||
fn run(&self) -> Result<u32> {
|
||||
self.run_internal(true).context("Error running with BTF")
|
||||
}
|
||||
|
||||
/// Run without loading btf
|
||||
fn run_no_btf(&self) -> Result<u32> {
|
||||
self.run_internal(false)
|
||||
.context("Error running without BTF")
|
||||
}
|
||||
|
||||
fn run_internal(&self, with_relocations: bool) -> Result<u32> {
|
||||
let mut loader = BpfLoader::new();
|
||||
if with_relocations {
|
||||
loader.btf(Some(&self.btf));
|
||||
} else {
|
||||
loader.btf(None);
|
||||
}
|
||||
let mut bpf = loader.load(&self.ebpf).context("Loading eBPF failed")?;
|
||||
let program: &mut TracePoint = bpf
|
||||
.program_mut("bpf_prog")
|
||||
.context("bpf_prog not found")?
|
||||
.try_into()
|
||||
.context("program not a tracepoint")?;
|
||||
program.load().context("Loading tracepoint failed")?;
|
||||
// Attach to sched_switch and wait some time to make sure it executed at least once
|
||||
program
|
||||
.attach("sched", "sched_switch")
|
||||
.context("attach failed")?;
|
||||
sleep(Duration::from_millis(1000));
|
||||
// To inspect the loaded eBPF bytecode, increse the timeout and run:
|
||||
// $ sudo bpftool prog dump xlated name bpf_prog
|
||||
|
||||
let output_map: Array<_, u32> = bpf.take_map("output_map").unwrap().try_into().unwrap();
|
||||
let key = 0;
|
||||
output_map.get(&key, 0).context("Getting key 0 failed")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue