Skip to content

Commit ceeaeca

Browse files
feat(agent): support ai agent syscall override enforcement
1 parent 7eebc06 commit ceeaeca

13 files changed

Lines changed: 599 additions & 38 deletions

File tree

agent/crates/enterprise-utils/src/lib.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,14 @@ pub mod ai_agent_enforcement {
520520
pub argv_contains_any: Vec<String>,
521521
}
522522

523+
#[derive(Clone, Debug, PartialEq, Eq)]
524+
pub struct SyscallRuleInput {
525+
pub id: String,
526+
pub mode: EnforcementMode,
527+
pub names: Vec<String>,
528+
pub symbols: Vec<String>,
529+
}
530+
523531
#[derive(Clone, Debug, PartialEq, Eq)]
524532
pub struct PolicyHit {
525533
pub rule_index: u32,
@@ -532,6 +540,11 @@ pub mod ai_agent_enforcement {
532540
pub epoch: u64,
533541
}
534542

543+
#[derive(Clone, Debug, PartialEq, Eq)]
544+
pub struct CompiledSyscallPolicy {
545+
pub epoch: u64,
546+
}
547+
535548
impl CompiledExecPolicy {
536549
pub fn match_exec(&self, _exec_path: &str, _cmdline: &str) -> Option<PolicyHit> {
537550
None
@@ -547,10 +560,63 @@ pub mod ai_agent_enforcement {
547560
}
548561
}
549562

563+
impl CompiledSyscallPolicy {
564+
pub fn to_bpf_records(&self) -> Vec<BpfSyscallRuleRecord> {
565+
vec![]
566+
}
567+
568+
pub fn sync_to_bpf_maps(
569+
&self,
570+
_syscall_rules_fd: i32,
571+
_policy_epoch_fd: i32,
572+
_max_records: usize,
573+
) -> Result<(), String> {
574+
Ok(())
575+
}
576+
}
577+
578+
#[repr(C)]
579+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
580+
pub struct BpfSyscallRuleRecord {
581+
pub rule_index: u32,
582+
pub mode: u8,
583+
pub syscall_key: u8,
584+
pub reserved: u16,
585+
pub syscall_id: u32,
586+
pub errno_code: i32,
587+
pub rule_id: [u8; 64],
588+
pub syscall_name: [u8; 32],
589+
}
590+
591+
impl Default for BpfSyscallRuleRecord {
592+
fn default() -> Self {
593+
Self {
594+
rule_index: 0,
595+
mode: 0,
596+
syscall_key: 0,
597+
reserved: 0,
598+
syscall_id: 0,
599+
errno_code: 0,
600+
rule_id: [0; 64],
601+
syscall_name: [0; 32],
602+
}
603+
}
604+
}
605+
550606
pub fn compile_exec_rules(_rules: &[ExecRuleInput]) -> Result<CompiledExecPolicy, String> {
551607
Ok(CompiledExecPolicy { epoch: 0 })
552608
}
553609

610+
pub fn compile_syscall_rules(
611+
_rules: &[SyscallRuleInput],
612+
) -> Result<CompiledSyscallPolicy, String> {
613+
Ok(CompiledSyscallPolicy { epoch: 0 })
614+
}
615+
616+
pub fn syscall_override_symbols(_syscall_key: u8) -> &'static [&'static str] {
617+
&[]
618+
}
619+
554620
pub fn set_global_exec_policy(_policy: Option<CompiledExecPolicy>) {}
555621

556622
pub fn global_exec_policy() -> Option<Arc<CompiledExecPolicy>> {

agent/src/common/kernel_capability.rs

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ pub struct KernelCapability {
1111
pub bpf_lsm_configured: bool,
1212
pub bpf_lsm_active: bool,
1313
pub bpf_kprobe_override_configured: bool,
14+
pub bpf_kprobe_override_available: bool,
15+
pub bpf_kprobe_override_symbols: Vec<String>,
1416
pub seccomp_filter_configured: bool,
1517
pub btf_vmlinux_available: bool,
1618
}
@@ -29,14 +31,18 @@ impl KernelCapability {
2931
let lsm_text = fs::read_to_string(sys_root.join("kernel/security/lsm")).unwrap_or_default();
3032
let config_text = read_kernel_config_from_roots(proc_root, boot_root).unwrap_or_default();
3133
let bpf_lsm_active = lsm_has_bpf(&lsm_text);
34+
let bpf_kprobe_override_symbols = read_kprobe_override_symbols(sys_root);
35+
let bpf_kprobe_override_configured =
36+
config_enabled(&config_text, "CONFIG_BPF_KPROBE_OVERRIDE")
37+
|| !bpf_kprobe_override_symbols.is_empty();
3238

3339
Self {
3440
bpf_lsm_configured: config_enabled(&config_text, "CONFIG_BPF_LSM") || bpf_lsm_active,
3541
bpf_lsm_active,
36-
bpf_kprobe_override_configured: config_enabled(
37-
&config_text,
38-
"CONFIG_BPF_KPROBE_OVERRIDE",
39-
),
42+
bpf_kprobe_override_configured,
43+
bpf_kprobe_override_available: bpf_kprobe_override_configured
44+
&& !bpf_kprobe_override_symbols.is_empty(),
45+
bpf_kprobe_override_symbols,
4046
seccomp_filter_configured: config_enabled(&config_text, "CONFIG_SECCOMP_FILTER"),
4147
btf_vmlinux_available: sys_root.join("kernel/btf/vmlinux").exists(),
4248
}
@@ -45,6 +51,14 @@ impl KernelCapability {
4551
pub fn supports_exec_lsm_enforcement(&self) -> bool {
4652
self.bpf_lsm_configured && self.bpf_lsm_active
4753
}
54+
55+
pub fn supports_kprobe_override_symbol(&self, symbol: &str) -> bool {
56+
self.bpf_kprobe_override_available
57+
&& self
58+
.bpf_kprobe_override_symbols
59+
.iter()
60+
.any(|allowed| allowed == symbol)
61+
}
4862
}
4963

5064
fn path_from_env(name: &str, default: &str) -> PathBuf {
@@ -97,6 +111,37 @@ fn read_proc_kernel_config(proc_root: &Path) -> Option<String> {
97111
decode_gzip(&compressed).ok()
98112
}
99113

114+
fn read_kprobe_override_symbols(sys_root: &Path) -> Vec<String> {
115+
const REL_PATHS: [&str; 2] = [
116+
"kernel/debug/error_injection/list",
117+
"kernel/debug/fail_function/injectable",
118+
];
119+
120+
let mut symbols = Vec::new();
121+
for rel_path in REL_PATHS {
122+
let Ok(text) = fs::read_to_string(sys_root.join(rel_path)) else {
123+
continue;
124+
};
125+
symbols.extend(parse_error_injection_symbols(&text));
126+
}
127+
symbols.sort();
128+
symbols.dedup();
129+
symbols
130+
}
131+
132+
fn parse_error_injection_symbols(text: &str) -> Vec<String> {
133+
text.lines()
134+
.filter_map(|line| {
135+
let token = line.split_whitespace().next().unwrap_or_default().trim();
136+
if token.is_empty() || token.starts_with('#') {
137+
None
138+
} else {
139+
Some(token.to_string())
140+
}
141+
})
142+
.collect()
143+
}
144+
100145
fn decode_gzip(bytes: &[u8]) -> Result<String, std::io::Error> {
101146
let mut decoder = GzDecoder::new(Cursor::new(bytes));
102147
let mut output = String::new();
@@ -123,6 +168,14 @@ mod tests {
123168
));
124169
}
125170

171+
#[test]
172+
fn parse_error_injection_list_takes_first_column() {
173+
assert_eq!(
174+
parse_error_injection_symbols("__x64_sys_reboot\tEI_ETYPE_ERRNO\n# ignored\n"),
175+
vec!["__x64_sys_reboot".to_string()]
176+
);
177+
}
178+
126179
#[test]
127180
fn support_exec_lsm_requires_config_and_active_lsm() {
128181
assert!(KernelCapability {
@@ -166,6 +219,31 @@ mod tests {
166219
let _ = fs::remove_dir_all(root);
167220
}
168221

222+
#[test]
223+
fn detect_from_roots_uses_error_injection_allowlist_for_kprobe_override() {
224+
let root = make_temp_root("kprobe-override-allowlist");
225+
let proc_root = root.join("host-proc");
226+
let sys_root = root.join("host-sys");
227+
let boot_root = root.join("boot");
228+
fs::create_dir_all(proc_root.join("sys/kernel")).unwrap();
229+
fs::create_dir_all(sys_root.join("kernel/debug/error_injection")).unwrap();
230+
fs::write(proc_root.join("sys/kernel/osrelease"), "4.18.0-test\n").unwrap();
231+
fs::write(
232+
sys_root.join("kernel/debug/error_injection/list"),
233+
"__x64_sys_reboot\n__x64_sys_init_module\n",
234+
)
235+
.unwrap();
236+
237+
let capability = KernelCapability::detect_from_roots(&proc_root, &sys_root, &boot_root);
238+
239+
assert!(capability.bpf_kprobe_override_configured);
240+
assert!(capability.bpf_kprobe_override_available);
241+
assert!(capability.supports_kprobe_override_symbol("__x64_sys_reboot"));
242+
assert!(!capability.supports_kprobe_override_symbol("__x64_sys_mount"));
243+
244+
let _ = fs::remove_dir_all(root);
245+
}
246+
169247
fn make_temp_root(name: &str) -> std::path::PathBuf {
170248
let root = std::env::temp_dir().join(format!(
171249
"deepflow-kernel-capability-{name}-{}",

agent/src/common/proc_event/linux.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,37 @@ mod tests {
952952
assert_eq!(pb.mechanism, "lsm");
953953
}
954954

955+
#[test]
956+
fn test_proc_block_event_into_metric_carries_syscall_override() {
957+
let raw = make_proc_block_raw(
958+
2,
959+
2,
960+
2,
961+
1,
962+
1,
963+
13,
964+
100,
965+
10,
966+
1000,
967+
1000,
968+
169,
969+
42,
970+
b"block-direct-reboot",
971+
b"reboot",
972+
b"",
973+
b"reboot",
974+
);
975+
let event = ProcBlockEventData::try_from(raw.as_slice()).unwrap();
976+
let pb: metric::ProcBlockEventData = event.into();
977+
assert_eq!(
978+
pb.target_type,
979+
metric::EnforcementTargetType::EnforcementTargetSyscall as i32
980+
);
981+
assert_eq!(pb.mechanism, "kprobe_override");
982+
assert_eq!(pb.syscall_name, "reboot");
983+
assert_eq!(pb.syscall_id, 169);
984+
}
985+
955986
#[test]
956987
fn test_new_proc_block_event_for_audit_encodes_proc_block_event() {
957988
let proc_event = ProcEvent {

agent/src/config/config.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,7 @@ pub struct AiAgentEnforcementRule {
668668
pub action: AiAgentEnforcementAction,
669669
pub audit: bool,
670670
pub exec: AiAgentExecMatch,
671+
pub syscall: AiAgentSyscallMatch,
671672
}
672673

673674
impl Default for AiAgentEnforcementRule {
@@ -680,6 +681,7 @@ impl Default for AiAgentEnforcementRule {
680681
action: AiAgentEnforcementAction::default(),
681682
audit: true,
682683
exec: AiAgentExecMatch::default(),
684+
syscall: AiAgentSyscallMatch::default(),
683685
}
684686
}
685687
}
@@ -710,6 +712,13 @@ pub struct AiAgentExecMatch {
710712
pub argv_contains_any: Vec<String>,
711713
}
712714

715+
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)]
716+
#[serde(default)]
717+
pub struct AiAgentSyscallMatch {
718+
pub names: Vec<String>,
719+
pub symbols: Vec<String>,
720+
}
721+
713722
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
714723
#[serde(default)]
715724
pub struct Proc {

agent/src/ebpf/kernel/include/bpf_base.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ struct task_struct;
4141
// Helper ID for bpf_get_current_task_btf (introduced in Linux 5.11).
4242
#define BPF_FUNC_get_current_task_btf 158
4343
#endif
44+
#ifndef BPF_FUNC_override_return
45+
// Helper ID for bpf_override_return (kprobe override / error injection).
46+
#define BPF_FUNC_override_return 58
47+
#endif
4448

4549
/*
4650
* bpf helpers
@@ -148,6 +152,11 @@ static int
148152
static int
149153
__attribute__ ((__unused__)) (*bpf_get_stack) (void *ctx, void *buf, __u32 size,
150154
int flags) = (void *)67;
155+
static long
156+
__attribute__ ((__unused__)) (*bpf_override_return) (struct pt_regs *regs,
157+
__u64 rc) =
158+
(void *)BPF_FUNC_override_return;
159+
#define DF_BPF_OVERRIDE_RETURN_HELPER_DECLARED 1
151160

152161

153162
// Linux 4.14: Added support for BPF_MAP_TYPE_CPUMAP, allowing packets to be redirected to specific CPUs.

agent/src/ebpf/test/test_ai_agent_source_contracts.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ def read_source(path: Path) -> str:
163163
"lsm_programs_handle" in tracer_c_text,
164164
"tracer.c must include LSM programs in the attach lifecycle",
165165
)
166+
require(
167+
"optional_kprobe_programs_handle" in tracer_c_text,
168+
"tracer.c must include optional AI Agent kprobe programs in the attach lifecycle",
169+
)
166170
require(
167171
"new_prog->type == BPF_PROG_TYPE_LSM" in load_text
168172
and "Skip optional BPF LSM program" in load_text,
@@ -225,5 +229,33 @@ def read_source(path: Path) -> str:
225229
"ai_agent_exec_enforce.bpf.c" in support_text,
226230
"support_extended_observability must include ai_agent_exec_enforce.bpf.c",
227231
)
232+
syscall_override_bpf = ENTERPRISE_BPF / "ai_agent_syscall_override.bpf.c"
233+
require(
234+
syscall_override_bpf.exists(),
235+
f"missing enterprise AI Agent syscall override BPF: {syscall_override_bpf}",
236+
)
237+
syscall_override_text = read_source(syscall_override_bpf)
238+
require(
239+
"bpf_override_return(ctx," in syscall_override_text,
240+
"AI Agent syscall enforcement must use bpf_override_return for blocking",
241+
)
242+
require(
243+
'SEC("kprobe/__x64_sys_reboot")' in syscall_override_text,
244+
"AI Agent syscall enforcement must hook direct reboot syscall with kprobe override",
245+
)
246+
require(
247+
"df_K_ai_agent_syscall_override_" in tracer_c_text
248+
or "optional kprobe: 'kprobe/__x64_sys_reboot'" in tracer_c_text,
249+
"tracer.c must explicitly attach AI Agent syscall override kprobes",
250+
)
251+
require(
252+
"ai_agent_syscall_override.bpf.c" in support_text,
253+
"support_extended_observability must include ai_agent_syscall_override.bpf.c",
254+
)
255+
require(
256+
"df_K_ai_agent_syscall_override_" in load_text
257+
and "Skip optional AI Agent kprobe override program" in load_text,
258+
"load.c must keep unsupported AI Agent kprobe override programs non-fatal",
259+
)
228260

229261
print("[OK]")

agent/src/ebpf/user/load.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,13 @@ static enum bpf_prog_type get_prog_type(struct sec_desc *desc)
665665
return prog_type;
666666
}
667667

668+
static bool is_optional_ai_agent_kprobe_override_prog(struct ebpf_prog *prog)
669+
{
670+
return prog != NULL && prog->type == BPF_PROG_TYPE_KPROBE &&
671+
prog->name != NULL &&
672+
strstr(prog->name, "df_K_ai_agent_syscall_override_") == prog->name;
673+
}
674+
668675
static int load_obj__progs(struct ebpf_object *obj)
669676
{
670677
int i;
@@ -1024,6 +1031,13 @@ static int load_obj__progs(struct ebpf_object *obj)
10241031
continue;
10251032
}
10261033

1034+
if (is_optional_ai_agent_kprobe_override_prog(new_prog)) {
1035+
ebpf_warning
1036+
("Skip optional AI Agent kprobe override program '%s'; syscall enforcement disabled for this hook.\n",
1037+
new_prog->name);
1038+
continue;
1039+
}
1040+
10271041
if (memcmp(desc->name, "uprobe/", 7) &&
10281042
memcmp(desc->name, "uretprobe/", 10)) {
10291043
return ETR_INVAL;

0 commit comments

Comments
 (0)