Skip to content

Commit 107cc32

Browse files
committed
feat: Detect eBPF features without relying on kernel version checks
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.
1 parent 45cc3d3 commit 107cc32

File tree

21 files changed

+591
-289
lines changed

21 files changed

+591
-289
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bpf-builder/include/compatibility.bpf.h

Lines changed: 0 additions & 55 deletions
This file was deleted.

crates/bpf-builder/include/output.bpf.h

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
#include "bpf/bpf_helpers.h"
77
#include "buffer.bpf.h"
8-
#include "compatibility.bpf.h"
98
#include "interest_tracking.bpf.h"
109

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

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

168-
// Decremnet value by one
167+
// Decrement value by one
169168
static __always_inline u64 sync_decrement(u64 *value) {
170-
if (HAVE_FEATURE_ATOMICS) {
171-
return __sync_fetch_and_sub(value, 1);
172-
} else {
173-
u64 old_value = *value;
174-
*value -= 1;
175-
return old_value;
176-
}
169+
#ifdef FEATURE_ATOMICS
170+
return __sync_fetch_and_sub(value, 1);
171+
#else
172+
u64 old_value = *value;
173+
*value -= 1;
174+
return old_value;
175+
#endif
177176
}

crates/bpf-builder/include/task.bpf.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/* SPDX-License-Identifier: GPL-2.0-only */
2+
3+
#pragma once
4+
5+
#include "common.bpf.h"
6+
7+
static __always_inline struct task_struct *get_current_task() {
8+
#ifdef FEATURE_TASK_BTF
9+
return bpf_get_current_task_btf();
10+
#else
11+
return (struct task_struct*)bpf_get_current_task();
12+
#endif
13+
}

crates/bpf-builder/src/lib.rs

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
use std::{env, path::PathBuf, process::Command, string::String};
1+
use std::{
2+
env,
3+
path::{Path, PathBuf},
4+
process::Command,
5+
string::String,
6+
};
27

38
use anyhow::{bail, Context};
49

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

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

24-
let out_file = PathBuf::from(env::var("OUT_DIR")?).join(name);
27+
let features = [
28+
("FEATURE_ATOMICS", "a"),
29+
("FEATURE_CGROUP_TASK_BTF", "c"),
30+
("FEATURE_FN_POINTERS", "f"),
31+
];
2532

26-
compile(
27-
source,
28-
out_file.with_extension("5_13.bpf.o"),
29-
&["-DVERSION_5_13"],
30-
)
31-
.context("Error compiling 5.13 version")?;
32-
compile(source, out_file.with_extension("5_5.bpf.o"), &[])
33-
.context("Error compiling 5.5 version")?;
33+
let out_dir = env::var("OUT_DIR").context("OUT_DIR not set")?;
34+
let out_path = Path::new(&out_dir).join(name);
35+
36+
for bits in 0..(1 << features.len()) {
37+
let mut feature_args = Vec::new();
38+
let mut suffix = String::new();
39+
40+
for (i, (feature, code)) in features.iter().enumerate() {
41+
if bits & (1 << i) != 0 {
42+
feature_args.push(format!("-D{}", feature));
43+
suffix.push_str(code);
44+
}
45+
}
46+
47+
if suffix.is_empty() {
48+
suffix.push_str("none");
49+
}
50+
51+
let filename = format!("{}.{}.bpf.o", name, suffix);
52+
let full_path = out_path.with_file_name(filename);
53+
compile(source, full_path, &feature_args)
54+
.context("Error compiling programs with features: {feature_args:?}")?;
55+
}
3456

3557
Ok(())
3658
}
3759

