Browse Source

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.
Tamir Duberstein 1 year ago
parent
commit
dca5e6c167

+ 69 - 16
.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 <asm/types.h> 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

+ 2 - 1
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 }

+ 3 - 1
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

+ 1 - 0
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",

+ 106 - 0
test/integration-test/bpf/reloc.bpf.c

@@ -0,0 +1,106 @@
+// clang-format off
+#include <linux/bpf.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_core_read.h>
+// 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();
+}

+ 77 - 0
test/integration-test/bpf/reloc.btf.c

@@ -0,0 +1,77 @@
+// clang-format off
+#include <linux/bpf.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_core_read.h>
+// clang-format on
+
+#include <stdlib.h>
+
+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();
+}

+ 54 - 6
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}"));
         }
 

+ 2 - 0
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"));
 

+ 59 - 402
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<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>> {
-        use std::io::Read as _;
-
-        let Self {
-            local_definition,
-            relocation_code,
-            ..
-        } = self;
-
-        let mut stdout = compile(&format!(
-            r#"
-                #include <linux/bpf.h>
-
-                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<Btf> {
-        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 <linux/bpf.h>
-
-                {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<ChildStdout> {
-    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<u8>,
-    btf: Btf,
-}
-
-impl RelocationTestRunner {
-    /// Run test and return the output value
-    fn run(&self) -> Result<u64> {
-        self.run_internal(true).context("Error running with BTF")
-    }
-
-    /// Run without loading btf
-    fn run_no_btf(&self) -> Result<u64> {
-        self.run_internal(false)
-            .context("Error running without BTF")
-    }
-
-    fn run_internal(&self, with_relocations: bool) -> Result<u64> {
-        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);
 }

+ 4 - 9
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"