Skip to content

feat(sight): observe-only BPF LSM audit probe (socket_connect + file_open)#665

Draft
jfeng18 wants to merge 5 commits into
alibaba:mainfrom
jfeng18:feat/lsm-audit
Draft

feat(sight): observe-only BPF LSM audit probe (socket_connect + file_open)#665
jfeng18 wants to merge 5 commits into
alibaba:mainfrom
jfeng18:feat/lsm-audit

Conversation

@jfeng18

@jfeng18 jfeng18 commented May 31, 2026

Copy link
Copy Markdown
Contributor

What

Adds an opt-in, observe-only BPF LSM probe (lsmaudit) that audits two security-relevant actions for traced Agent families:

  • lsm/socket_connect — every outbound connection attempt (destination IP:port), regardless of protocol or port.
  • lsm/file_open — every file opened (basename + access mode), deduped per (pid, inode) via an LRU map.

Both hooks filter to traced families via the shared traced_processes map (is_pid_traced) and emit through the shared ring buffer (EVENT_SOURCE_LSM). Events surface as structured audit log lines:

[LSM] connect pid=… comm=… dst=IP:PORT
[LSM] open    pid=… comm=… mode=r|w|rw path=…

Observe-only

Strictly observe-only: every program returns the incoming ret unchanged, so no operation is ever denied. The LSM attach point leaves room for future enforcement.

Opt-in

Off by default. Enable with --enable-lsm-audit (or the matching config field). Requires BPF LSM active (bpf in /sys/kernel/security/lsm); otherwise the probe is skipped with a warning.

Correctness fixes folded in (commit fix(sight): lsmaudit BPF correctness + leak guard)

A self-review of the as-pushed branch caught three issues, all fixed in the latest commit:

  1. path[] info leakbpf_ringbuf_reserve returns a slot that may carry residue from a previously-submitted record on the shared ring buffer (sslsniff TLS plaintext, filewrite content). The original code only zeroed path[0], leaving path[1..256] uninitialized. Memset the whole record after reserve in both hooks.
  2. file_open dedup-before-emit — the (pid, inode) LRU was inserted before reserve / read, so any failure permanently suppressed that file. Move the dedup mark to after a successful submit; bpf_ringbuf_discard the slot on path-read failure.
  3. socket_connect addrlen unchecked — LSM fires before the protocol-level length check; if addrlen < sizeof(struct sockaddr_in[6]) the destination fields are stack residue. Drop the event in that case.

Plus an always-on Rust-side leak detector: LsmEvent::from_bytes scans path[] past the null terminator and log::warn!s on any non-zero byte — permanent regression net for the producer-side memset invariant.

Testing