38-
fn compile(probe: &str, out_object: PathBuf, extra_args: &[&str]) -> anyhow::Result<()> {
60+
fn compile(probe: &str, out_object: PathBuf, extra_args: &[String]) -> anyhow::Result<()> {
3961
let clang = env::var("CLANG").unwrap_or_else(|_| String::from(CLANG_DEFAULT));
4062
let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap();
4163
let include_path = PathBuf::from(INCLUDE_PATH);

crates/bpf-common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ sqlite3-vendored = ["libsqlite3-sys/bundled"]
1313

1414
[dependencies]
1515
aya = { workspace = true, features = ["async_tokio"] }
16+
aya-ebpf-bindings = { workspace = true }
1617
aya-obj = { workspace = true }
1718
bytes = { workspace = true }
1819
thiserror = { workspace = true }
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use aya_ebpf_bindings::bindings::{BPF_DW, BPF_JEQ, BPF_REG_0, BPF_REG_1, BPF_REG_10, BPF_XCHG};
2+
use aya_obj::generated::bpf_insn;
3+
use log::warn;
4+
5+
use super::{load_program, BpfProgType};
6+
use crate::insn;
7+
8+
/// eBPF program bytecode with simple atomic operations.
9+
fn bpf_prog_atomic() -> Vec<bpf_insn> {
10+
vec![
11+
// val = 3;
12+
insn::bpf_st_mem(BPF_DW as u8, BPF_REG_10 as u8, -8, 3),
13+
// old = atomic_xchg(&val, 4);
14+
insn::bpf_mov64_imm(BPF_REG_1 as u8, 4),
15+
insn::bpf_atomic_op(
16+
BPF_DW as u8,
17+
BPF_XCHG,
18+
BPF_REG_10 as u8,
19+
BPF_REG_1 as u8,
20+
-8,
21+
),
22+
// if (old != 3) exit(1);
23+
insn::bpf_jmp_imm(BPF_JEQ as u8, BPF_REG_1 as u8, 3, 2),
24+
insn::bpf_mov64_imm(BPF_REG_0 as u8, 1),
25+
insn::bpf_exit_insn(),
26+
// if (val != 4) exit(2);
27+
insn::bpf_ldx_mem(BPF_DW as u8, BPF_REG_0 as u8, BPF_REG_10 as u8, -8),
28+
insn::bpf_jmp_imm(BPF_JEQ as u8, BPF_REG_0 as u8, 4, 2),
29+
insn::bpf_mov64_imm(BPF_REG_0 as u8, 2),
30+
insn::bpf_exit_insn(),
31+
// exit(0);
32+
insn::bpf_mov64_imm(BPF_REG_0 as u8, 0),
33+
insn::bpf_exit_insn(),
34+
]
35+
}
36+
37+
/// Checks whether the current kernel supports atomic operations in eBPF.
38+
pub fn atomics_supported() -> bool {
39+
let insns = bpf_prog_atomic();
40+
// Program type doesn't matter, kprobe is just the most basic one.
41+
let res = load_program(BpfProgType::BPF_PROG_TYPE_KPROBE, insns);
42+
match res {
43+
Ok(_) => true,
44+
Err(e) => {
45+
warn!("Atomic operations in eBPF are not supported by the kernel: {e}");
46+
false
47+
}
48+
}
49+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use aya_ebpf_bindings::bindings::BPF_REG_0;
2+
use aya_obj::generated::bpf_insn;
3+
use log::warn;
4+
5+
use super::{load_program, BpfProgType};
6+
use crate::insn;
7+
8+
/// eBPF program bytecode with a simple function call (with `bpf_emit_call`
9+
/// instruction) to the function with the given ID.
10+
fn bpf_prog_func_id(func_id: u32) -> Vec<bpf_insn> {
11+
vec![
12+
insn::bpf_emit_call(func_id),
13+
insn::bpf_mov64_imm(BPF_REG_0 as u8, 0),
14+
insn::bpf_exit_insn(),
15+
]
16+
}
17+
18+
/// Checks whether the provided `func_id` for the given `prog_type` is
19+
/// supported by the current kernel, by loading a minimal program trying to use
20+
/// it.
21+
///
22+
/// Similar checks are performed by [`bpftool`].
23+
///
24+
/// [`bpftool`]: https://github.com/torvalds/linux/blob/v6.8/tools/bpf/bpftool/feature.c#L534-L544
25+
pub fn func_id_supported(func_id: u32, prog_type: BpfProgType) -> bool {
26+
let insns = bpf_prog_func_id(func_id);
27+
let res = load_program(prog_type, insns);
28+
match res {
29+
Ok(_) => true,
30+
Err(err) => {
31+
warn!("Function {func_id} in program type {prog_type:?} not supported: {err}");
32+
false
33+
}
34+
}
35+
}
Lines changed: 20 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
1-
use anyhow::{anyhow, Context, Result};
2-
use aya::{include_bytes_aligned, programs::Lsm, BpfLoader, Btf};
1+
// use anyhow::{anyhow, Context, Result};
2+
// use aya::{include_bytes_aligned, programs::Lsm, BpfLoader, Btf};
3+
use aya_ebpf_bindings::bindings::BPF_REG_0;
4+
use aya_obj::generated::bpf_insn;
5+
use log::warn;
6+
7+
use super::{load_program, BpfProgType};
8+
use crate::insn;
9+
10+
/// Minimal eBPF LSM program bytecode.
11+
fn bpf_prog_lsm() -> Vec<bpf_insn> {
12+
vec![
13+
insn::bpf_mov64_imm(BPF_REG_0 as u8, 0),
14+
insn::bpf_exit_insn(),
15+
]
16+
}
317

418
/// Check if the system supports eBPF LSM programs.
519
/// The kernel must be build with CONFIG_BPF_LSM=y, which is available
@@ -12,44 +26,12 @@ use aya::{include_bytes_aligned, programs::Lsm, BpfLoader, Btf};
1226
///
1327
/// NOTE: this function is blocking.
1428
pub fn lsm_supported() -> bool {
15-
match try_load() {
16-
Ok(()) => true,
29+
let insns = bpf_prog_lsm();
30+
match load_program(BpfProgType::BPF_PROG_TYPE_LSM, insns) {
31+
Ok(_) => true,
1732
Err(err) => {
18-
if log::log_enabled!(log::Level::Debug) {
19-
log::warn!("LSM not supported: {err:?}");
20-
} else {
21-
log::warn!("LSM not supported: {err}");
22-
}
23-
33+
warn!("LSM not supported: {err}");
2434
false
2535
}
2636
}
2737
}
28-
29-
const PATH: &str = "/sys/kernel/security/lsm";
30-
static TEST_LSM_PROBE: &[u8] =
31-
include_bytes_aligned!(concat!(env!("OUT_DIR"), "/test_lsm.5_13.bpf.o"));
32-
33-
fn try_load() -> Result<()> {
34-
// Check if LSM enabled
35-
std::fs::read_to_string(PATH)
36-
.with_context(|| format!("Reading {PATH} failed"))?
37-
.split(',')
38-
.any(|lsm_subsystem| lsm_subsystem == "bpf")
39-
.then_some(())
40-
.ok_or_else(|| anyhow!("eBPF LSM programs disabled"))?;
41-
42-
// Check if we can load a program
43-
let mut bpf = BpfLoader::new()
44-
.load(TEST_LSM_PROBE)
45-
.context("LSM enabled, but initial loading failed")?;
46-
let program: &mut Lsm = bpf
47-
.program_mut("socket_bind")
48-
.context("LSM program not found")?
49-
.try_into()
50-
.context("LSM program of the wrong type")?;
51-
let btf = Btf::from_sys_fs().context("Loading Btf failed")?;
52-
program.load("socket_bind", &btf).context("Load failed")?;
53-
program.attach().context("Attach failed")?;
54-
Ok(())
55-
}

0 commit comments

Comments
 (0)