> your AI agent picks dependencies from memory; give it dated facts — try starlog.dev ↗ vet your agent's deps ↗ vibe-coding is fine. vibe-importing isn’t. — try starlog.dev ↗ vibe-importing isn’t fine ↗ your agent has never seen your private packages — try starlog.dev ↗ facts for private packages ↗ a linter for the dependencies your AI agent picks — try starlog.dev ↗ a linter for agent deps ↗ whois is redacted, cdns mask the rest — get the real operator — try whoisgeni.us ↗ who really runs that domain ↗ domain attribution that shows its work — full evidence chain — try whoisgeni.us ↗ domain intel w/ evidence ↗

← Back to Articles

Sandlock: Building AI Agent Sandboxes with Seccomp User Notifications Instead of Containers

[ View on GitHub ]

Sandlock: Building AI Agent Sandboxes with Seccomp User Notifications Instead of Containers

Hook

Every restricted syscall in a sandboxed process can pause execution and ask a supervisor process for permission—and that supervisor can lie about what it approved. This is how Sandlock turns the Linux kernel into a security policy engine without root access.

Context

AI agents that execute arbitrary code present a unique sandboxing challenge. Traditional containers take 50-100ms to start, require root or user namespaces, and provide coarse network controls that can't distinguish between 'POST to OpenAI API' and 'GET internal credentials endpoint'. When an LLM hallucinates a command or gets prompt-injected into exfiltrating data, you need microsecond-latency isolation with HTTP-aware filtering—not a full VM.

Sandlock emerged from this gap. Existing solutions either sacrifice startup time (Firecracker's 100ms), require privilege (Docker needs root or complex /etc/subuid setups), or lack protocol-level filtering (gVisor blocks IPs, not HTTP paths). For FaaS platforms running thousands of untrusted code snippets per second, or AI agent frameworks executing tool calls, neither containers nor VMs fit. Sandlock's thesis: combine Linux 5.6's seccomp user notifications with Landlock LSM to create sub-millisecond sandboxes that intercept syscalls in userspace and enforce HTTP access control lists—all without containers or privilege.

Technical Insight

Policy Enforcement

Confinement Layer

configure policy

fork + apply seccomp

monitor via pidfd

syscall attempt

SECCOMP_RET_USER_NOTIF

policy decision

resume/deny

HTTP connect

MITM + filter

validated request

file write

capture to overlay

Application Code

Sandbox Controller

Confined Child Process

Supervisor Process

Linux Kernel

HTTP Proxy Layer

Overlay Filesystem

External API

Write Tracking

System architecture — auto-generated

Sandlock's architecture revolves around seccomp user notifications, a kernel feature that lets a supervisor process intercept and mediate syscalls from a confined child. When you mark a syscall with SECCOMP_RET_USER_NOTIF, the kernel suspends the calling process and delivers a notification to a supervisor via a file descriptor. The supervisor inspects arguments, enforces policy, and either allows the call, denies it, or provides a fake return value.

Here's the basic confinement flow in Rust:

use sandlock::{Sandbox, SandboxConfig, HttpPolicy};

let config = SandboxConfig::new()
    .memory_limit(100 * 1024 * 1024) // 100MB
    .cpu_time_limit(5000) // 5s
    .filesystem_readonly("/usr")
    .filesystem_readwrite("/tmp/workspace")
    .http_policy(HttpPolicy::new()
        .allow_method("POST", "api.openai.com", "/v1/chat/*")
        .deny_all());

let sandbox = Sandbox::create(config)?;
let result = sandbox.execute(&["python3", "agent.py"])?;

Under the hood, Sandlock installs a seccomp filter before exec that marks mmap, brk, connect, and file access syscalls for notification. When the Python process attempts socket.connect(), the kernel freezes it and notifies the supervisor. The supervisor checks the destination against the HTTP policy: if it's api.openai.com:443, it allows the connection but installs a transparent proxy in the middle. This proxy performs TLS MITM, inspects the HTTP method and path, and only forwards requests matching the allowlist.

The HTTP interception layer is where Sandlock diverges from traditional sandboxes. Most tools give you iptables-style rules: allow traffic to 1.2.3.4:443 or block it. Sandlock operates at HTTP semantics:

HttpPolicy::new()
    .allow_method("GET", "*.wikipedia.org", "/*")
    .allow_method("POST", "api.anthropic.com", "/v1/messages")
    .deny_method("*", "169.254.169.254", "/*") // Block AWS metadata
    .deny_all()

This matters for AI agents because prompt injection attacks often try to make the LLM request internal URLs or use unexpected HTTP methods. A policy that says "only POST to /v1/chat" defeats exfiltration via GET /admin/users.

The filesystem layer combines Landlock LSM with copy-on-write tracking. Landlock provides mandatory access control without root—you call landlock_restrict_self() to irreversibly confine the process to specific paths. Sandlock extends this with a COW layer that tracks writes:

let config = SandboxConfig::new()
    .filesystem_readonly("/app")
    .filesystem_cow("/data"); // Writes go to tmpfs overlay

let sandbox = Sandbox::create(config)?;
sandbox.execute(&["./process_data"])?;

// On failure, writes to /data are discarded
// On success, promote writes to real filesystem
if result.exit_code == 0 {
    sandbox.commit_writes()?;
}

This is implemented with bind mounts and MS_PRIVATE propagation rather than overlayfs. The supervisor creates a tmpfs, bind-mounts the original directory read-only, then overlays a writable tmpfs on top. All writes hit the tmpfs. On commit, it copies modified files back. This avoids overlayfs kernel requirements and makes cleanup deterministic—just unmount the tmpfs.

The resource limiting is clever but fragile. Since Sandlock can't use cgroups without root, it intercepts memory allocation syscalls (mmap, brk) and maintains a running tally. When the process exceeds quota, the supervisor returns ENOMEM to the syscall:

// In supervisor's seccomp notification handler
fn handle_mmap_request(&mut self, req: &SeccompNotif) -> SeccompResponse {
    let size = req.args[1]; // mmap's length parameter
    if self.current_memory + size > self.config.memory_limit {
        return SeccompResponse::Error(libc::ENOMEM);
    }
    self.current_memory += size;
    SeccompResponse::Allow
}

This works until the process tries to allocate 1GB at once—the supervisor sees the syscall after the kernel already validated arguments but before allocating memory. There's a TOCTOU window, though smaller than ptrace-based approaches.

The Landlock dependency is aggressive. Sandlock requires ABI v6 (Linux 6.12+, released November 2024) for IPC scoping. Most sandboxes gracefully degrade; Sandlock refuses to run. The reasoning: older Landlock versions can't restrict Unix domain sockets, so a confined process could connect to systemd or D-Bus and escape. This kills compatibility but provides honest security boundaries.

Gotcha

The Linux 6.12 kernel requirement is a deployment showstopper. Ubuntu 24.04 LTS ships kernel 6.8. RHEL 9 is on 5.14. Unless you're running Arch or compiling custom kernels, you're waiting until 2025-2026 for distribution support. The README mentions graceful degradation for older Landlock ABIs, but IPC isolation—the feature that prevents D-Bus escapes—only works on 6.12+. You can run Sandlock on older kernels, but you're trusting confinement that the authors explicitly warn is incomplete.

The seccomp notification performance model is unproven at scale. Every intercepted syscall requires a context switch to the supervisor, a policy decision, and a context switch back. The benchmarks show 97% Redis performance compared to native, but Redis is mostly network I/O and memory reads—not syscall-heavy. Try sandboxing a build system that forks 10,000 times or a file scanner that stats 100,000 inodes. Each fork, mmap, and open syscall blocks on supervisor notification. The authors cite 530μs fork times, but that's in ideal conditions. Under load with hundreds of sandboxes, the supervisor becomes a serialization bottleneck.

HTTP MITM breaks certificate pinning and mutual TLS. The transparent proxy has to terminate TLS, inspect the HTTP layer, then re-encrypt to the real destination. Any application doing certificate pinning (many security-conscious APIs) will reject the supervisor's certificate. The docs suggest installing the supervisor's CA cert in the sandbox, but that requires modifying the container image or language-specific trust stores—friction that defeats 'no privilege' claims.

Verdict

Use Sandlock if you're building an AI agent platform or FaaS system on bleeding-edge Linux (6.12+) and need microsecond startup with HTTP-aware filtering that containers can't provide. The seccomp notification model is genuinely novel for policy enforcement, and the HTTP ACL layer solves real prompt injection scenarios. It's ideal for controlled environments where you ship the kernel (custom AMIs, NixOS deployments) and workloads that aren't syscall-intensive. Skip it if you're on enterprise Linux with multi-year kernel support cycles, need defense against kernel exploits (shared kernel = shared fate), or run workloads with certificate pinning or abstract Unix sockets. For production multi-tenant isolation, gVisor's overhead is worth the security. For compatibility, Bubblewrap works on ancient kernels. Sandlock occupies a niche: maximum performance on minimum infrastructure, if you control the kernel and trust the LSM boundary.