Runtime Monitor Reference¶
Status: Production Ready (Linux / BPF LSM)
Assay's Runtime Monitor provides kernel-level enforcement for MCP security policies. Unlike traditional tracepoints which are detect-only and vulnerable to TOCTOU (Time-of-Check Time-of-Use) attacks, Assay uses BPF LSM to block unauthorized operations before they occur.
1. Architecture¶
The monitor bridges kernel space and user space using a producer-consumer model over a high-performance BPF Ring Buffer.
flowchart TD
subgraph KS["Kernel Space (eBPF)"]
LSM["LSM Hooks (file_open)"] -->|Match| BLK["Block (-EPERM)"]
LSM -->|Log| RB["RingBuf Map"]
SOCK["Socket Hooks (connect)"] -->|Match| RB
end
subgraph US["User Space (Rust)"]
RB -->|Poll| L["LinuxMonitor (assay-monitor)"]
L -->|EventStream| CLI["Assay CLI (monitor)"]
end Key Components¶
assay-ebpf: Native BPF programs. Implements prefix/exact path matching and CIDR-based network blocking.assay-monitor: Orchestrates BPF lifecycle. Implements RAII Link Persistence to ensure programs remain attached.assay-xtask: Unified build automation. Supports building eBPF via a dedicated Docker toolchain.
2. Technical Capabilities¶
LSM File Prevention¶
Assay hooks the file_open LSM gate. It allows or denies access based on: - SOTA Inode Resolution: Resolves paths to (dev, ino) pairs securely using open(O_PATH | O_NOFOLLOW) to prevent TOCTOU/symlink attacks. - Exact Path Matches: High-performance hash-based lookup for files like /var/lib/private-demo/credentials.txt. - Cgroup Scoping: Automatically monitors only the processes within the target MCP sandbox.
Network Egress Control¶
Uses the Cgroup connect4 hook to enforce IPv4/TCP connect() rules: - Port Blocklists: Block SSH, Telnet, or internal databases. - IPv4 CIDR deny rules: Block matching destination ranges. - IPv4 CIDR allow exceptions: Exempt more-specific ranges from a broader deny; an allow CIDR alone does not restrict unmatched traffic.
IPv6 CIDR policies are refused before any rule map is changed; they are not silently reduced to their IPv4 subset. connect6_hook is compiled-but-unattached enforcement (Unsupported); it is not the connect observer. sys_enter_connect is always attached and handles AF_INET6. That tracepoint can observe AF_INET6 connect syscalls; it is not IPv6 enforcement. IPv6 CIDR enforcement, UDP/QUIC, DNS resolution, already-open sockets, raw sockets, and proxy/tunnel identity remain outside the enforcement claim tracked in issue #1576.
Measured IORING_OP_CONNECT (one host)¶
This section is the canonical public record of the measured cell. Other pages link here rather than restating host, kernel, SHA, or ports.
On one measured host — assay-bpf-runner, Ubuntu 24.04, kernel 6.8.0-137-generic, aarch64, Assay checkout 886ebce908401cb0a49502e7c7515f85fc9ceebd — raw io_uring_setup / io_uring_enter with IORING_OP_CONNECT to 127.0.0.1:9101:
- produced no
sys_enter_connectevent (the syscall tracepoint was blind to that opcode on this kernel); - was observed by attached cgroup
connect4when IPv4/TCP network enforcement was requested and:9101was allowed (cqe_res=0; the listener accepted;observed_peerscontained127.0.0.1:9101; socket counters incremented); - was blocked by that same connect4 path when
:9101was denied (cqe_res=-EPERM; the listener accepted nothing;blocked_port=1; enforcementblocked_count=1).
Syscall connect to :9102 remained visible to sys_enter_connect in the audit-only cell, and was blocked by connect4 when :9102 was denied. UDP connect then sendmsg to :9103 produced peer evidence from the connect-time connect4 hook only; that row is not send-probe evidence.
This cell does not claim that every io_uring connect is blocked, that other kernels or architectures match, or that io_uring SEND / SENDMSG is observed. Ring-buffer drops were reported as 0 in every discriminator cell of that historical measurement; that broader zero-drop statement is recorded as reported, not re-proven here. Shutdown was the monitor's internal --duration.
The object loaded for a monitor run is ./target/assay-ebpf.o, produced by cargo xtask build-ebpf --release --no-docker. That object's digest was not retained, so checkout-to-object binding is a non-claim.
On this host, sys_enter_connect did not see IORING_OP_CONNECT; attached cgroup connect4 did. Those surfaces must not be collapsed. Coverage-label derivation (Runner counts vs CLI attach) is not restated here; see coverage-descriptor-v0.
observed_peers is diagnostic¶
--observed-peers writes assay.monitor.observed_peers.v0. Peers are distinct destinations decoded from cgroup connect events (EVENT_CONNECT_OBSERVED / EVENT_CONNECT_BLOCKED), not from the sys_enter_connect tracepoint. The set is diagnostic and not exhaustive: connect-time only, empty when connect4 is not attached, and not a complete peer inventory. It does not prove UDP/QUIC identity, already-open sockets, or every destination the process reached.
One unpredictable run_id is created for each assay monitor invocation and shared by the requested --observed-peers and --observation-health artifacts. This lets a consumer refuse a crossed pair from different invocations. The value is only a correlation handle: it is not content identity, artifact authenticity, cryptographic provenance, or proof that the files were not copied after production.
Before monitoring starts, requested output names are cleared and exclusively reserved. Aliases that resolve to one filesystem target are refused rather than allowing one artifact to overwrite the other.
3. Developer Workflow¶
Environment Setup¶
eBPF development requires a specific toolchain (LLVM, nightly Rust, bpf-linker). The delegated runner path uses native bpf-linker builds so cold Docker image builds stay out of the proof hot path:
# 1. Install the native eBPF toolchain
rustup toolchain install nightly-2026-01-01 --profile minimal
rustup component add rust-src --toolchain nightly-2026-01-01
rustup run nightly-2026-01-01 cargo install bpf-linker --version 0.10.3 --locked
# 2. Compile eBPF bytecode
cargo xtask build-ebpf --release --no-docker
Docker remains available as a fallback for machines that cannot host the native toolchain: cargo xtask build-image && cargo xtask build-ebpf --docker.
Verification¶
Local verification is best done via Lima VM on macOS or directly on Linux:
4. Production Deployment¶
The monitor requires CAP_BPF and CAP_PERFMON (or sudo).
# Run monitor with a specific policy
sudo assay monitor --ebpf ./target/assay-ebpf.o --policy policy.yaml
Operator Output¶
Blocked-file denials are rendered as structured fields so operators can correlate the kernel event with the exact deny rule:
At the end of a run, assay monitor also prints a summary of emitted and dropped ring-buffer events for the tracepoint, LSM, and socket paths. If any drop counter is non-zero, the CLI prints a Ring buffer pressure detected warning so an operator can distinguish "no events" from "events were dropped under load".
[!IMPORTANT] Ensure your kernel is booted with
lsm=...,bpfin the command line parameters to enable BPF LSM support.
5. Probe inventory and non-claims¶
The release eBPF object compiles 11 programs (PROBE_PROGRAMS in assay-monitor). EXPECTED_PROBES lists the 7 Always-class surfaces (sys_enter_openat, sys_enter_openat2, sys_exit_openat, sys_exit_openat2, sys_enter_connect, sys_enter_fork, lsm:file_open). The other four are mode-aware:
| ELF program | Surface | Present inventory |
|---|---|---|
connect4_hook | cgroup_sock_addr:connect4 | Requested only with a network policy; otherwise not_requested |
connect6_hook | cgroup_sock_addr:connect6 | Compiled-but-unattached enforcement (Unsupported / AttachSpec::None) |
assay_monitor_sendto | sys_enter_sendto | Always attempted (AlwaysAttempted) as syscalls/sys_enter_sendto; mode-aware terminal status |
assay_monitor_sendmsg | sys_enter_sendmsg | Always attempted (AlwaysAttempted) as syscalls/sys_enter_sendmsg; mode-aware terminal status |
Send attach is attempted unconditionally. Terminal outcomes are attached / failed / unavailable / unsupported. #2350 records that Unsupported classification is not reliable.
S1b (#2345, now on main as 7816e3c4) proved a bounded live matrix: explicit IPv4 sendto/sendmsg endpoint observation plus receiver effects; no_peer / non_ip counters; zero TRACEPOINT ring-buffer drops in that cell; an attach-disabled negative; and controlled shutdown. That matrix is not an io_uring SEND / SENDMSG result, not an exhaustive peer set, and not a multi-kernel, IPv6, QUIC, DNS, raw-socket, or tunnel claim.
Mode-aware outcomes are distinct: not_requested ≠ unavailable ≠ failed ≠ unsupported ≠ attached (#2339). The CONFIG-ABI gate (#2340) and exact compiled program-set gate (#2341) prove object/loader agreement. They do not prove runtime attach success, event completeness, or enforcement.
Non-claims for this page:
- one measured kernel and architecture (
6.8.0-137-generic/ aarch64); no kernel-version generality; - no SQPOLL submitter attribution;
- no io_uring
SEND/SENDMSGresult; - no IPv6 enforcement (
connect6_hookremains unattached;sys_enter_connectobservingAF_INET6is not an enforcement claim); - no UDP/QUIC/DNS/already-open-socket/raw-socket/proxy-tunnel identity claim;
observed_peersis diagnostic, not an exhaustive peer set;- program-set and CONFIG-ABI gates ≠ attach completeness;
- no scalar trust score, no "complete egress", no certification (ADR-042);
- checkout-to-object binding:
./target/assay-ebpf.odigest was not retained; - monitor artifact
run_idequality is correlation, not authenticity or cryptographic provenance.