Skip to content

Commit

Permalink
feat: Detect eBPF features without relying on kernel version checks
Browse files Browse the repository at this point in the history
Kernel version checks are unreliable:

* Features differ across architectures.
* Linux distros tend to backport features to their stable/LTS kernels.

Instead of relying on version, use small probes (written in eBPF
assembly) to figure out what features are available.
  • Loading branch information
vadorovsky committed May 10, 2024
1 parent 45cc3d3 commit 8a082d9
Show file tree
Hide file tree
Showing 20 changed files with 555 additions and 252 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

55 changes: 0 additions & 55 deletions crates/bpf-builder/include/compatibility.bpf.h

This file was deleted.

41 changes: 20 additions & 21 deletions crates/bpf-builder/include/output.bpf.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

#include "bpf/bpf_helpers.h"
#include "buffer.bpf.h"
#include "compatibility.bpf.h"
#include "interest_tracking.bpf.h"

// eBPF programs could interrupt each other, see "Are BPF programs preemptible?"
Expand Down Expand Up @@ -151,27 +150,27 @@ static __always_inline int is_initialized() {

// Get a value and increment it by one
static __always_inline u64 sync_increment(u64 *value) {
if (HAVE_FEATURE_ATOMICS) {
return __sync_fetch_and_add(value, 1);
} else {
// If we miss atomic operations, it still shouldn't cause problems:
// - the nesting levels are kept on a PERCPU array
// - even if an eBPF program interrupts this, the nesting level
// will be left in a consistent state as all eBPF programs will
// reset the counter to its previous value before exiting.
u64 old_value = *value;
*value += 1;
return old_value;
}
#ifdef FEATURE_ATOMICS
return __sync_fetch_and_add(value, 1);
#else
// If we miss atomic operations, it still shouldn't cause problems:
// - the nesting levels are kept on a PERCPU array
// - even if an eBPF program interrupts this, the nesting level
// will be left in a consistent state as all eBPF programs will
// reset the counter to its previous value before exiting.
u64 old_value = *value;
*value += 1;
return old_value;
#endif
}

// Decremnet value by one
// Decrement value by one
static __always_inline u64 sync_decrement(u64 *value) {
if (HAVE_FEATURE_ATOMICS) {
return __sync_fetch_and_sub(value, 1);
} else {
u64 old_value = *value;
*value -= 1;
return old_value;
}
#ifdef FEATURE_ATOMICS
return __sync_fetch_and_sub(value, 1);
#else
u64 old_value = *value;
*value -= 1;
return old_value;
#endif
}
13 changes: 13 additions & 0 deletions crates/bpf-builder/include/task.bpf.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* SPDX-License-Identifier: GPL-2.0-only */

#pragma once

#include "common.bpf.h"

static __always_inline struct task_struct *get_current_task() {
#ifdef FEATURE_TASK_BTF
return bpf_get_current_task_btf();
#else
return (struct task_struct*)bpf_get_current_task();
#endif
}
54 changes: 38 additions & 16 deletions crates/bpf-builder/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
use std::{env, path::PathBuf, process::Command, string::String};
use std::{
env,
path::{Path, PathBuf},
process::Command,
string::String,
};

use anyhow::{bail, Context};

Expand All @@ -7,10 +12,8 @@ static LLVM_STRIP: &str = "llvm-strip";
static INCLUDE_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/include");

// Given a probe name and the eBPF program source code path, compile it to OUT_DIR.
// We'll build multiple versions:
// - `${OUT_DIR}/{name}.5_13.bpf.o`: will contain the full version
// - `${OUT_DIR}/{name}.5_5.bpf.o`: will contain a version with the FEATURE_5_5 constant
// defined. This version should be loaded on kernel < 5.13, see ../include/compatibility.bpf.h
// We'll build multiple versions with all combinations of eBPF features we rely
// on. Lack of certain feature will result in using legacy replacements.
pub fn build(name: &str, source: &str) -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rerun-if-changed={source}");
println!("cargo:rerun-if-changed={INCLUDE_PATH}/common.bpf.h");
Expand All @@ -19,23 +22,42 @@ pub fn build(name: &str, source: &str) -> Result<(), Box<dyn std::error::Error>>
println!("cargo:rerun-if-changed={INCLUDE_PATH}/interest_tracking.bpf.h");
println!("cargo:rerun-if-changed={INCLUDE_PATH}/loop.bpf.h");
println!("cargo:rerun-if-changed={INCLUDE_PATH}/get_path.bpf.h");
println!("cargo:rerun-if-changed={INCLUDE_PATH}/compatibility.bpf.h");
println!("cargo:rerun-if-changed={INCLUDE_PATH}/task.bpf.h");

let out_file = PathBuf::from(env::var("OUT_DIR")?).join(name);
let features = [
("FEATURE_ATOMICS", "a"),
("FEATURE_CGROUP_TASK_BTF", "c"),
("FEATURE_FN_POINTERS", "f"),
];

compile(
source,
out_file.with_extension("5_13.bpf.o"),
&["-DVERSION_5_13"],
)
.context("Error compiling 5.13 version")?;
compile(source, out_file.with_extension("5_5.bpf.o"), &[])
.context("Error compiling 5.5 version")?;
let out_dir = env::var("OUT_DIR").context("OUT_DIR not set")?;
let out_path = Path::new(&out_dir).join(name);

for bits in 0..(1 << features.len()) {
let mut feature_args = Vec::new();
let mut suffix = String::new();

for (i, (feature, code)) in features.iter().enumerate() {
if bits & (1 << i) != 0 {
feature_args.push(format!("-D{}", feature));
suffix.push_str(code);
}
}

if suffix.is_empty() {
suffix.push_str("none");
}

let filename = format!("{}.{}.bpf.o", name, suffix);
let full_path = out_path.with_file_name(filename);
compile(source, full_path, &feature_args)
.context("Error compiling programs with features: {feature_args:?}")?;
}

Ok(())
}