Verification Status What was checked
已 E2E(real-machine, kernel 6.6.102, BPF LSM active) Both hooks attach; traced agent's connects/opens captured with correct fields; observe-only proven (all attempts succeed); 173 events on the post-fix sanity run, 0 leak-detector warnings, paths clean.
仅单测(cargo test --lib lsmaudit) 8/8: gate (cacheability), IPv4/IPv6 + port byte-order decode, file_open parsing, RawLsmEvent C↔Rust layout, leak detector positive (garbage tail triggers warn + decoder still cleanly extracts path), leak detector negative (clean tail stays silent). 447 lib tests, zero regressions.
未 E2E(documented gap) The leak class (residue from ringbuf wrap) is not reproducible in a synthetic test in seconds — the 32 MiB ring rarely wraps. A reverse-test with the memset removed produced 158 events and 0 leak warnings on the synthetic load. The fix is correct by construction (defense-in-depth, parallel to filewrite's pattern); the detector exists to catch the real case in long-running production.

Notes

  • The event struct is lsm_audit_event to avoid colliding with the kernel's own enum lsm_event in vmlinux.h.
  • file_open records the basename only (full path is future work — bpf_d_path is allowlist-rejected for lsm/file_open).
  • Other probes likely share the same path-tail-residue pattern (filewrite's buf[16384]). Out of scope for this PR; tracked as a separate follow-up.

Independent of #661#668.

jfeng18 and others added 5 commits June 6, 2026 20:56
Add an observe-only BPF LSM program that audits two security-relevant
actions for traced Agent families, capturing what the existing probes miss:

  - lsm/socket_connect: every outbound connection attempt (destination
    IP:port), regardless of protocol or port -- sslsniff/tcpsniff/udpdns
    only see TLS / specific ports / DNS.
  - lsm/file_open: every file opened (basename + access mode), deduped
    per (pid, inode) via an LRU map so a chatty Agent cannot flood the
    shared ring buffer.

Both hooks filter to traced families via is_pid_traced and emit through the
shared ring buffer (EVENT_SOURCE_LSM). They are strictly observe-only: every
program returns the incoming `ret` unchanged, so no operation is ever denied
(the LSM attach point leaves room for future enforcement).

The event struct is named lsm_audit_event to avoid colliding with the
kernel's own `enum lsm_event` in vmlinux.h. file_open records the basename
(bpf_d_path is allowlist-rejected for lsm/file_open); full path is future work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parse raw lsm_audit_event ring-buffer records into a typed LsmEvent
(Connect { dst_ip, dport, ... } / FileOpen { path, open_flags, ... }),
decoding the destination address by family and the port from network byte
order. LsmAudit::new_with_maps reuses the shared traced_processes and ring
buffer maps and attaches both lsm/ programs; bpf_lsm_available() gates the
probe on `bpf` being in the active LSM list. Adds Event::Lsm and its parser
arm.

Unit tests cover IPv4/IPv6 decode, port byte-order, file_open parsing, and
short/unknown-kind inputs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dispatch EVENT_SOURCE_LSM records to LsmEvent in the shared ring-buffer
poller and create/attach the probe (opt-in, skipped with a warning when BPF
LSM is inactive). Surface each audit event as a structured log line in the
event loop:

  [LSM] connect pid=... comm=... dst=IP:PORT
  [LSM] open    pid=... comm=... mode=r|w|rw path=...

Add the --enable-lsm-audit CLI flag and the matching config field (default
off; follows the enable_filewatch plumbing).

Verified on kernel 6.6.102 (BPF LSM active): both hooks attach; a traced
agent's outbound connects are captured with correct IP:port while all 10
connects still succeed (observe-only, nothing denied); file opens are deduped
to one event per file; events are emitted only for traced PIDs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three correctness fixes in the LSM audit probe, all caught by an adversarial
review of the as-pushed branch:

1. **path[] info leak** — `bpf_ringbuf_reserve` returns a slot that may carry
   residue from a previously-submitted record on the shared ring buffer
   (sslsniff TLS plaintext, filewrite content). The original code only zeroed
   `path[0]`, leaving `path[1..256]` uninitialized after `bpf_probe_read_kernel_str`
   wrote `basename + nul + ?`. Memset the whole record immediately after
   reserve in both hooks, drop the now-redundant per-field zeros.

2. **file_open dedup-before-emit** — the `(pid, inode)` LRU entry was inserted
   BEFORE reserve / `bpf_probe_read_kernel_str`, so any reserve failure or
   NULL/short read permanently suppressed that file for the process until LRU
   eviction. Move the dedup `bpf_map_update_elem` to after a successful submit;
   `bpf_ringbuf_discard` the slot on path-read failure so a future open still
   gets a chance.

3. **socket_connect addrlen unchecked** — LSM fires before the protocol-level
   length check, so `sin_port`/`sin_addr` can be uninitialized stack residue
   if `addrlen < sizeof(struct sockaddr_in)`. Drop the event in that case
   rather than emit garbage destination fields.

Also adds an always-on Rust-side leak detector: `LsmEvent::from_bytes` scans
`path[]` past the null terminator and `log::warn!`s if any byte is non-zero —
a permanent regression net for the producer-side memset invariant. Two unit
tests (positive: garbage tail triggers detector and decoder still extracts the
correct path; negative: clean tail stays silent).

Verified: cargo build, cargo test (8/8 lsmaudit + 447/447 lib), ECS post-fix
sanity (kernel 6.6.102, 173 LSM events, zero leak warnings, paths clean).

Note on E2E differential signal: the leak class (residue from ringbuf wrap)
cannot be reliably triggered in a synthetic test — the 32 MiB ring rarely
wraps in seconds. A reverse-test with the memset removed produced 158 events
and 0 leak warnings on the synthetic load. The detector remains in
production as the regression net for real long-running deployments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EVENT_SOURCE_SCHED=7 (from alibaba#662 schedmon) would collide with
EVENT_SOURCE_LSM=7 on merge. Renumber LSM to 8.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jfeng18 jfeng18 marked this pull request as draft June 10, 2026 08:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component:sight src/agentsight/

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant