Skip to content

Commit e742286

Browse files
fix(ebpf): harden ai agent exec lsm enforcement
1 parent e390ccd commit e742286

6 files changed

Lines changed: 201 additions & 24 deletions

File tree

agent/src/common/kernel_capability.rs

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::{
2-
fs,
2+
env, fs,
33
io::{Cursor, Read},
4-
path::Path,
4+
path::{Path, PathBuf},
55
};
66

77
use flate2::read::GzDecoder;
@@ -17,18 +17,28 @@ pub struct KernelCapability {
1717

1818
impl KernelCapability {
1919
pub fn detect() -> Self {
20-
let lsm_text = fs::read_to_string("/sys/kernel/security/lsm").unwrap_or_default();
21-
let config_text = read_kernel_config().unwrap_or_default();
20+
let proc_root = path_from_env("PROCFS_ROOT", "/proc");
21+
let sys_root = path_from_env("SYSFS_ROOT", "/sys");
22+
let boot_root =
23+
host_sibling_root(&proc_root, "boot").unwrap_or_else(|| PathBuf::from("/boot"));
24+
25+
Self::detect_from_roots(&proc_root, &sys_root, &boot_root)
26+
}
27+
28+
pub fn detect_from_roots(proc_root: &Path, sys_root: &Path, boot_root: &Path) -> Self {
29+
let lsm_text = fs::read_to_string(sys_root.join("kernel/security/lsm")).unwrap_or_default();
30+
let config_text = read_kernel_config_from_roots(proc_root, boot_root).unwrap_or_default();
31+
let bpf_lsm_active = lsm_has_bpf(&lsm_text);
2232

2333
Self {
24-
bpf_lsm_configured: config_enabled(&config_text, "CONFIG_BPF_LSM"),
25-
bpf_lsm_active: lsm_has_bpf(&lsm_text),
34+
bpf_lsm_configured: config_enabled(&config_text, "CONFIG_BPF_LSM") || bpf_lsm_active,
35+
bpf_lsm_active,
2636
bpf_kprobe_override_configured: config_enabled(
2737
&config_text,
2838
"CONFIG_BPF_KPROBE_OVERRIDE",
2939
),
3040
seccomp_filter_configured: config_enabled(&config_text, "CONFIG_SECCOMP_FILTER"),
31-
btf_vmlinux_available: Path::new("/sys/kernel/btf/vmlinux").exists(),
41+
btf_vmlinux_available: sys_root.join("kernel/btf/vmlinux").exists(),
3242
}
3343
}
3444

@@ -37,6 +47,18 @@ impl KernelCapability {
3747
}
3848
}
3949

50+
fn path_from_env(name: &str, default: &str) -> PathBuf {
51+
env::var_os(name)
52+
.filter(|value| !value.is_empty())
53+
.map(PathBuf::from)
54+
.unwrap_or_else(|| PathBuf::from(default))
55+
}
56+
57+
fn host_sibling_root(proc_root: &Path, sibling: &str) -> Option<PathBuf> {
58+
let parent = proc_root.parent()?;
59+
Some(parent.join(sibling))
60+
}
61+
4062
fn lsm_has_bpf(lsm_text: &str) -> bool {
4163
lsm_text
4264
.trim()
@@ -54,20 +76,24 @@ fn config_enabled(config_text: &str, option: &str) -> bool {
5476
}
5577

5678
fn read_kernel_config() -> Option<String> {
57-
if let Some(config) = read_boot_kernel_config() {
79+
read_kernel_config_from_roots(Path::new("/proc"), Path::new("/boot"))
80+
}
81+
82+
fn read_kernel_config_from_roots(proc_root: &Path, boot_root: &Path) -> Option<String> {
83+
if let Some(config) = read_boot_kernel_config(proc_root, boot_root) {
5884
return Some(config);
5985
}
60-
read_proc_kernel_config()
86+
read_proc_kernel_config(proc_root)
6187
}
6288

63-
fn read_boot_kernel_config() -> Option<String> {
64-
let release = fs::read_to_string("/proc/sys/kernel/osrelease").ok()?;
65-
let path = format!("/boot/config-{}", release.trim());
89+
fn read_boot_kernel_config(proc_root: &Path, boot_root: &Path) -> Option<String> {
90+
let release = fs::read_to_string(proc_root.join("sys/kernel/osrelease")).ok()?;
91+
let path = boot_root.join(format!("config-{}", release.trim()));
6692
fs::read_to_string(path).ok()
6793
}
6894

69-
fn read_proc_kernel_config() -> Option<String> {
70-
let compressed = fs::read("/proc/config.gz").ok()?;
95+
fn read_proc_kernel_config(proc_root: &Path) -> Option<String> {
96+
let compressed = fs::read(proc_root.join("config.gz")).ok()?;
7197
decode_gzip(&compressed).ok()
7298
}
7399

@@ -113,4 +139,40 @@ mod tests {
113139
}
114140
.supports_exec_lsm_enforcement());
115141
}
142+
143+
#[test]
144+
fn detect_from_roots_reads_host_sysfs_lsm_in_container() {
145+
let root = make_temp_root("host-sysfs-lsm");
146+
let proc_root = root.join("host-proc");
147+
let sys_root = root.join("host-sys");
148+
let boot_root = root.join("boot");
149+
fs::create_dir_all(sys_root.join("kernel/security")).unwrap();
150+
fs::create_dir_all(sys_root.join("kernel/btf")).unwrap();
151+
fs::create_dir_all(proc_root.join("sys/kernel")).unwrap();
152+
fs::write(
153+
sys_root.join("kernel/security/lsm"),
154+
"capability,yama,selinux,bpf",
155+
)
156+
.unwrap();
157+
fs::write(sys_root.join("kernel/btf/vmlinux"), b"btf").unwrap();
158+
fs::write(proc_root.join("sys/kernel/osrelease"), "4.18.0-test\n").unwrap();
159+
160+
let capability = KernelCapability::detect_from_roots(&proc_root, &sys_root, &boot_root);
161+
162+
assert!(capability.bpf_lsm_active);
163+
assert!(capability.bpf_lsm_configured);
164+
assert!(capability.btf_vmlinux_available);
165+
166+
let _ = fs::remove_dir_all(root);
167+
}
168+
169+
fn make_temp_root(name: &str) -> std::path::PathBuf {
170+
let root = std::env::temp_dir().join(format!(
171+
"deepflow-kernel-capability-{name}-{}",
172+
std::process::id()
173+
));
174+
let _ = fs::remove_dir_all(&root);
175+
fs::create_dir_all(&root).unwrap();
176+
root
177+
}
116178
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Lightweight manual harness for AI Agent exec enforcement verification.
5+
# It intentionally does not start or reconfigure deepflow-agent; effective policy
6+
# must be delivered by DeepFlow server/controller agent-group configuration.
7+
8+
SYSFS_ROOT="${SYSFS_ROOT:-/sys}"
9+
LSM_FILE="${SYSFS_ROOT%/}/kernel/security/lsm"
10+
BLOCKED_CMD="${BLOCKED_CMD:-/usr/bin/uname}"
11+
SERVER_NODE="${SERVER_NODE:-10.50.120.81}"
12+
AGENT_NODE="${AGENT_NODE:-10.50.120.21}"
13+
NAMESPACE="${NAMESPACE:-deepflow}"
14+
AGENT_DS="${AGENT_DS:-deepflow-agent-r4-dcn-ctrl}"
15+
16+
if [[ ! -r "$LSM_FILE" ]]; then
17+
echo "SKIP: cannot read $LSM_FILE"
18+
exit 0
19+
fi
20+
21+
if ! tr ',' '\n' <"$LSM_FILE" | grep -qx bpf; then
22+
echo "SKIP: BPF LSM is not active in $LSM_FILE"
23+
exit 0
24+
fi
25+
26+
if [[ ! -x "$BLOCKED_CMD" ]]; then
27+
echo "SKIP: blocked command $BLOCKED_CMD is not executable on this host"
28+
exit 0
29+
fi
30+
31+
echo "OK: BPF LSM active and $BLOCKED_CMD exists."
32+
cat <<EOF
33+
34+
Manual K8s verification checklist:
35+
36+
1. Configure the rule through DeepFlow server/controller agent-group config,
37+
not only through the Kubernetes ConfigMap:
38+
inputs.proc.ai_agent.enforcement.enabled: true
39+
inputs.proc.ai_agent.enforcement.mode: block
40+
exact rule path: $BLOCKED_CMD
41+
42+
2. Refresh controller vtap cache, then check the target DaemonSet:
43+
ssh root@$SERVER_NODE 'kubectl -n $NAMESPACE get ds $AGENT_DS -o wide'
44+
45+
3. Confirm agent logs show both capability and LSM attach success:
46+
ssh root@$AGENT_NODE \\
47+
'sudo crictl ps --name deepflow-agent && sudo crictl logs <container-id> 2>&1 | grep -Ei "KernelCapability|bpf_lsm|attach lsm" | tail -50'
48+
49+
4. Trigger an AI endpoint hit from the same process that later executes
50+
$BLOCKED_CMD. Expected result in block mode:
51+
PermissionError errno=1
52+
53+
5. If event persistence is being validated, deploy a server/schema version that
54+
contains event.proc_block_event before querying ClickHouse.
55+
EOF

agent/src/ebpf/test/test_ai_agent_source_contracts.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ def read_source(path: Path) -> str:
137137
"BPF_PROG_TYPE_LSM" in load_text,
138138
"load.c must map lsm/ programs to BPF_PROG_TYPE_LSM",
139139
)
140+
require(
141+
"prog_load_name" in load_text
142+
and "prog->type == BPF_PROG_TYPE_LSM" in load_text
143+
and '"lsm__%s"' in load_text,
144+
"load.c must pass lsm__<hook> to BCC so it sets BPF_LSM_MAC and lets libbpf find bpf_lsm_<hook>",
145+
)
140146
require(
141147
"program__attach_lsm" in probe_text,
142148
"probe.c must provide an LSM attach helper",
@@ -145,6 +151,10 @@ def read_source(path: Path) -> str:
145151
"bpf_raw_tracepoint_open" in probe_text,
146152
"LSM attach helper must use the raw tracepoint attach syscall path",
147153
)
154+
require(
155+
"bpf_raw_tracepoint_open(NULL, ebpf_prog->prog_fd)" in probe_text,
156+
"LSM attach helper must attach by loaded attach_btf_id, not by raw tracepoint hook name",
157+
)
148158
require(
149159
"struct lsm_prog" in tracer_h_text and "lsms_count" in tracer_h_text,
150160
"tracer.h must keep LSM program attach state",
@@ -177,6 +187,10 @@ def read_source(path: Path) -> str:
177187
'SEC("lsm/bprm_check_security")' in exec_enforce_text,
178188
"AI Agent exec enforcement must attach to lsm/bprm_check_security",
179189
)
190+
require(
191+
"BPF_PROG(bpf_lsm_bprm_check_security," in exec_enforce_text,
192+
"AI Agent exec enforcement BPF function name must match the bpf_lsm_<hook> BTF name for BCC/libbpf lookup",
193+
)
180194
require(
181195
"is_ai_agent_process" in exec_enforce_text
182196
or "ai_agent_pids" in exec_enforce_text,
@@ -186,6 +200,23 @@ def read_source(path: Path) -> str:
186200
"DATA_SOURCE_PROC_BLOCK_EVENT" in exec_enforce_text,
187201
"AI Agent exec enforcement must emit proc block events",
188202
)
203+
require(
204+
"#define AI_AGENT_EXEC_MAX_RULES 8" in exec_enforce_text,
205+
"AI Agent exec enforcement must cap BPF-side rule scan to 8 records to stay under old verifier complexity limits",
206+
)
207+
require(
208+
"ai_agent_match_contains" not in exec_enforce_text
209+
and "AI_AGENT_EXEC_MATCH_ARGV_CONTAINS" not in exec_enforce_text,
210+
"AI Agent exec enforcement BPF must not include argv_contains nested scans on old verifier kernels",
211+
)
212+
require(
213+
"pattern_hash" in exec_enforce_text
214+
and "ai_agent_hash_exec_path" in exec_enforce_text
215+
and "ai_agent_match_exact" not in exec_enforce_text
216+
and "ai_agent_match_prefix" not in exec_enforce_text
217+
and "ai_agent_match_suffix" not in exec_enforce_text,
218+
"AI Agent exec enforcement BPF must use precomputed exact path hashes instead of verifier-expensive string scans",
219+
)
189220
require(
190221
"ai_agent_submit_event" in exec_enforce_text,
191222
"AI Agent exec enforcement must submit events through the AI Agent pipeline",

agent/src/ebpf/user/load.c

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,23 @@ extern int btf__set_pointer_size(struct btf *btf, size_t ptr_sz);
7474

7575
static int probe_read_kernel_feat;
7676

77+
static const char *prog_load_name(const struct ebpf_prog *prog, char *buf,
78+
size_t buf_len)
79+
{
80+
if (prog->type == BPF_PROG_TYPE_LSM && prog->sec_name != NULL &&
81+
!strncmp(prog->sec_name, "lsm/", 4) && prog->sec_name[4] != '\0') {
82+
/*
83+
* BCC uses the lsm__ prefix to set expected_attach_type to
84+
* BPF_LSM_MAC. libbpf then adds the kernel BTF bpf_lsm_
85+
* prefix while resolving attach_btf_id.
86+
*/
87+
snprintf(buf, buf_len, "lsm__%s", prog->sec_name + 4);
88+
return buf;
89+
}
90+
91+
return prog->name;
92+
}
93+
7794
int suspend_stderr()
7895
{
7996
fflush(stderr);
@@ -261,7 +278,10 @@ static void log_verifier_tail(const char *buf, size_t len)
261278

262279
int load_ebpf_prog(struct ebpf_prog *prog)
263280
{
264-
return bcc_prog_load(prog->type, prog->name,
281+
char name_buf[128];
282+
const char *name = prog_load_name(prog, name_buf, sizeof(name_buf));
283+
284+
return bcc_prog_load(prog->type, name,
265285
prog->insns, prog->insns_size, prog->obj->license,
266286
prog->obj->kern_version, 0, NULL,
267287
0 /*EBPF_LOG_LEVEL, log_buf, LOG_BUF_SZ */ );
@@ -776,12 +796,15 @@ static int load_obj__progs(struct ebpf_object *obj)
776796
// Modify eBPF instructions based on BTF relocation information.
777797
obj_relocate_core(new_prog);
778798

799+
char name_buf[128];
800+
const char *name =
801+
prog_load_name(new_prog, name_buf, sizeof(name_buf));
779802
int stderr_fd = suspend_stderr();
780803
if (stderr_fd < 0) {
781804
ebpf_warning("Failed to suspend stderr\n");
782805
}
783806
new_prog->prog_fd =
784-
bcc_prog_load(new_prog->type, new_prog->name,
807+
bcc_prog_load(new_prog->type, name,
785808
new_prog->insns, new_prog->insns_size,
786809
obj->license, obj->kern_version, 0, NULL,
787810
0 /*EBPF_LOG_LEVEL, log_buf, LOG_BUF_SZ */ );
@@ -792,7 +815,7 @@ static int load_obj__progs(struct ebpf_object *obj)
792815
bool save_full_log = env_flag_enabled(VERIFIER_LOG_ENV);
793816
ebpf_warning
794817
("bcc_prog_load() failed. name: %s, %s errno: %d\n",
795-
new_prog->name, strerror(errno), errno);
818+
name, strerror(errno), errno);
796819
char log_path[] = "/tmp/df_verifier_XXXXXX.log";
797820
int tmp_fd = -1;
798821
char tail_buf[VERIFIER_LOG_TAIL_BYTES + 1] = { 0 };
@@ -858,7 +881,7 @@ static int load_obj__progs(struct ebpf_object *obj)
858881
log_pipe[1] = -1;
859882

860883
fd2 = bcc_prog_load(new_prog->type,
861-
new_prog->name,
884+
name,
862885
new_prog->insns,
863886
new_prog->insns_size,
864887
obj->license,
@@ -912,7 +935,7 @@ static int load_obj__progs(struct ebpf_object *obj)
912935
}
913936

914937
fd2 = bcc_prog_load(new_prog->type,
915-
new_prog->name,
938+
name,
916939
new_prog->insns,
917940
new_prog->insns_size,
918941
obj->license,
@@ -936,7 +959,7 @@ static int load_obj__progs(struct ebpf_object *obj)
936959

937960
if (!load_attempted) {
938961
fd2 = bcc_prog_load(new_prog->type,
939-
new_prog->name,
962+
name,
940963
new_prog->insns,
941964
new_prog->insns_size,
942965
obj->license,
@@ -972,7 +995,7 @@ static int load_obj__progs(struct ebpf_object *obj)
972995
// Preserve errno from the latest attempt for better diagnostics.
973996
ebpf_warning
974997
("bcc_prog_load() still failed. name: %s, errno after retry: %d (orig %d)\n",
975-
new_prog->name, retry_errno, saved_errno);
998+
name, retry_errno, saved_errno);
976999
errno = retry_errno;
9771000
}
9781001

agent/src/ebpf/user/probe.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,11 @@ struct ebpf_link *program__attach_lsm(void *prog)
511511
return NULL;
512512
}
513513

514-
pfd = bpf_raw_tracepoint_open(hook, ebpf_prog->prog_fd);
514+
/*
515+
* BPF LSM programs are loaded with attach_btf_id. The raw tracepoint
516+
* attach syscall should use a NULL name and attach by that BTF id.
517+
*/
518+
pfd = bpf_raw_tracepoint_open(NULL, ebpf_prog->prog_fd);
515519
if (pfd < 0) {
516520
if (errno == EOPNOTSUPP || errno == EINVAL) {
517521
ebpf_warning("BPF LSM attach unsupported for %s: %s(%d)\n",

agent/src/ebpf_dispatcher.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ pub mod memory_profile;
4343
use std::ffi::{CStr, CString};
4444
use std::ptr::{self, null_mut};
4545
use std::slice;
46-
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicU64, Ordering};
46+
#[cfg(feature = "enterprise")]
47+
use std::sync::atomic::AtomicI32;
48+
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
4749
use std::sync::Arc;
4850
use std::thread::{self, JoinHandle};
4951
use std::time::Duration;
@@ -684,7 +686,7 @@ static AI_AGENT_EXEC_RULES_MAP_FD: AtomicI32 = AtomicI32::new(-1);
684686
#[cfg(feature = "enterprise")]
685687
static AI_AGENT_POLICY_EPOCH_MAP_FD: AtomicI32 = AtomicI32::new(-1);
686688
#[cfg(feature = "enterprise")]
687-
const AI_AGENT_EXEC_RULES_BPF_MAX: usize = 256;
689+
const AI_AGENT_EXEC_RULES_BPF_MAX: usize = 8;
688690

689691
#[cfg(feature = "enterprise")]
690692
fn ai_agent_enforcement_mode_eq(value: &str, expected: &str) -> bool {

0 commit comments

Comments
 (0)