#![cfg_attr(not(target_os = "linux"), allow(dead_code))]
use aa_security::policy::{lower_to_ebpf, EbpfRuleSet, PathVerdict, PolicyDocument};
const DEFAULT_LOADERD_SOCKET: &str = "/run/aa-ebpf-loaderd.sock";
const LOADERD_SOCKET_ENV: &str = "AA_EBPF_LOADERD_SOCK";
const POLICY_PATH_ENV: &str = "AA_EBPF_POLICY_PATH";
const CONFINE_PID_ENV: &str = "AA_EBPF_CONFINE_PID";
const INPROCESS_LOAD_ENV: &str = "AA_EBPF_INPROCESS_LOAD";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ProbeKind {
Tls,
FileIo,
Exec,
SyscallGuard,
}
impl ProbeKind {
fn sub_layer(self) -> &'static str {
match self {
ProbeKind::Tls => "ebpf/tls",
ProbeKind::FileIo => "ebpf/file_io",
ProbeKind::Exec => "ebpf/exec",
ProbeKind::SyscallGuard => "ebpf/syscall_guard",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum PlannedOp {
Load(ProbeKind),
UpdatePathMap(Vec<(String, bool)>),
UpdateSyscallAllowlist(Vec<u32>),
}
pub(crate) fn plan_control_ops(ruleset: &EbpfRuleSet, confine_pid: Option<u32>) -> Vec<PlannedOp> {
let mut plan = vec![
PlannedOp::Load(ProbeKind::Tls),
PlannedOp::Load(ProbeKind::FileIo),
PlannedOp::Load(ProbeKind::Exec),
];
plan.push(PlannedOp::UpdatePathMap(
ruleset
.path_rules
.iter()
.map(|r| (r.pattern.clone(), r.verdict == PathVerdict::Deny))
.collect(),
));
if confine_pid.is_some() && !ruleset.syscall_allowlist.is_empty() {
plan.push(PlannedOp::Load(ProbeKind::SyscallGuard));
plan.push(PlannedOp::UpdateSyscallAllowlist(ruleset.syscall_allowlist.clone()));
}
plan
}
pub(crate) fn resolve_loaderd_socket() -> std::path::PathBuf {
std::env::var_os(LOADERD_SOCKET_ENV)
.map(std::path::PathBuf::from)
.unwrap_or_else(|| std::path::PathBuf::from(DEFAULT_LOADERD_SOCKET))
}
pub(crate) fn confine_pid() -> Option<u32> {
std::env::var(CONFINE_PID_ENV)
.ok()
.and_then(|v| v.parse::<u32>().ok())
.filter(|&n| n > 0)
}
pub(crate) fn use_inprocess_load() -> bool {
std::env::var(INPROCESS_LOAD_ENV)
.ok()
.map(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "true" | "1" | "yes" | "on"))
.unwrap_or(false)
}
pub(crate) fn load_ebpf_ruleset() -> EbpfRuleSet {
let Some(path) = std::env::var_os(POLICY_PATH_ENV)
.filter(|v| !v.is_empty())
.map(std::path::PathBuf::from)
else {
return EbpfRuleSet::default();
};
match std::fs::read_to_string(&path) {
Ok(yaml) => match PolicyDocument::from_yaml(&yaml) {
Ok(doc) => lower_to_ebpf(&doc),
Err(e) => {
tracing::warn!(error = %e, path = %path.display(), "eBPF policy parse failed — using empty rule set");
EbpfRuleSet::default()
}
},
Err(e) => {
tracing::warn!(error = %e, path = %path.display(), "eBPF policy unreadable — using empty rule set");
EbpfRuleSet::default()
}
}
}
fn degrade(
broadcast_tx: &tokio::sync::broadcast::Sender<crate::pipeline::PipelineEvent>,
degraded_layers: &mut Vec<String>,
sub_layer: &str,
reason: String,
) {
tracing::warn!(sub_layer, %reason, "degrading eBPF sub-layer");
crate::runtime::emit_ebpf_degradation(broadcast_tx, sub_layer, reason);
degraded_layers.push(sub_layer.to_string());
}
#[cfg(target_os = "linux")]
fn loaderd_deadline() -> std::time::Duration {
let ms = std::env::var("AA_EBPF_LOADERD_TIMEOUT_MS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.filter(|&v| v > 0)
.unwrap_or(5_000);
std::time::Duration::from_millis(ms)
}
#[cfg(target_os = "linux")]
async fn await_loaderd<F, T, E>(fut: F) -> Result<T, String>
where
F: std::future::Future<Output = Result<T, E>>,
E: std::fmt::Display,
{
match tokio::time::timeout(loaderd_deadline(), fut).await {
Ok(Ok(v)) => Ok(v),
Ok(Err(e)) => Err(e.to_string()),
Err(_elapsed) => Err("timed out waiting for loaderd".to_string()),
}
}
#[cfg(target_os = "linux")]
pub(crate) async fn drive_ebpf_layer(
broadcast_tx: &tokio::sync::broadcast::Sender<crate::pipeline::PipelineEvent>,
degraded_layers: &mut Vec<String>,
) {
use aa_ebpf::control::client::LoaderControlClient;
use aa_ebpf::control::protocol::{PathRuleWire, ProbeSet};
let socket = resolve_loaderd_socket();
let ruleset = load_ebpf_ruleset();
let confine_pid = confine_pid();
let observe_pid = std::process::id();
let plan = plan_control_ops(&ruleset, confine_pid);
let mut client = match await_loaderd(LoaderControlClient::connect(&socket)).await {
Ok(c) => c,
Err(e) => {
let reason = format!("loaderd control socket unreachable at {}: {e}", socket.display());
for kind in [ProbeKind::Tls, ProbeKind::FileIo, ProbeKind::Exec] {
degrade(broadcast_tx, degraded_layers, kind.sub_layer(), reason.clone());
}
if confine_pid.is_some() {
degrade(
broadcast_tx,
degraded_layers,
ProbeKind::SyscallGuard.sub_layer(),
reason.clone(),
);
}
return;
}
};
for op in plan {
match op {
PlannedOp::Load(kind) => {
let (set, pid) = match kind {
ProbeKind::Tls => (ProbeSet::Tls, observe_pid),
ProbeKind::FileIo => (ProbeSet::FileIo, observe_pid),
ProbeKind::Exec => (ProbeSet::Exec, observe_pid),
ProbeKind::SyscallGuard => match confine_pid {
Some(pid) => (ProbeSet::SyscallGuard, pid),
None => {
tracing::error!(
"BUG: SyscallGuard planned without a confine PID; refusing to scope \
the SIGKILL guard to the runtime's own PID"
);
continue;
}
},
};
if let Err(e) = await_loaderd(client.load_probe_set(set, pid)).await {
degrade(
broadcast_tx,
degraded_layers,
kind.sub_layer(),
format!("loaderd load failed: {e}"),
);
}
}
PlannedOp::UpdatePathMap(rules) => {
let wire: Vec<PathRuleWire> = rules
.into_iter()
.map(|(pattern, deny)| PathRuleWire { pattern, deny })
.collect();
if let Err(e) = await_loaderd(client.update_path_map(wire)).await {
degrade(
broadcast_tx,
degraded_layers,
ProbeKind::FileIo.sub_layer(),
format!("loaderd path map update failed: {e}"),
);
}
}
PlannedOp::UpdateSyscallAllowlist(syscalls) => {
if let Err(e) = await_loaderd(client.update_syscall_allowlist(syscalls)).await {
degrade(
broadcast_tx,
degraded_layers,
ProbeKind::SyscallGuard.sub_layer(),
format!("loaderd syscall allowlist update failed: {e}"),
);
}
}
}
}
tracing::info!(socket = %socket.display(), confine_pid = ?confine_pid, "eBPF layer delegated to loaderd");
}
#[cfg(not(target_os = "linux"))]
pub(crate) async fn drive_ebpf_layer(
broadcast_tx: &tokio::sync::broadcast::Sender<crate::pipeline::PipelineEvent>,
degraded_layers: &mut Vec<String>,
) {
for sub_layer in ["ebpf/tls", "ebpf/file_io", "ebpf/exec"] {
degrade(
broadcast_tx,
degraded_layers,
sub_layer,
"eBPF not supported on this platform".to_string(),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use aa_security::policy::{EbpfRuleSet, PathRule, PathVerdict};
fn ruleset(path_rules: Vec<PathRule>, syscalls: Vec<u32>) -> EbpfRuleSet {
EbpfRuleSet {
path_rules,
egress_allowlist: Vec::new(),
syscall_allowlist: syscalls,
}
}
#[test]
fn plan_always_loads_the_three_observe_only_sets() {
let plan = plan_control_ops(&ruleset(vec![], vec![]), None);
assert!(plan.contains(&PlannedOp::Load(ProbeKind::Tls)));
assert!(plan.contains(&PlannedOp::Load(ProbeKind::FileIo)));
assert!(plan.contains(&PlannedOp::Load(ProbeKind::Exec)));
}
#[test]
fn plan_always_pushes_a_path_map_op() {
let plan = plan_control_ops(&ruleset(vec![], vec![]), None);
assert!(plan.iter().any(|op| matches!(op, PlannedOp::UpdatePathMap(_))));
}
#[test]
fn path_rules_lower_to_deny_flags() {
let rules = vec![
PathRule {
pattern: "/etc/shadow".into(),
verdict: PathVerdict::Deny,
},
PathRule {
pattern: "/tmp/ok".into(),
verdict: PathVerdict::Allow,
},
];
let plan = plan_control_ops(&ruleset(rules, vec![]), None);
let map = plan
.iter()
.find_map(|op| match op {
PlannedOp::UpdatePathMap(m) => Some(m.clone()),
_ => None,
})
.expect("path map op present");
assert_eq!(
map,
vec![("/etc/shadow".to_string(), true), ("/tmp/ok".to_string(), false)]
);
}
#[test]
fn syscall_guard_never_planned_without_a_confine_pid() {
let plan = plan_control_ops(&ruleset(vec![], vec![0, 1, 60]), None);
assert!(!plan.contains(&PlannedOp::Load(ProbeKind::SyscallGuard)));
assert!(!plan.iter().any(|op| matches!(op, PlannedOp::UpdateSyscallAllowlist(_))));
}
#[test]
fn syscall_guard_never_planned_with_empty_allowlist() {
let plan = plan_control_ops(&ruleset(vec![], vec![]), Some(4321));
assert!(!plan.contains(&PlannedOp::Load(ProbeKind::SyscallGuard)));
}
#[test]
fn syscall_guard_planned_only_with_confine_pid_and_allowlist() {
let plan = plan_control_ops(&ruleset(vec![], vec![0, 1, 60]), Some(4321));
let load_idx = plan
.iter()
.position(|op| *op == PlannedOp::Load(ProbeKind::SyscallGuard))
.expect("guard load present");
let update_idx = plan
.iter()
.position(|op| matches!(op, PlannedOp::UpdateSyscallAllowlist(_)))
.expect("allowlist update present");
assert!(load_idx < update_idx, "guard must load before its allowlist is set");
assert_eq!(plan[update_idx], PlannedOp::UpdateSyscallAllowlist(vec![0, 1, 60]));
}
#[test]
fn resolve_socket_honours_env_then_falls_back_to_default() {
unsafe {
std::env::set_var(LOADERD_SOCKET_ENV, "/tmp/aa-loaderd-test.sock");
}
assert_eq!(
resolve_loaderd_socket(),
std::path::PathBuf::from("/tmp/aa-loaderd-test.sock")
);
unsafe {
std::env::remove_var(LOADERD_SOCKET_ENV);
}
assert_eq!(
resolve_loaderd_socket(),
std::path::PathBuf::from(DEFAULT_LOADERD_SOCKET)
);
}
}