For each item below, we explain the technical limitation clearly so that readers understand not just what is missing, but why it cannot be trivially added.
- Why it's hard: eBPF has a 512-byte stack limit, bounded loops, and a ~1M instruction limit. SHA-256 requires 64 rounds per 64-byte block. A 1MB binary = ~16,000 blocks. The eBPF verifier will reject this for large binaries.
- The alternative
bpf_ima_file_hash()requires Linux 5.18+ andCONFIG_IMA=y-- not enabled by default on Ubuntu, Fedora, or most distros. - Userspace hashing has a TOCTOU race (binary swapped between hash and exec).
- This is Veto's core innovation -- they solved the kernel-side hashing problem with significant eBPF expertise.
- Verdict: Not implementable without major kernel config requirements or novel eBPF techniques.
- Would make Guardian immune to ALL path tricks including symlinks.
- Challenge 1: Inode numbers are reused after deletion.
(dev, ino)pairs are needed. - Challenge 2: Editors (vim,
sed -i) create new files with new inodes -- deny rules break. - Challenge 3: Cross-filesystem inodes are not unique without the device number.
- Challenge 4: Need inotify watchers + invalidation logic in userspace.
- Challenge 5: LSM hook needs
bpf_d_path()(Linux 5.11+) or directstruct inodefield reads. - Verdict: Partially feasible but semantic complexity makes it fragile.
- Proper fix requires
bpf_d_path()(Linux 5.11+) in the LSM hook. - Currently the LSM hook only checks
PENDING_DENY-- it does not read the file struct at all. - Adding path reading to LSM means moving policy evaluation INTO the LSM context.
- The 512-byte stack makes path comparison in LSM context very difficult.
- This would require fundamental rearchitecting of the enforcement flow.
- Verdict: Requires rearchitecting enforcement -- no longer tracepoint-decides, LSM-enforces.
- LSM
socket_connecthook could block connections. - But requires reading
sockaddrfrom LSM context (different from tracepoint context). - Aya-ebpf supports this but it adds eBPF verifier complexity.
- Port-based blocking in eBPF is feasible; IP-range blocking needs BPF map design.
- DNS resolution happens before connect, so domain-based rules need DNS monitoring.
- Verdict: Feasible but complex; deferred to future phase. Monitoring implemented now.
io_uringoperations bypass syscall tracepoints entirely.- LSM hooks still fire for
io_uring(sofile_openenforcement works). - But tracepoint-based monitoring misses
io_uringoperations. - Full fix requires seccomp BPF to block
io_uring_setuporio_uring_entersyscalls. - Verdict: Partially mitigated (LSM still fires); full fix needs seccomp.
Guardian Shell uses a tracepoint + LSM two-phase enforcement pattern:
- Tracepoint captures syscall arguments (easy path reading).
- Tracepoint evaluates policy and sets
PENDING_DENYflag. - LSM hook checks
PENDING_DENYand blocks access.
-
Separation of concerns: Tracepoints handle data capture and policy evaluation; LSM handles enforcement. This avoids complex path reading in LSM context.
-
Per-agent policy: Cgroup-based identity enables different policies for different agents. Veto and most eBPF tools are system-wide.
-
Rich observability stack: Dashboard, SSE, alerting, Prometheus. Most eBPF security tools are CLI-only.
-
Interactive approval workflow: Permission requests with risk classification. Unique among eBPF security tools.
-
Single binary deployment: Templates compiled in, static files embedded. No external dependencies.
-
Graceful degradation: If LSM unavailable, falls back to monitor-only. If
openat2/openunavailable, continues with what is available. -
Composable policy layers: File access + exec + network + permissions + rate limiting all stack.
-
Path-based matching is fundamentally fragile: Symlinks, bind mounts, hard links, procfs tricks all create alternative paths to the same file. Normalization helps but cannot be complete.
-
Tracepoint-LSM timing dependency: The enforcement model assumes the tracepoint fires before the LSM hook in the same syscall. This is true on x86_64 but is technically a kernel implementation detail, not a guarantee.
-
No per-agent BPF maps: Policy maps (
DENY_EXACT,ALLOW_PREFIXES, etc.) are shared across all agents. This means deny rules from one agent affect all agents. True per-agent isolation would require per-agent map instances or encoding agent identity into map keys. -
String matching in kernel space: LPM trie matching works for prefix patterns, but complex glob matching (single-level wildcards) happens in userspace only. Kernel policy is a subset of userspace policy.
-
Raw syscall argument capture: Tracepoints see the path the application passes to the syscall, not the canonical path the kernel resolves. This is the root cause of most bypass vectors.
-
No atomic policy updates: BPF maps are updated entry-by-entry. During a policy reload, there is a window where the map is partially old and partially new.
Approach: Move ALL policy evaluation into LSM hooks. Use bpf_d_path() to get canonical paths.
Pros:
- Canonical paths -- immune to symlinks,
/proctricks, all path manipulation. - Atomic enforcement point -- decision and enforcement in the same context.
- No
PENDINGmap race conditions. - Can read
struct file,struct inode, get device/inode numbers.
Cons:
- Requires Linux 5.11+ for
bpf_d_path(). - 512-byte stack limit makes path comparison very difficult in LSM context.
- Would need to restructure policy evaluation to work within eBPF constraints.
- Loses easy access to syscall arguments (no filename from
openatargs). - More complex eBPF programs = harder to pass verifier.
When to consider: If symlink/path bypass attacks become the primary threat. Requires committing to Linux 5.11+ as minimum kernel version.
Approach: Use seccomp-BPF filters to restrict syscalls available to agents.
Pros:
- Simple, well-supported by all Linux kernels (3.5+).
- Can block entire syscall classes (
io_uring_enter,ptrace, etc.). - Inherited by child processes (like cgroups).
- No
CONFIG_BPF_LSMrequirement.
Cons:
- Cannot inspect file paths (operates on syscall numbers and arguments).
- Cannot make content-aware decisions.
- Binary allow/deny at syscall level -- no per-path granularity.
- Would need to be combined with other approaches for file-level control.
When to consider: As a defense-in-depth layer on top of existing architecture. Block dangerous syscalls (io_uring_enter, ptrace, etc.) while keeping current file/exec enforcement.
Approach: Combine all three mechanisms:
- Seccomp: Block dangerous syscalls (
io_uring,ptrace,personality). - LSM
file_openwithbpf_d_path(): Canonical path enforcement (Linux 5.11+). - Tracepoints: Rich event capture for monitoring and alerting.
- Current architecture for kernels < 5.11.
Pros:
- Defense in depth -- multiple enforcement layers.
- Canonical path resolution where available.
- Backward compatibility with older kernels.
- Covers
io_uringand other tracepoint bypasses.
Cons:
- Most complex implementation.
- Multiple code paths based on kernel version.
- Harder to test and reason about.
- Higher maintenance burden.
When to consider: This is likely the long-term target architecture. Can be incrementally adopted.
Approach: Run each agent in its own Linux namespace (mount, network, PID, user) with a whitelist filesystem overlay.
Pros:
- Kernel-enforced isolation at the namespace level.
- Mount namespace: agent only sees allowed paths.
- Network namespace: completely isolated network stack.
- No BPF/eBPF required -- works on any Linux kernel.
- Well-understood security model (OCI containers).
Cons:
- Heavy-weight: each agent needs its own filesystem view.
- No fine-grained per-file policy -- it is all-or-nothing per mount.
- No interactive permission requests (cannot dynamically change mounts).
- Loses the rich monitoring and alerting capabilities.
- Not suitable for agents that need to interact with host filesystem.
When to consider: For high-security deployments where agents have clearly defined, static resource needs. Not suitable for interactive development agents.
Veto uses a single LSM hook (bprm_check_security) with kernel-side SHA-256 hashing. Binary identity is its content hash, not its path.
- Immune to all path manipulation: Hash does not change regardless of path.
- Pre-execution blocking: Binary never runs a single instruction.
- Simple, auditable policy: List of hashes, no complex patterns.
- No timing dependencies: Single enforcement point, no
PENDINGmaps.
- File access control: Veto cannot restrict which files a process reads/writes.
- Per-agent policy: Different agents get different rules.
- Interactive approval: Permission requests with risk-based UI.
- Rich observability: Dashboard, alerting, Prometheus, SSE.
- Interpreted code: Can block script file reads; Veto is blind to Python/Bash.
- Network monitoring: Can see outbound connections; Veto cannot.
- Dynamic policy: Temporary grants, hot reload; Veto is static.
Veto and Guardian Shell solve fundamentally different problems:
- Veto: "Which binaries can run?" (execution control)
- Guardian Shell: "What can this agent access?" (resource control)
Veto's architecture is BETTER for binary execution control. Guardian Shell's architecture is BETTER for resource access control and interactive workflows. They are complementary, not competing.
The ideal deployment uses BOTH:
- Veto blocks unauthorized binary execution.
- Guardian Shell controls file access, monitors network, provides interactive approval.
- File enforcement via
PENDING_DENYpattern. - Exec enforcement via
PENDING_EXEC_DENYpattern. - Network monitoring via tracepoint.
- Path normalization for common bypasses.
- Add seccomp-BPF filters to
guardian-launch. - Block:
io_uring_enter,io_uring_setup,ptrace,personality. - Block:
process_vm_readv,process_vm_writev(cross-process memory). - This eliminates the
io_uringbypass and several escalation vectors. - Effort: Low-medium. seccomp is well-documented and supported.
- For kernels 5.11+: Move file policy evaluation into LSM hook using
bpf_d_path(). - Eliminates symlink and all path manipulation bypasses.
- Keep tracepoint for monitoring/alerting (rich event data).
- Keep current tracepoint +
PENDINGpattern as fallback for older kernels. - Effort: High. Requires restructuring eBPF enforcement logic.
- Combine all three: seccomp + LSM with
bpf_d_path+ tracepoint monitoring. - Inode-based deny map for critical files (using
bpf_d_path-resolved inodes). - Content hashing via
bpf_ima_file_hashfor critical binary verification (where IMA available). - Network enforcement via LSM
socket_connect. - Full defense-in-depth stack.
| Feature | Current Architecture | Full LSM | Seccomp + Current | Hybrid (Recommended) |
|---|---|---|---|---|
| File enforcement | Path-based | Canonical path | Path-based | Canonical path |
| Exec enforcement | Phase 7 | Yes | Yes | Yes |
| Network enforcement | Monitor only | Possible | Monitor only | Yes (LSM socket_connect) |
| Symlink immunity | No | Yes (bpf_d_path) |
No | Yes |
| io_uring protection | Partial (LSM) | Partial (LSM) | Yes (seccomp blocks) | Yes |
| Binary hash verification | No | No | No | Partial (IMA) |
| Kernel version requirement | 5.2+ | 5.11+ | 5.2+ | 5.11+ (degraded 5.2+) |
| Per-agent policy | Yes | Yes | Yes | Yes |
| Interactive approval | Yes | Yes | Yes | Yes |
| Implementation complexity | Medium | High | Medium | Very High |
Bottom line: The current architecture is sound for its scope. The weaknesses (path-based matching, no symlink resolution) are inherent to the tracepoint approach and can only be fully fixed by adding LSM bpf_d_path() enforcement (Architecture C). The recommended path is incremental: add seccomp first (quick win), then bpf_d_path LSM (when ready to commit to 5.11+).