fn compile(probe: &str, out_object: PathBuf, extra_args: &[&str]) -> anyhow::Result<()> {
fn compile(probe: &str, out_object: PathBuf, extra_args: &[String]) -> anyhow::Result<()> {
let clang = env::var("CLANG").unwrap_or_else(|_| String::from(CLANG_DEFAULT));
let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
let include_path = PathBuf::from(INCLUDE_PATH);
Expand Down
1 change: 1 addition & 0 deletions crates/bpf-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ sqlite3-vendored = ["libsqlite3-sys/bundled"]

[dependencies]
aya = { workspace = true, features = ["async_tokio"] }
aya-ebpf-bindings = { workspace = true }
aya-obj = { workspace = true }
bytes = { workspace = true }
thiserror = { workspace = true }
Expand Down
49 changes: 49 additions & 0 deletions crates/bpf-common/src/feature_autodetect/atomic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use aya_ebpf_bindings::bindings::{BPF_DW, BPF_JEQ, BPF_REG_0, BPF_REG_1, BPF_REG_10, BPF_XCHG};
use aya_obj::generated::bpf_insn;
use log::warn;

use super::{load_program, BpfProgType};
use crate::insn;

/// eBPF program bytecode with simple atomic operations.
fn bpf_prog_atomic() -> Vec<bpf_insn> {
vec![
// val = 3;
insn::bpf_st_mem(BPF_DW as u8, BPF_REG_10 as u8, -8, 3),
// old = atomic_xchg(&val, 4);
insn::bpf_mov64_imm(BPF_REG_1 as u8, 4),
insn::bpf_atomic_op(
BPF_DW as u8,
BPF_XCHG,
BPF_REG_10 as u8,
BPF_REG_1 as u8,
-8,
),
// if (old != 3) exit(1);
insn::bpf_jmp_imm(BPF_JEQ as u8, BPF_REG_1 as u8, 3, 2),
insn::bpf_mov64_imm(BPF_REG_0 as u8, 1),
insn::bpf_exit_insn(),
// if (val != 4) exit(2);
insn::bpf_ldx_mem(BPF_DW as u8, BPF_REG_0 as u8, BPF_REG_10 as u8, -8),
insn::bpf_jmp_imm(BPF_JEQ as u8, BPF_REG_0 as u8, 4, 2),
insn::bpf_mov64_imm(BPF_REG_0 as u8, 2),
insn::bpf_exit_insn(),
// exit(0);
insn::bpf_mov64_imm(BPF_REG_0 as u8, 0),
insn::bpf_exit_insn(),
]
}

/// Checks whether the current kernel supports atomic operations in eBPF.
pub fn atomics_supported() -> bool {
let insns = bpf_prog_atomic();
// Program type doesn't matter, kprobe is just the most basic one.
let res = load_program(BpfProgType::BPF_PROG_TYPE_KPROBE, insns);
match res {
Ok(_) => true,
Err(e) => {
warn!("Atomic operations in eBPF are not supported by the kernel: {e}");
false
}
}
}
35 changes: 35 additions & 0 deletions crates/bpf-common/src/feature_autodetect/func.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use aya_ebpf_bindings::bindings::BPF_REG_0;
use aya_obj::generated::bpf_insn;
use log::warn;

use super::{load_program, BpfProgType};
use crate::insn;

/// eBPF program bytecode with a simple function call (with `bpf_emit_call`
/// instruction) to the function with the given ID.
fn bpf_prog_func_id(func_id: u32) -> Vec<bpf_insn> {
vec![
insn::bpf_emit_call(func_id),
insn::bpf_mov64_imm(BPF_REG_0 as u8, 0),
insn::bpf_exit_insn(),
]
}

/// Checks whether the provided `func_id` for the given `prog_type` is
/// supported by the current kernel, by loading a minimal program trying to use
/// it.
///
/// Similar checks are performed by [`bpftool`].
///
/// [`bpftool`]: https://github.com/torvalds/linux/blob/v6.8/tools/bpf/bpftool/feature.c#L534-L544
pub fn func_id_supported(func_id: u32, prog_type: BpfProgType) -> bool {
let insns = bpf_prog_func_id(func_id);
let res = load_program(prog_type, insns);
match res {
Ok(_) => true,
Err(err) => {
warn!("Function {func_id} in program type {prog_type:?} not supported: {err}");
false
}
}
}
2 changes: 1 addition & 1 deletion crates/bpf-common/src/feature_autodetect/lsm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub fn lsm_supported() -> bool {

const PATH: &str = "/sys/kernel/security/lsm";
static TEST_LSM_PROBE: &[u8] =
include_bytes_aligned!(concat!(env!("OUT_DIR"), "/test_lsm.5_13.bpf.o"));
include_bytes_aligned!(concat!(env!("OUT_DIR"), "/test_lsm.none.bpf.o"));

fn try_load() -> Result<()> {
// Check if LSM enabled
Expand Down
Loading

0 comments on commit 8a082d9

Please sign in to comment.