From dca5e6c1674f1f9d9db70c7ea522e77228d637eb Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Mon, 31 Jul 2023 13:57:25 -0400 Subject: [PATCH] integration-test: Remove runtime toolchain deps Move the use of clang and llvm-objcopy from run-time to build-time. This allows the integration tests to run on VMs with simpler userlands. Create a new CI job to build the integration tests separately from running them. Ship them from that job to the runner job using github actions artifacts. --- .github/workflows/ci.yml | 85 +++- Cargo.toml | 3 +- test/README.md | 4 +- test/integration-test/Cargo.toml | 1 + test/integration-test/bpf/reloc.bpf.c | 106 ++++ test/integration-test/bpf/reloc.btf.c | 77 +++ test/integration-test/build.rs | 60 ++- test/integration-test/src/lib.rs | 2 + .../src/tests/btf_relocations.rs | 461 +++--------------- test/run.sh | 13 +- 10 files changed, 377 insertions(+), 435 deletions(-) create mode 100644 test/integration-test/bpf/reloc.bpf.c create mode 100644 test/integration-test/bpf/reloc.btf.c diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b431e17..e2ab4cee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,7 @@ jobs: - name: Run miri run: | + set -euxo pipefail cargo hack miri test --all-targets --feature-powerset \ --exclude aya-bpf \ --exclude aya-bpf-bindings \ @@ -79,6 +80,7 @@ jobs: - name: Build run: | + set -euxo pipefail cargo hack build --all-targets --feature-powerset \ --exclude aya-bpf \ --exclude aya-bpf-bindings \ @@ -90,6 +92,7 @@ jobs: env: RUST_BACKTRACE: full run: | + set -euxo pipefail cargo hack test --all-targets --feature-powerset \ --exclude aya-bpf \ --exclude aya-bpf-bindings \ @@ -122,7 +125,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - - name: Prereqs + - name: bpf-linker run: cargo install bpf-linker --git https://github.com/aya-rs/bpf-linker.git - uses: taiki-e/install-action@cargo-hack @@ -130,26 +133,71 @@ jobs: env: CARGO_CFG_BPF_TARGET_ARCH: ${{ matrix.arch }} run: | + set -euxo pipefail cargo hack build --package aya-bpf --package aya-log-ebpf \ --feature-powerset \ --target ${{ matrix.target }} \ -Z build-std=core - integration-test: - runs-on: macos-latest - strategy: - fail-fast: false - matrix: - # See https://doc.rust-lang.org/cargo/reference/profiles.html for the names - # of the builtin profiles. Note that dev builds "debug" targets. - profile: - - release - - dev + build-integration-test: + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 with: submodules: recursive + - uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly + components: rust-src + + - uses: Swatinem/rust-cache@v2 + + - name: bpf-linker + run: cargo install bpf-linker --git https://github.com/aya-rs/bpf-linker.git + + - name: Install dependencies + # ubuntu-22.04 comes with clang 14[0] which doesn't include support for signed and 64bit + # enum values which was added in clang 15[1]. + # + # gcc-multilib provides at least which is referenced by libbpf. + # + # llvm provides llvm-objcopy which is used to build the BTF relocation tests. + # + # [0] https://github.com/actions/runner-images/blob/ubuntu22/20230724.1/images/linux/Ubuntu2204-Readme.md + # + # [1] https://github.com/llvm/llvm-project/commit/dc1c43d + run: | + set -euxo pipefail + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc + echo deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy main | sudo tee /etc/apt/sources.list.d/llvm.list + sudo apt-get update + sudo apt-get -y install clang gcc-multilib llvm + + - name: Build + run: | + set -euxo pipefail + mkdir -p integration-test-binaries + # See https://doc.rust-lang.org/cargo/reference/profiles.html for the + # names of the builtin profiles. Note that dev builds "debug" targets. + cargo xtask build-integration-test --cargo-arg=--profile=dev | xargs -I % cp % integration-test-binaries/dev + cargo xtask build-integration-test --cargo-arg=--profile=release | xargs -I % cp % integration-test-binaries/release + + - uses: actions/upload-artifact@v3 + with: + name: integration-test-binaries + path: integration-test-binaries + + run-integration-test: + runs-on: macos-latest + needs: ["build-integration-test"] + steps: + - uses: actions/checkout@v3 + with: + sparse-checkout: | + test/run.sh + test/cloud-localds + - name: Install Pre-requisites run: | brew install qemu gnu-getopt coreutils cdrtools @@ -161,20 +209,25 @@ jobs: .tmp/*.qcow2 .tmp/test_rsa .tmp/test_rsa.pub - # FIXME: we should invalidate the cache on new bpf-linker releases. - # For now we must manually delete the cache when we release a new - # bpf-linker version. key: tmp-files-${{ hashFiles('test/run.sh') }} + - uses: actions/download-artifact@v3 + with: + name: integration-test-binaries + path: integration-test-binaries + - name: Run integration tests - run: test/run.sh --cargo-arg=--profile=${{ matrix.profile }} + run: | + set -euxo pipefail + find integration-test-binaries -type f -exec chmod +x {} \; + test/run.sh integration-test-binaries # Provides a single status check for the entire build workflow. # This is used for merge automation, like Mergify, since GH actions # has no concept of "when all status checks pass". # https://docs.mergify.com/conditions/#validating-all-status-checks build-workflow-complete: - needs: ["lint", "build-test-aya", "build-test-aya-bpf", "integration-test"] + needs: ["lint", "build-test-aya", "build-test-aya-bpf", "run-integration-test"] runs-on: ubuntu-latest steps: - name: Build Complete diff --git a/Cargo.toml b/Cargo.toml index 2dfb461c..3b37003f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,8 +75,8 @@ netns-rs = { version = "0.1", default-features = false } num_enum = { version = "0.6", default-features = false } object = { version = "0.31", default-features = false } parking_lot = { version = "0.12.0", default-features = false } -proc-macro2 = { version = "1", default-features = false } proc-macro-error = { version = "1.0", default-features = false } +proc-macro2 = { version = "1", default-features = false } public-api = { version = "0.31.2", default-features = false } quote = { version = "1", default-features = false } rbpf = { version = "0.2.0", default-features = false } @@ -84,6 +84,7 @@ rustdoc-json = { version = "0.8.6", default-features = false } rustup-toolchain = { version = "0.1.5", default-features = false } syn = { version = "2", default-features = false } tempfile = { version = "3", default-features = false } +test-case = { version = "3.1.0", default-features = false } testing_logger = { version = "0.1.1", default-features = false } thiserror = { version = "1", default-features = false } tokio = { version = "1.24.0", default-features = false } diff --git a/test/README.md b/test/README.md index f7a8429e..0a6d1b84 100644 --- a/test/README.md +++ b/test/README.md @@ -33,7 +33,9 @@ cargo xtask integration-test ### Virtualized ``` -./test/run.sh +mkdir -p integration-test-binaries +cargo xtask build-integration-test | xargs -I % cp % integration-test-binaries +./test/run.sh integration-test-binaries ``` ### Writing an integration test diff --git a/test/integration-test/Cargo.toml b/test/integration-test/Cargo.toml index e4e91647..876a9a86 100644 --- a/test/integration-test/Cargo.toml +++ b/test/integration-test/Cargo.toml @@ -15,6 +15,7 @@ log = { workspace = true } netns-rs = { workspace = true } object = { workspace = true } rbpf = { workspace = true } +test-case = { workspace = true } tokio = { workspace = true, default-features = false, features = [ "macros", "time", diff --git a/test/integration-test/bpf/reloc.bpf.c b/test/integration-test/bpf/reloc.bpf.c new file mode 100644 index 00000000..510406dd --- /dev/null +++ b/test/integration-test/bpf/reloc.bpf.c @@ -0,0 +1,106 @@ +// clang-format off +#include +#include +#include +// clang-format on + +char _license[] __attribute__((section("license"), used)) = "GPL"; + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __type(key, __u32); + __type(value, __u64); + __uint(max_entries, 1); +} output_map SEC(".maps"); + +long set_output(__u64 value) { + __u32 key = 0; + return bpf_map_update_elem(&output_map, &key, &value, BPF_ANY); +} + +struct relocated_struct_with_scalars { + __u8 a; + __u8 b; + __u8 c; +}; + +__attribute__((noinline)) int field_global() { + struct relocated_struct_with_scalars s = {1, 2, 3}; + return set_output(__builtin_preserve_access_index(s.b)); +} + +SEC("uprobe/field") int field(void *ctx) { + return field_global(); +} + +struct relocated_struct_with_pointer { + struct relocated_struct_with_pointer *first; + struct relocated_struct_with_pointer *second; +}; + +__attribute__((noinline)) int pointer_global() { + struct relocated_struct_with_pointer s = { + (struct relocated_struct_with_pointer *)42, + (struct relocated_struct_with_pointer *)21, + }; + return set_output((__u64)__builtin_preserve_access_index(s.first)); +} + +SEC("uprobe/pointer") int pointer(void *ctx) { + return pointer_global(); +} + +__attribute__((noinline)) int struct_flavors_global() { + struct relocated_struct_with_scalars s = {1, 2, 3}; + if (bpf_core_field_exists(s.a)) { + return set_output(__builtin_preserve_access_index(s.a)); + } else { + return set_output(__builtin_preserve_access_index(s.b)); + } +} + +SEC("uprobe/struct_flavors") int struct_flavors(void *ctx) { + return struct_flavors_global(); +} + +enum relocated_enum_unsigned_32 { U32 = 0xAAAAAAAA }; + +__attribute__((noinline)) int enum_unsigned_32_global() { + return set_output(bpf_core_enum_value(enum relocated_enum_unsigned_32, U32)); +} + +SEC("uprobe/enum_unsigned_32") +int enum_unsigned_32(void *ctx) { + return enum_unsigned_32_global(); +} + +enum relocated_enum_signed_32 { S32 = -0x7AAAAAAA }; + +__attribute__((noinline)) int enum_signed_32_global() { + return set_output(bpf_core_enum_value(enum relocated_enum_signed_32, S32)); +} + +SEC("uprobe/enum_signed_32") int enum_signed_32(void *ctx) { + return enum_signed_32_global(); +} + +enum relocated_enum_unsigned_64 { U64 = 0xAAAAAAAABBBBBBBB }; + +__attribute__((noinline)) int enum_unsigned_64_global() { + return set_output(bpf_core_enum_value(enum relocated_enum_unsigned_64, U64)); +} + +SEC("uprobe/enum_unsigned_64") +int enum_unsigned_64(void *ctx) { + return enum_unsigned_64_global(); +} + +enum relocated_enum_signed_64 { u64 = -0xAAAAAAABBBBBBBB }; + +__attribute__((noinline)) int enum_signed_64_global() { + return set_output(bpf_core_enum_value(enum relocated_enum_signed_64, u64)); +} + +SEC("uprobe/enum_signed_64") int enum_signed_64(void *ctx) { + return enum_signed_64_global(); +} diff --git a/test/integration-test/bpf/reloc.btf.c b/test/integration-test/bpf/reloc.btf.c new file mode 100644 index 00000000..86801061 --- /dev/null +++ b/test/integration-test/bpf/reloc.btf.c @@ -0,0 +1,77 @@ +// clang-format off +#include +#include +#include +// clang-format on + +#include + +long set_output(__u64 value) { exit((int)value); } + +struct relocated_struct_with_scalars { + __u8 b; + __u8 c; + __u8 d; +}; + +__attribute__((noinline)) int field_global() { + struct relocated_struct_with_scalars s = {1, 2, 3}; + return set_output(__builtin_preserve_access_index(s.b)); +} + +struct relocated_struct_with_pointer { + struct relocated_struct_with_pointer *second; + struct relocated_struct_with_pointer *first; +}; + +__attribute__((noinline)) int pointer_global() { + struct relocated_struct_with_pointer s = { + (struct relocated_struct_with_pointer *)42, + (struct relocated_struct_with_pointer *)21, + }; + return set_output((__u64)__builtin_preserve_access_index(s.first)); +} + +__attribute__((noinline)) int struct_flavors_global() { + struct relocated_struct_with_scalars s = {1, 2, 3}; + if (bpf_core_field_exists(s.b)) { + return set_output(__builtin_preserve_access_index(s.b)); + } else { + return set_output(__builtin_preserve_access_index(s.c)); + } +} + +enum relocated_enum_unsigned_32 { U32 = 0xBBBBBBBB }; + +__attribute__((noinline)) int enum_unsigned_32_global() { + return set_output(bpf_core_enum_value(enum relocated_enum_unsigned_32, U32)); +} + +enum relocated_enum_signed_32 { S32 = -0x7BBBBBBB }; + +__attribute__((noinline)) int enum_signed_32_global() { + return set_output(bpf_core_enum_value(enum relocated_enum_signed_32, S32)); +} + +enum relocated_enum_unsigned_64 { U64 = 0xCCCCCCCCDDDDDDDD }; + +__attribute__((noinline)) int enum_unsigned_64_global() { + return set_output(bpf_core_enum_value(enum relocated_enum_unsigned_64, U64)); +} + +enum relocated_enum_signed_64 { u64 = -0xCCCCCCCDDDDDDDD }; + +__attribute__((noinline)) int enum_signed_64_global() { + return set_output(bpf_core_enum_value(enum relocated_enum_signed_64, u64)); +} + +// Avoids dead code elimination by the compiler. +int main() { + field_global(); + pointer_global(); + struct_flavors_global(); + enum_unsigned_32_global(); + enum_signed_32_global(); + enum_unsigned_64_global(); + enum_signed_64_global(); +} diff --git a/test/integration-test/build.rs b/test/integration-test/build.rs index 8ce23465..cd816164 100644 --- a/test/integration-test/build.rs +++ b/test/integration-test/build.rs @@ -64,16 +64,19 @@ fn main() { panic!("unsupported endian={:?}", endian) }; - const C_BPF_PROBES: &[(&str, &str)] = &[ + const C_BPF: &[(&str, &str)] = &[ ("ext.bpf.c", "ext.bpf.o"), ("main.bpf.c", "main.bpf.o"), ("multimap-btf.bpf.c", "multimap-btf.bpf.o"), + ("reloc.bpf.c", "reloc.bpf.o"), ("text_64_64_reloc.c", "text_64_64_reloc.o"), ]; - let c_bpf_probes = C_BPF_PROBES - .iter() - .map(|(src, dst)| (src, out_dir.join(dst))); + let c_bpf = C_BPF.iter().map(|(src, dst)| (src, out_dir.join(dst))); + + const C_BTF: &[(&str, &str)] = &[("reloc.btf.c", "reloc.btf.o")]; + + let c_btf = C_BTF.iter().map(|(src, dst)| (src, out_dir.join(dst))); if build_integration_bpf { let libbpf_dir = manifest_dir @@ -113,7 +116,7 @@ fn main() { target_arch.push(arch); }; - for (src, dst) in c_bpf_probes { + for (src, dst) in c_bpf { let src = bpf_dir.join(src); println!("cargo:rerun-if-changed={}", src.to_str().unwrap()); @@ -130,6 +133,51 @@ fn main() { .unwrap(); } + for (src, dst) in c_btf { + let src = bpf_dir.join(src); + println!("cargo:rerun-if-changed={}", src.to_str().unwrap()); + + let mut cmd = Command::new("clang"); + cmd.arg("-I") + .arg(&libbpf_headers_dir) + .args(["-g", "-target", target, "-c"]) + .arg(&target_arch) + .arg(src) + .args(["-o", "-"]); + + let mut child = cmd + .stdout(Stdio::piped()) + .spawn() + .unwrap_or_else(|err| panic!("failed to spawn {cmd:?}: {err}")); + + let Child { stdout, .. } = &mut child; + let stdout = stdout.take().unwrap(); + + let mut output = OsString::new(); + output.push(".BTF="); + output.push(dst); + exec( + // NB: objcopy doesn't support reading from stdin, so we have to use llvm-objcopy. + Command::new("llvm-objcopy") + .arg("--dump-section") + .arg(output) + .arg("-") + .stdin(stdout), + ) + .unwrap(); + + let status = child + .wait() + .unwrap_or_else(|err| panic!("failed to wait for {cmd:?}: {err}")); + match status.code() { + Some(code) => match code { + 0 => {} + code => panic!("{cmd:?} exited with status code {code}"), + }, + None => panic!("{cmd:?} terminated by signal"), + } + } + let target = format!("{target}-unknown-none"); let Package { manifest_path, .. } = integration_ebpf_package; @@ -225,7 +273,7 @@ fn main() { .unwrap_or_else(|err| panic!("failed to copy {binary:?} to {dst:?}: {err}")); } } else { - for (_src, dst) in c_bpf_probes { + for (_src, dst) in c_bpf.chain(c_btf) { fs::write(&dst, []).unwrap_or_else(|err| panic!("failed to create {dst:?}: {err}")); } diff --git a/test/integration-test/src/lib.rs b/test/integration-test/src/lib.rs index 043b0cff..80cd0b1d 100644 --- a/test/integration-test/src/lib.rs +++ b/test/integration-test/src/lib.rs @@ -4,6 +4,8 @@ pub const EXT: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/ext.bpf pub const MAIN: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/main.bpf.o")); pub const MULTIMAP_BTF: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/multimap-btf.bpf.o")); +pub const RELOC_BPF: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/reloc.bpf.o")); +pub const RELOC_BTF: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/reloc.btf.o")); pub const TEXT_64_64_RELOC: &[u8] = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/text_64_64_reloc.o")); diff --git a/test/integration-test/src/tests/btf_relocations.rs b/test/integration-test/src/tests/btf_relocations.rs index 75f36223..7e1020ba 100644 --- a/test/integration-test/src/tests/btf_relocations.rs +++ b/test/integration-test/src/tests/btf_relocations.rs @@ -1,405 +1,62 @@ -use anyhow::{anyhow, bail, Context as _, Result}; -use std::{ - process::{Child, ChildStdout, Command, Stdio}, - thread::sleep, - time::Duration, -}; - -use aya::{maps::Array, programs::TracePoint, util::KernelVersion, BpfLoader, Btf, Endianness}; - -// In the tests below we often use values like 0xAAAAAAAA or -0x7AAAAAAA. Those values have no -// special meaning, they just have "nice" bit patterns that can be helpful while debugging. - -#[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}; - struct foo *ptr = (struct foo *) &memory; - value = __builtin_preserve_access_index(ptr->c); - "#, - } - .build() - .unwrap(); - assert_eq!(test.run().unwrap(), 2); - assert_eq!(test.run_no_btf().unwrap(), 3); -} - -#[test] -fn relocate_enum() { - let test = RelocationTest { - local_definition: r#" - enum foo { D = 0xAAAAAAAA }; - "#, - target_btf: r#" - enum foo { D = 0xBBBBBBBB } e1; - "#, - relocation_code: r#" - #define BPF_ENUMVAL_VALUE 1 - value = __builtin_preserve_enum_value(*(typeof(enum foo) *)D, BPF_ENUMVAL_VALUE); - "#, - } - .build() - .unwrap(); - assert_eq!(test.run().unwrap(), 0xBBBBBBBB); - assert_eq!(test.run_no_btf().unwrap(), 0xAAAAAAAA); -} - -#[test] -fn relocate_enum_signed() { - let kernel_version = KernelVersion::current().unwrap(); - if kernel_version < KernelVersion::new(6, 0, 0) { - eprintln!("skipping test on kernel {kernel_version:?}, support for signed enum was added in 6.0.0; see https://github.com/torvalds/linux/commit/6089fb3"); - return; - } - let test = RelocationTest { - local_definition: r#" - enum foo { D = -0x7AAAAAAA }; - "#, - target_btf: r#" - enum foo { D = -0x7BBBBBBB } e1; - "#, - relocation_code: r#" - #define BPF_ENUMVAL_VALUE 1 - value = __builtin_preserve_enum_value(*(typeof(enum foo) *)D, BPF_ENUMVAL_VALUE); - "#, - } - .build() - .unwrap(); - assert_eq!(test.run().unwrap() as i64, -0x7BBBBBBBi64); - assert_eq!(test.run_no_btf().unwrap() as i64, -0x7AAAAAAAi64); -} - -#[test] -fn relocate_enum64() { - let kernel_version = KernelVersion::current().unwrap(); - if kernel_version < KernelVersion::new(6, 0, 0) { - eprintln!("skipping test on kernel {kernel_version:?}, support for enum64 was added in 6.0.0; see https://github.com/torvalds/linux/commit/6089fb3"); - return; - } - let test = RelocationTest { - local_definition: r#" - enum foo { D = 0xAAAAAAAABBBBBBBB }; - "#, - target_btf: r#" - enum foo { D = 0xCCCCCCCCDDDDDDDD } e1; - "#, - relocation_code: r#" - #define BPF_ENUMVAL_VALUE 1 - value = __builtin_preserve_enum_value(*(typeof(enum foo) *)D, BPF_ENUMVAL_VALUE); - "#, - } - .build() - .unwrap(); - assert_eq!(test.run().unwrap(), 0xCCCCCCCCDDDDDDDD); - assert_eq!(test.run_no_btf().unwrap(), 0xAAAAAAAABBBBBBBB); -} - -#[test] -fn relocate_enum64_signed() { - let kernel_version = KernelVersion::current().unwrap(); - if kernel_version < KernelVersion::new(6, 0, 0) { - eprintln!("skipping test on kernel {kernel_version:?}, support for enum64 was added in 6.0.0; see https://github.com/torvalds/linux/commit/6089fb3"); - return; - } - let test = RelocationTest { - local_definition: r#" - enum foo { D = -0xAAAAAAABBBBBBBB }; - "#, - target_btf: r#" - enum foo { D = -0xCCCCCCCDDDDDDDD } e1; - "#, - relocation_code: r#" - #define BPF_ENUMVAL_VALUE 1 - value = __builtin_preserve_enum_value(*(typeof(enum foo) *)D, BPF_ENUMVAL_VALUE); - "#, - } - .build() - .unwrap(); - assert_eq!(test.run().unwrap() as i64, -0xCCCCCCCDDDDDDDDi64); - assert_eq!(test.run_no_btf().unwrap() as i64, -0xAAAAAAABBBBBBBBi64); -} - -#[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 bar* ptr = (struct bar *) &memory; - value = (__u64) __builtin_preserve_access_index(ptr->f); - "#, - } - .build() - .unwrap(); - assert_eq!(test.run().unwrap(), 42); - assert_eq!(test.run_no_btf().unwrap(), 42); -} - -#[test] -fn relocate_struct_flavors() { - let definition = r#" - struct foo {}; - struct bar { struct foo *f; }; - struct bar___cafe { struct foo *e; struct foo *f; }; - "#; - - let relocation_code = r#" - __u8 memory[] = {42, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0}; - struct bar* ptr = (struct bar *) &memory; - - if (__builtin_preserve_field_info((((typeof(struct bar___cafe) *)0)->e), 2)) { - value = (__u64) __builtin_preserve_access_index(((struct bar___cafe *)ptr)->e); - } else { - value = (__u64) __builtin_preserve_access_index(ptr->f); +use test_case::test_case; + +use aya::{maps::Array, programs::UProbe, util::KernelVersion, BpfLoader, Btf, Endianness}; + +#[test_case("field", false, None, 2)] +#[test_case("field", true, None, 1)] +#[test_case("enum_unsigned_32", false, None, 0xAAAAAAAA)] +#[test_case("enum_unsigned_32", true, None, 0xBBBBBBBB)] +#[test_case("pointer", false, None, 42)] +#[test_case("pointer", true, None, 21)] +#[test_case("struct_flavors", false, None, 1)] +#[test_case("struct_flavors", true, None, 1)] +#[test_case("enum_signed_32", false, Some((KernelVersion::new(6, 0, 0), "https://github.com/torvalds/linux/commit/6089fb3")), -0x7AAAAAAAi32 as u64)] +#[test_case("enum_signed_32", true, Some((KernelVersion::new(6, 0, 0), "https://github.com/torvalds/linux/commit/6089fb3")), -0x7BBBBBBBi32 as u64)] +#[test_case("enum_unsigned_64", false, Some((KernelVersion::new(6, 0, 0), "https://github.com/torvalds/linux/commit/6089fb3")), 0xAAAAAAAABBBBBBBB)] +#[test_case("enum_unsigned_64", true, Some((KernelVersion::new(6, 0, 0), "https://github.com/torvalds/linux/commit/6089fb3")), 0xCCCCCCCCDDDDDDDD)] +#[test_case("enum_signed_64", false, Some((KernelVersion::new(6, 0, 0), "https://github.com/torvalds/linux/commit/6089fb3")), -0xAAAAAAABBBBBBBBi64 as u64)] +#[test_case("enum_signed_64", true, Some((KernelVersion::new(6, 0, 0), "https://github.com/torvalds/linux/commit/6089fb3")), -0xCCCCCCCDDDDDDDDi64 as u64)] +fn relocation_tests( + program: &str, + with_relocations: bool, + required_kernel_version: Option<(KernelVersion, &str)>, + expected: u64, +) { + if let Some((required_kernel_version, commit)) = required_kernel_version { + let current_kernel_version = KernelVersion::current().unwrap(); + if current_kernel_version < required_kernel_version { + eprintln!("skipping test on kernel {current_kernel_version:?}, support for {program} was added in {required_kernel_version:?}; see {commit}"); + return; } - "#; - - let test_no_flavor = RelocationTest { - local_definition: definition, - target_btf: definition, - relocation_code, - } - .build() - .unwrap(); - assert_eq!(test_no_flavor.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 { - 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> { - use std::io::Read as _; - - let Self { - local_definition, - relocation_code, - .. - } = self; - - let mut stdout = compile(&format!( - r#" - #include - - static long (*bpf_map_update_elem)(void *map, const void *key, const void *value, __u64 flags) = (void *) 2; - - {local_definition} - - struct {{ - int (*type)[BPF_MAP_TYPE_ARRAY]; - __u32 *key; - __u64 *value; - int (*max_entries)[1]; - }} output_map - __attribute__((section(".maps"), used)); - - __attribute__ ((noinline)) int bpf_func() {{ - __u32 key = 0; - __u64 value = 0; - {relocation_code} - bpf_map_update_elem(&output_map, &key, &value, BPF_ANY); - return 0; - }} - - __attribute__((section("tracepoint/bpf_prog"), used)) - int bpf_prog(void *ctx) {{ - bpf_func(); - return 0; - }} - - char _license[] __attribute__((section("license"), used)) = "GPL"; - "# - )) - .context("failed to compile eBPF program")?; - let mut output = Vec::new(); - stdout.read_to_end(&mut output)?; - Ok(output) - } - - /// - Generate the target BTF source with a mock main() - /// - Compile it with clang - /// - Extract the BTF with llvm-objcopy - fn build_btf(&self) -> Result { - use std::io::Read as _; - - let Self { - target_btf, - relocation_code, - .. - } = self; - - // 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 stdout = compile(&format!( - r#" - #include - - {target_btf} - int main() {{ - __u64 value = 0; - // 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")?; - - let mut cmd = Command::new("llvm-objcopy"); - cmd.args(["--dump-section", ".BTF=-", "-"]) - .stdin(stdout) - .stdout(Stdio::piped()); - let mut child = cmd - .spawn() - .with_context(|| format!("failed to spawn {cmd:?}"))?; - let Child { stdout, .. } = &mut child; - let mut stdout = stdout.take().ok_or(anyhow!("failed to open stdout"))?; - let status = child - .wait() - .with_context(|| format!("failed to wait for {cmd:?}"))?; - match status.code() { - Some(code) => match code { - 0 => {} - code => bail!("{cmd:?} exited with code {code}"), - }, - None => bail!("{cmd:?} terminated by signal"), - } - - let mut output = Vec::new(); - stdout.read_to_end(&mut output)?; - - Btf::parse(output.as_slice(), Endianness::default()) - .context("failed to parse generated BTF") - } -} - -/// Compile an eBPF program and return its bytes. -fn compile(source_code: &str) -> Result { - use std::io::Write as _; - - let mut cmd = Command::new("clang"); - cmd.args([ - "-c", "-g", "-O2", "-target", "bpf", "-x", "c", "-", "-o", "-", - ]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()); - let mut child = cmd - .spawn() - .with_context(|| format!("failed to spawn {cmd:?}"))?; - let Child { stdin, stdout, .. } = &mut child; - { - let mut stdin = stdin.take().ok_or(anyhow!("failed to open stdin"))?; - stdin - .write_all(source_code.as_bytes()) - .context("failed to write to stdin")?; - } - let stdout = stdout.take().ok_or(anyhow!("failed to open stdout"))?; - let status = child - .wait() - .with_context(|| format!("failed to wait for {cmd:?}"))?; - match status.code() { - Some(code) => match code { - 0 => {} - code => bail!("{cmd:?} exited with code {code}"), - }, - None => bail!("{cmd:?} terminated by signal"), - } - Ok(stdout) -} - -struct RelocationTestRunner { - ebpf: Vec, - btf: Btf, -} - -impl RelocationTestRunner { - /// Run test and return the output value - fn run(&self) -> Result { - self.run_internal(true).context("Error running with BTF") - } - - /// Run without loading btf - fn run_no_btf(&self) -> Result { - self.run_internal(false) - .context("Error running without BTF") - } - - fn run_internal(&self, with_relocations: bool) -> Result { - 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<_, u64> = bpf.take_map("output_map").unwrap().try_into().unwrap(); - let key = 0; - output_map.get(&key, 0).context("Getting key 0 failed") } + let mut bpf = BpfLoader::new() + .btf( + with_relocations + .then(|| Btf::parse(crate::RELOC_BTF, Endianness::default()).unwrap()) + .as_ref(), + ) + .load(crate::RELOC_BPF) + .unwrap(); + let program: &mut UProbe = bpf.program_mut(program).unwrap().try_into().unwrap(); + program.load().unwrap(); + program + .attach( + Some("trigger_btf_relocations_program"), + 0, + "/proc/self/exe", + None, + ) + .unwrap(); + + trigger_btf_relocations_program(); + + let output_map: Array<_, u64> = bpf.take_map("output_map").unwrap().try_into().unwrap(); + let key = 0; + assert_eq!(output_map.get(&key, 0).unwrap(), expected) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn trigger_btf_relocations_program() { + core::hint::black_box(trigger_btf_relocations_program); } diff --git a/test/run.sh b/test/run.sh index 998dd572..4cbd48b1 100755 --- a/test/run.sh +++ b/test/run.sh @@ -191,11 +191,7 @@ EOF exec_vm sudo dnf config-manager --set-enabled updates-testing exec_vm sudo dnf config-manager --set-enabled updates-testing-modular echo "Installing dependencies" - exec_vm sudo dnf install -qy bpftool llvm llvm-devel clang clang-devel zlib-devel git - exec_vm 'curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- \ - -y --profile minimal --default-toolchain nightly --component rust-src --component clippy' - exec_vm 'echo source ~/.cargo/env >> ~/.bashrc' - exec_vm cargo install bpf-linker --git https://github.com/aya-rs/bpf-linker.git + exec_vm sudo dnf install -qy bpftool } scp_vm() { @@ -237,12 +233,11 @@ start_vm trap cleanup_vm EXIT # make sure we always use fresh sources (also see comment at the end) -exec_vm "rm -rf aya/*" -rsync_vm "--exclude=target --exclude=.tmp $AYA_SOURCE_DIR" +rsync_vm "$*" -exec_vm "cd aya; cargo xtask integration-test $*" +exec_vm "find $* -type f -executable -print0 | xargs -0 -I {} sudo {} --test-threads=1" # we rm and sync but it doesn't seem to work reliably - I guess we could sleep a # few seconds after but ain't nobody got time for that. Instead we also rm # before rsyncing. -exec_vm "rm -rf aya/*; sync" +exec_vm "rm -rf $*; sync"