Skip to content

Commit aa8c314

Browse files
ErenAriclaude
andauthored
feat(bpf/file): Tier-3 signal-fallback enforcement for the file class (#212)
Closes the file-class arm of Tier-3 signal-fallback, the symmetric twin of the already-shipped network arm (handle_tp_connect). On kernels without BPF-LSM, lsm/file_open cannot attach and open() cannot be denied with -EPERM; previously the sys_enter_openat tracepoint was pure audit. It now mirrors handle_tp_connect exactly: when --enforce-fallback=signal is set (agent_cfg.signal_fallback_enforce) AND the agent is in enforce mode, an open of a denied path is met with bpf_send_signal() (default SIGKILL, with the same "default to SIGKILL when no escalation configured" rule since a tracepoint cannot return -EPERM). Inert by default, so LSM-capable hosts behave exactly as before (audit-only). Honesty: this tier is detection+signal, not synchronous denial, and is PATH-based (the inode is not resolvable at syscall entry) — so it does NOT carry the inode-alias guarantee proved for lsm/file_open in proofs/inode_alias_resistance.py. GUARANTEES.md is updated to state the mechanism now covers both connect() and open(), and to keep the gate promotion (accepting signal-fallback as PRIMARY enforcement on genuinely no-LSM hosts) honestly flagged as the remaining follow-up — that step relaxes the No-Pretend gate and must be validated on a no-LSM kernel. Reuses the existing --enforce-fallback=signal flag (no new wiring). No new program/section, so the bpfcompat + kernel-compat manifests are unchanged; object builds and loads clean on 6.17. Cross-kernel verifier-safety is gated by the bpfcompat matrix on this PR. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 63ad98f commit aa8c314

2 files changed

Lines changed: 60 additions & 15 deletions

File tree

bpf/aegis_file.bpf.h

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,23 @@ int BPF_PROG(handle_inode_permission, struct inode *inode, int mask)
289289
return ret;
290290
}
291291

292+
/* ============================================================================
293+
* File open tracepoint.
294+
*
295+
* Always-on AUDIT for the path-based deny list. Additionally provides the
296+
* file-class arm of Tier-3 signal-fallback enforcement (the symmetric twin of
297+
* handle_tp_connect in aegis_net.bpf.h): on kernels without BPF-LSM, where
298+
* lsm/file_open cannot attach and open() cannot be denied with -EPERM, an open
299+
* of a denied path is met with bpf_send_signal() when signal-fallback is enabled
300+
* AND the agent is in enforce mode.
301+
*
302+
* This is detection + signal, NOT synchronous denial, and it is PATH-based (the
303+
* inode is not resolvable at syscall entry), so it is strictly weaker than the
304+
* inode-based lsm/file_open deny — in particular it does NOT carry the
305+
* inode-alias guarantee proved in proofs/inode_alias_resistance.py. Inert unless
306+
* agent_cfg.signal_fallback_enforce is set (default off), so on LSM-capable
307+
* hosts running normally it behaves exactly as before (audit-only).
308+
* ============================================================================ */
292309
SEC("tracepoint/syscalls/sys_enter_openat")
293310
int handle_openat(struct trace_event_raw_sys_enter *ctx)
294311
{
@@ -323,7 +340,26 @@ int handle_openat(struct trace_event_raw_sys_enter *ctx)
323340
increment_cgroup_stat(cgid);
324341
increment_path_stat(&key);
325342

326-
/* Send block event (audit only - tracepoints can't block) */
343+
/* Tier-3 signal-fallback enforcement (see header comment). A tracepoint
344+
* cannot return -EPERM, so when no SIGKILL escalation is configured we
345+
* default the fallback to SIGKILL to make the tier meaningful — mirroring
346+
* handle_tp_connect. */
347+
__u8 audit = get_effective_audit_mode();
348+
__u8 enforce_signal = 0;
349+
if (agent_cfg.signal_fallback_enforce && !audit) {
350+
__u64 start_time = task ? BPF_CORE_READ(task, start_time) : 0;
351+
__u8 configured_signal = get_effective_enforce_signal();
352+
if (configured_signal == SIGKILL) {
353+
enforce_signal = runtime_enforce_signal(configured_signal, pid, start_time,
354+
get_sigkill_escalation_threshold(),
355+
get_sigkill_escalation_window_ns());
356+
} else {
357+
enforce_signal = configured_signal ? configured_signal : SIGKILL;
358+
}
359+
maybe_send_enforce_signal(enforce_signal);
360+
}
361+
362+
/* Send block event */
327363
if (!should_emit_event(sample_rate))
328364
return 0;
329365
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
@@ -335,7 +371,7 @@ int handle_openat(struct trace_event_raw_sys_enter *ctx)
335371
e->block.ino = 0;
336372
e->block.dev = 0;
337373
__builtin_memcpy(e->block.path, key.path, sizeof(e->block.path));
338-
__builtin_memcpy(e->block.action, "AUDIT", sizeof("AUDIT"));
374+
set_action_string(e->block.action, audit ? 1 : 0, enforce_signal);
339375
bpf_ringbuf_submit(e, 0);
340376
} else {
341377
increment_ringbuf_drops();

docs/GUARANTEES.md

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -132,19 +132,28 @@ see `docs/THREAT_MODEL.md`.
132132

133133
### Signal-fallback enforcement is opt-in and signal-based
134134

135-
- On kernels without BPF-LSM, `connect()` cannot be denied with `-EPERM`. The
136-
opt-in `--enforce-fallback=signal` flag attaches a `sys_enter_connect`
137-
tracepoint that, in enforce mode, terminates a process connecting to a denied
138-
endpoint via `bpf_send_signal()` (default `SIGKILL`).
139-
- This is signal-based termination, not synchronous denial: the `connect()` may
140-
partially proceed before the signal is delivered on syscall return. Protocol is
141-
not resolvable at syscall entry, so only protocol-agnostic rules are evaluated.
142-
- Verified to fire (`SIGKILL`, `net_connect_block` action=`KILL`) on a connect to
143-
a denied IP. NOTE: when BPF-LSM is genuinely absent the enforce-gate currently
144-
treats the missing LSM hook as a degradation (fail-closed exit or audit
145-
fallback); teaching the gate to accept signal-fallback as primary connect
146-
enforcement on no-LSM hosts is a follow-up. Today the tier is verified as
147-
defense-in-depth alongside LSM enforcement.
135+
- On kernels without BPF-LSM, `connect()` and `open()` cannot be denied with
136+
`-EPERM`. The opt-in `--enforce-fallback=signal` flag arms two symmetric
137+
syscall tracepoints — `sys_enter_connect` (network) and `sys_enter_openat`
138+
(file) — that, in enforce mode, terminate a process reaching a denied endpoint
139+
or opening a denied path via `bpf_send_signal()` (default `SIGKILL`).
140+
- This is signal-based termination, not synchronous denial: the syscall may
141+
partially proceed before the signal is delivered on syscall return. It is also
142+
evaluated on the *syscall-entry* view — protocol is not resolvable for
143+
`connect()` (protocol-agnostic rules only), and the file path is matched
144+
by-path rather than by-inode, so the file arm does **not** carry the
145+
inode-alias guarantee that the `lsm/file_open` deny does (proved in
146+
`proofs/inode_alias_resistance.py`). Signal-fallback is a strictly weaker tier.
147+
- The network arm is verified to fire (`SIGKILL`, `net_connect_block`
148+
action=`KILL`) on a connect to a denied IP; the file arm mirrors it on an open
149+
of a denied path.
150+
- **Gate status (honest):** when BPF-LSM is genuinely absent the enforce-gate
151+
still treats the missing LSM hook as a degradation (fail-closed exit or audit
152+
fallback) — it does not yet *promote* signal-fallback to primary enforcement on
153+
no-LSM hosts, because doing so safely requires validating the relaxed
154+
No-Pretend gate on a no-LSM kernel. Today both arms are verified as
155+
defense-in-depth alongside LSM enforcement; gate promotion is the remaining
156+
follow-up.
148157

149158
## Known bypass classes
150159

0 commit comments

Comments
 (0)