An AI coding agent runs code before any human has reviewed it: shell commands, generated scripts, pip and npm installs, tool plugins. It can be steered by prompt injection hidden in a web page or a dependency, and even a well-meaning agent can rm -rf the wrong tree. AI review agents exist, but they mostly read the diff after the code has already run, and they are LLMs that the same prompt injection can fool, so they complement a sandbox rather than replace it. So what does a sandbox for that code actually have to do on Linux, and why is “minimal” so hard?
A Linux sandbox is a stack of partial mechanisms that all share one host kernel. Each layer stops one class of thing and nothing else, and there is no single switch. The strongest boundary is a second kernel, which means a virtual machine. That is why the serious hosted products are microVMs, and why the OS-level tools (Landlock, seccomp, bubblewrap) are best kept for code you mostly trust on a machine you control.
The mechanisms involved, in the order a launcher typically applies them. They are not ranked: each answers a different question.
| Layer | Question it answers | What it does not stop |
|---|---|---|
| cgroup v2 | how much? | anything within quota; not a security boundary |
| user namespace | who am I? | kernel bugs reachable by unprivileged users |
| pid/net/ipc/uts namespaces | what can I see? | the shared kernel behind every view |
mount namespace + pivot_root |
what filesystem? | files under the roots you granted |
capabilities + no_new_privs |
can I re-privilege? | syscalls that need no privilege |
| Landlock | which paths, TCP/UDP ports, unix sockets, signals? | anything outside the rights it handles; most syscalls |
| seccomp-bpf | which syscalls? | anything an allowed syscall can reach |
Everything here is point-in-time (late October 2026). Details of products and papers move monthly, so verify specifics before relying on them.
📚 One shared kernel is the whole problem. Namespaces, cgroups, capabilities, seccomp and Landlock all run in the host kernel. A single kernel vulnerability reachable from the sandbox is a full escape. The Linux syscall surface is enormous (hundreds of syscalls, each with many argument shapes), and features like io_uring add surface. Enabling unprivileged user namespaces, which you need to sandbox without root, itself exposes more kernel code to untrusted callers and has been a recurring source of local privilege-escalation CVEs.
🎮 Agents need capability, which fights confinement. A file-only agent is easy to box. A useful coding agent wants to install packages (network), spawn compilers and test runners (exec and many syscalls), read large parts of the filesystem, and sometimes use GPUs. Every capability you grant is attack surface you re-open. The art is least privilege per task, which is exactly what is hard to specify up front.
🧱 Network egress is the real prize, and the hardest to get right. The damaging outcomes are network actions: exfiltrating secrets, reaching internal services, hitting the cloud metadata endpoint (169.254.169.254), pulling a malicious payload. A binary on/off switch (blocking AF_INET sockets in seccomp) is too coarse for an agent that must reach a package registry. A filtering proxy is the right tool, but it needs TLS interception or a domain allow-list, plus care that the agent can’t just talk to localhost services or use DNS as a covert channel.
🗒️ Secrets and ambient authority leak in surprising ways. Environment variables, inherited file descriptors, cloud instance-metadata credentials, ~/.aws, ~/.ssh, a mounted Docker socket, a GITHUB_TOKEN in the environment: none of these are “files the agent opened”, so a filesystem sandbox alone misses them. A 2026 security note on AWS Bedrock AgentCore argued that its code-interpreter microVM still exposes execution-role credentials via the metadata service; AWS considers that by design. Whoever is right, the lesson stands: isolation of the compute does not imply isolation of the credentials.
🐳 Views leak the host. Inside a container, /proc/cpuinfo, /proc/meminfo and /proc/stat show the host’s numbers unless something virtualises them (lxcfs). That is the same reason top in a container shows host CPU and steal time. Information disclosure is a weaker threat than escape, but it feeds fingerprinting and side channels.
📦 Correctness is a sequencing minefield. The layers have prerequisite chains and failure modes. no_new_privs must be set before an unprivileged seccomp filter. A gid map needs setgroups=deny. A bind mount can’t go read-only in one step on older kernels. Landlock rights must be masked to the running ABI or restrict_self fails. Seccomp goes in last, so your own setup syscalls aren’t filtered. Each is a place to silently under-confine, and a half-applied sandbox is worse than none, because you think you’re protected. A good sandbox aborts on any layer that fails rather than running the agent half-confined.
🐧 Deny-lists have traps. A syscall deny-list is deceptively easy to under-build:
openat operations as ring operations, without issuing those syscalls, so a naive seccomp network block is sidestepped by an agent that opens a ring and connects through it. Any deny-list that ignores io_uring is porous.AUDIT_ARCH_X86_64 token but OR 0x40000000 into the number. A filter that only compares plain numbers lets the x32 variant of every denied call through.ioctl(TIOCSTI). A process sharing the controlling terminal can push bytes into the terminal’s input queue, and the parent shell runs them after the sandbox exits. That is a full escape that touches no file and opens no socket. Since Linux 6.2 the host can switch it off with sysctl dev.tty.legacy_tiocsti=0, and several distributions ship it off, but a sandbox should still call setsid() and filter the ioctl itself.i386) calling convention uses different numbers for different calls, so a filter must check the architecture first.Each hole is invisible if you reason about syscalls by name. This is why serious sandboxes invert to deny-by-default and lean on audited, maintained profiles instead of a hand-rolled list.
📝 Portability and kernel-version drift. Landlock needs Linux 5.13 or newer and grows new abilities per ABI: TCP bind and connect by port (ABI 4, Linux 6.7), device ioctls (ABI 5), abstract-unix-socket and signal scoping (ABI 6, Linux 6.12), audit logging (ABI 7), pathname unix sockets (ABI 9) and UDP (ABI 10). A sandbox written for ABI 10 quietly gets less on a 6.6 LTS kernel unless it checks. Seccomp syscall numbers are per-architecture. Unprivileged user namespaces are disabled on some hardened distributions, and from Ubuntu 24.04 AppArmor restricts them (kernel.apparmor_restrict_unprivileged_userns=1), which breaks bubblewrap unless a profile grants it userns. A sandbox that works on your machine may fail open elsewhere.
🔥 Performance and density. Hosted agent platforms run many sandboxes and want sub-second cold starts and snapshot/restore. OS-level sandboxes are cheap (microseconds to milliseconds) but weak. VMs are strong but were historically slow to boot, which is the gap Firecracker plus snapshotting closed, and why microVMs became viable for this at all.
🔥 MicroVMs are becoming the default for untrusted code, not the exception. Firecracker-style microVMs give each sandbox its own kernel at roughly 100 to 150 ms cold start, and snapshot/restore (lazy page-fault-driven restore via userfaultfd) makes per-task or per-step forking cheap. The hosted market (E2B, Vercel Sandbox, AWS AgentCore) has largely standardised on this.
📝 A tiered “right-sized isolation” model is winning over one-size-fits-all. Guidance is converging across 2026 write-ups: OS-level confinement (Landlock, seccomp, bubblewrap) for trusted code on trusted machines; gVisor as a middle tier; microVMs and Kata for untrusted, multi-tenant code. The interesting engineering is in choosing and composing tiers per workload, not in one perfect sandbox.
📦 Landlock is maturing into a real agent primitive. It began filesystem-only (Linux 5.13). ABI 4 (Linux 6.7) added TCP bind and connect rules, ABI 6 (Linux 6.12) added abstract-unix-socket and signal scoping, and ABIs 9 and 10 added pathname unix sockets and UDP. That turns Landlock from “restrict files” into “restrict a process tree’s whole resource reach”, unprivileged and self-applied, which is ideal for a CLI agent that sandboxes itself.
🛂 seccomp-notify supervisors put user-space policy in the loop. Instead of a static allow/deny BPF program, the kernel can forward a syscall to a supervisor process that decides at runtime and can even virtualise the effect. Sandlock (Wang and Zheng, arXiv:2605.26298), a 2026 research prototype that is now an open-source project (section 4), combines Landlock and seccomp with a seccomp-notification supervisor to get dynamic network decisions, HTTP-level access control, and TOCTOU-safe inspection of execve arguments, all unprivileged, with about 5 ms startup overhead. “Static policy compiled into the kernel, plus a narrow supervisor for the hard cases” is likely where lightweight agent sandboxes head. NVIDIA’s OpenShell (section 4) now ships this shape as a product.
☸️ Orchestration is standardising. The Kubernetes SIG’s agent-sandbox (a Sandbox CRD with warm pools, delegating isolation to gVisor or Kata via RuntimeClass) signals that “a sandbox” is becoming a first-class, schedulable object with stable identity, persistent storage and a sub-second claim from a pre-warmed pool, rather than an ad-hoc docker run.
🧊 Snapshotting is a product feature, not just a speed trick. Pause/resume, fork-a-running-state, and “close your laptop, the agent keeps going” (AWS AgentCore) are becoming table stakes. That pushes toward VM-level isolation, because snapshotting a whole guest is cleaner than checkpointing a process tree (CRIU is fragile).
🔌 Egress control moves up the stack. The trend is away from “block all sockets” toward identity-aware filtering proxies with per-domain allow-lists and audit. Agents must reach some network, and the useful policy is about where, not whether.
The status quo for running arbitrary code has been OCI containers (Docker and runc): namespaces, cgroups, a default seccomp profile and dropped capabilities. They have good ergonomics and a huge ecosystem, but they share a kernel, so they are insufficient for genuinely untrusted, multi-tenant code. The contenders differ mainly in where the trust boundary sits.
| Approach | Boundary | Isolation strength | Start | Cost / density | Best fit for agents |
|---|---|---|---|---|---|
chroot |
path resolution only | ~none (escapable) | instant | free | not a sandbox |
| bwrap / nsjail / Landlock launchers (e.g. Sandlock) | namespaces, shared kernel | low to moderate | ~ms | very high | trusted code; per-call confinement |
OCI container (runc) |
namespaces + seccomp, shared kernel | moderate | ~100 ms | very high | semi-trusted; the status quo |
| gVisor | user-space kernel intercepts syscalls | high (small host surface) | ~100 to 300 ms | high | untrusted code without a VM; some compatibility cost |
| Kata / Firecracker / Cloud Hypervisor | separate guest kernel (KVM) | highest (on Linux) | ~100 to 150 ms, faster from snapshot | moderate | untrusted, multi-tenant: the serious default |
| WASM runtime | capability-based VM, no syscalls | high for what it supports | ~ms | very high | narrow, pure-compute tools |
Moving down the table trades density and compatibility for isolation strength. Containers sit in the middle and are fine for code you mostly trust. The direction of travel for untrusted agent code is toward gVisor and especially microVMs, with OS-level tools relegated to the innermost layer or to trusted single-user setups.
A few nuances the table flattens:
Two families, by where they run.
sandbox-exec). Its docs warn that inside Docker the needed APIs may be unavailable and sandboxing can fail, which is the portability problem from section 1 made concrete.socat, which gives domain-level egress control. A seccomp filter blocks other unix sockets on x86-64 and arm64; without it, unix sockets stay open with a warning. On macOS it uses sandbox-exec. By default it warns and runs unsandboxed if dependencies are missing, with a failIfUnavailable hard-fail mode for managed deployments. That is the “fail open vs fail closed” decision made visible.These share a fundamental limit that several guides state: bubblewrap and Seatbelt run on the same kernel as the host, so they offer little against kernel exploits. They are suitable mainly for trusted code on trusted machines, the right tool for a developer running an agent on their own laptop and not for a multi-tenant service executing strangers’ code.
agent-sandbox. Not an isolator itself: a Sandbox CRD plus controller that schedules pods onto gVisor or Kata via RuntimeClass, with warm pools for sub-second starts. It is the orchestration story for self-hosting agent sandboxes at scale.OpenShell is NVIDIA’s answer to agent sandboxing. It was announced at GTC on 16 March 2026 as part of the NVIDIA Agent Toolkit, released under Apache-2.0, and is at version 0.1.x as of October 2026. It sits between the two families above: it runs on a laptop like the local CLIs, but it is built as a control plane for fleets of agents, like the hosted platforms. It does not replace an agent CLI. It wraps one: the docs and NVIDIA’s own write-ups list Claude Code, Codex, OpenCode and GitHub Copilot CLI as workloads.
🦀 Three components. The architecture splits trust three ways:
The supervisor and sandbox talk over one mutually authenticated HTTP/2 connection, carried over a unix socket (Docker, Podman), TCP (Kubernetes) or vsock (microVM).
📦 The kernel mechanisms are the ones in this post. Per the support matrix, launching a sandbox requires:
SECCOMP_IOCTL_NOTIF_ADDFD with SECCOMP_ADDFD_FLAG_SEND, used to intercept TCP opens and DNS queries. ADDFD is how a supervisor installs a file descriptor it opened into the confined process, the same fd-delegation idea as Capsicum’s broker in section 7.process_vm_readv or /proc/<pid>/mem, so the supervisor can inspect syscall arguments.SO_BINDTODEVICE to bind workload sockets to loopback.The agent runs as one non-root user with no Linux capabilities. If any requirement is missing, launch fails closed. If the supervisor connection drops, the sandbox freezes the agent, and only the same supervisor process can resume it. That is exactly the “static policy compiled into the kernel, plus a narrow supervisor for the hard cases” shape that section 2 predicted from the Sandlock research, shipped by a major vendor.
🧱 An outer fence, so the supervisor is the only way out. Seccomp interception alone is porous (section 1’s io_uring trap), so each runtime also removes direct network access using its own tools:
| Runtime driver | Where the supervisor runs | How direct egress is blocked | Kernel |
|---|---|---|---|
| Docker | its own container | workload container has network=none
|
shared with host |
| Podman (rootless) | its own container | workload container has networking off | shared with host |
| Kubernetes | its own pod |
NetworkPolicy allows only the supervisor; the CNI must enforce it |
shared, unless the pod uses a Kata RuntimeClass
|
| MicroVM (libkrun) | host process | guest has no network device | its own, via KVM or Apple Hypervisor.framework |
This maps one-to-one onto section 3’s tiers. The container drivers are the shared-kernel tier with a better egress design, and the microVM driver is the second-kernel tier. The microVM driver is opt-in, never auto-detected, and the docs do not recommend one driver for untrusted code, so for strangers’ code choose the VM driver (or Kata on Kubernetes) deliberately. On macOS the Linux kernel controls run inside Docker Desktop’s VM. The Kubernetes driver schedules pods through the same Agent Sandbox controller described above.
📦 Policy: one YAML file, deny by default. A policy has up to five sections: filesystem_policy (read-only and read-write paths, enforced by Landlock), landlock, process (the user and group), network_policies and network_middlewares. Filesystem, Landlock and process settings are fixed at startup; network rules can change while the sandbox runs. Every outbound connection is denied unless a rule allows it. Network rules go well past a domain allow-list:
protocol: rest, rules match method, path and query, with presets such as access: read-only (GET, HEAD, OPTIONS). To see inside HTTPS, the supervisor terminates TLS unless an endpoint sets tls: skip.169.254.169.254, are never authorised.For example, read-only GitHub API access for curl and nothing else:
github_readonly:
endpoints:
- host: api.github.com
port: 443
protocol: rest
enforcement: enforce
access: read-only
binaries:
- path: /usr/bin/curl
📦 Credentials never enter the sandbox. Providers map a service to a stored credential, and the supervisor adds it only to requests bound for approved endpoints. The sandbox holds no gateway key, gateway token or provider secret. This is the credential brokering recommended in section 8.
📜 Policy review with a solver. Two features turn section 6’s “policy matters as much as mechanism” into tooling:
auto mode exists.link_local_reach, credential_reach_expansion and l7_bypass_credentialed (a credential reachable by traffic the supervisor cannot inspect). Any finding forces human review, even in auto mode. The prover answers unsupported rather than guessing for GraphQL, MCP, WebSocket and JSON-RPC, and a pass means only “within the boundary in the checked areas”, not “safe”.📝 Sentry: a monitor the host cannot reach. On 28 September 2026 NVIDIA wrapped OpenShell into an “Open Agent Safety Platform” with Sentry, a monitoring and enforcement layer that runs on BlueField-4 DPUs instead of on the machine running the agent. The premise is that “an agent that deviates from its intended task cannot be relied upon to govern itself”: if the agent escapes the shared kernel, a monitor on the network card is still in the path. It is an answer to this post’s core problem (one shared kernel), but it needs NVIDIA data-centre hardware, is not open source, and the adoption claims around it come from NVIDIA and its partners.
🛠️ How it measures against this post.
| Theme in this post | OpenShell’s answer |
|---|---|
| Shared kernel (section 1) | Unchanged for the container drivers; solved by the opt-in microVM driver or Kata. Sentry adds out-of-band monitoring on DPU hardware. |
| Egress is the real prize (section 1) | Supervisor-only egress, deny by default, per-binary and per-method rules, TLS termination, supervisor-side DNS, metadata always blocked. The strongest part of the design. |
| Exfiltration through allowed channels (section 8) | Partly: read-only REST rules stop pushes to github.com, but only for traffic it can inspect (tls: skip and non-HTTP protocols bypass L7 rules). |
| Credential brokering (section 8) | Yes, by design. |
| Fail closed (decision guide) | Yes at launch and on supervisor loss; check the landlock section’s compatibility setting, where only hard_requirement aborts startup when Landlock rules cannot be applied. |
| Least privilege per task (section 6) | Advisor proposals plus prover checks plus human approval. |
| Persistence after exit (section 8) | Not addressed: the workspace persists across stop and start, so protect hooks and editor configs yourself. |
| The model as an exfiltration channel (section 8) | Not addressed by the sandbox; no sandbox can see what the model writes. |
| GPUs (section 5) |
--gpu via NVIDIA CDI devices on the container drivers (the shared host driver), one GPU on the microVM driver. |
⚠️ Caveats worth knowing before relying on it:
enforcement defaults to audit, which logs method and path violations and lets them through. Set enforcement: enforce for the read-only rules to mean anything. Host, port and binary matching is still deny by default./usr/bin/python3 or /bin/bash covers any script that interpreter runs, and children inherit the rule. Keep interpreters out of rules that carry credentials.Sandlock started as the research prototype mentioned in section 2 (arXiv:2605.26298, a preliminary paper for the Agentic OS workshop at ASPLOS 2026) and is now an open-source project from Multikernel Technologies: Rust, Apache-2.0, at v0.9.0 as of 13 October 2026, after releases every two to three weeks since March. Where OpenShell wraps the supervisor pattern in containers or microVMs, Sandlock strips everything else away: no root, no namespaces, no cgroups, no image. It confines an ordinary child process with Landlock, seccomp-bpf and a seccomp-notification supervisor, and nothing more. That puts it squarely in section 3’s shared-kernel tier, next to bubblewrap, but with a far richer policy than a list of bind mounts.
$ sandlock run -w /tmp -r /usr -r /lib -m 512M -- python3 untrusted.py
It ships as a CLI, a Rust library, Python and Go SDKs over a C ABI, and an OCI runtime shim.
🪜 How confinement is applied. The architecture is a fixed sequence after fork(). The child sets no_new_privs, applies Landlock (filesystem, TCP ports, IPC scoping), and installs a seccomp filter that denies some syscalls outright and routes others to a listener. It passes the listener’s file descriptor to the parent, waits for the go-ahead, closes inherited descriptors and calls exec. The parent runs an async supervisor on that listener. The policy is therefore in force before the untrusted program runs its first instruction. What the supervisor handles:
| Intercepted syscalls | What the supervisor does |
|---|---|
clone, fork, vfork
|
caps the number of processes |
mmap, munmap, brk, mremap, mprotect
|
tracks memory against the limit |
connect, sendto, sendmsg
|
checks the destination, performs the call on the child’s behalf, redirects HTTP to the filtering proxy |
bind, getsockname
|
binds on the child’s behalf, with per-sandbox port virtualisation |
openat, getdents64
|
virtualises /proc, redirects reads and writes for the copy-on-write workdir |
execve, execveat
|
holds the call for an optional policy callback |
getrandom, timers |
can inject deterministic randomness and freeze time |
🧱 Kernel floor, and failing closed. By default it needs Linux 6.12 (Landlock ABI 6, which added scoping for abstract unix sockets and signals) and refuses to start if a protection is unavailable. Callers can opt out per protection: allow_degraded enforces a protection where the kernel supports it and silently skips it where it does not, and disable never enforces it. Strict is the right default, but many long-term-support distributions ship older kernels (Debian 12 has 6.1; RHEL 9 is based on 5.14), so a mixed fleet will tempt you towards allow_degraded, which quietly gives up exactly the protections section 1 worries about. Check what sandlock check reports instead of assuming.
🔌 TOCTOU: act on the child’s behalf. The classic weakness of a seccomp-notify supervisor is that when it answers “continue”, the kernel re-reads pointer arguments from the child’s memory, and another thread can change them in between. Sandlock never continues a mediated network call. It copies the sockaddr once, checks the copy, takes the socket with pidfd_getfd, and performs connect itself, the same fd-delegation idea as OpenShell’s ADDFD and Capsicum’s broker. For execve arguments it freezes sibling threads and any process sharing the child’s memory while it reads, and denies with EPERM when it cannot freeze them (for example when Yama blocks ptrace). The paper is explicit that matching on arguments is an observation signal, not a containment boundary: containment comes from Landlock, seccomp and the network rules.
🌐 Network: deny by default, two enforcement paths. With no rules, Landlock denies every TCP connect and seccomp blocks creating UDP, ICMP and raw sockets. Rules name protocol and destination (--net-allow api.openai.com:443, udp://…, icmp://…), a deny list wins any overlap, and raw ICMP is never exposed. The network model has two paths:
Hostnames are resolved once at start and pinned in a synthetic /etc/hosts, and wildcard hostnames (*.example.com) are not supported. With no UDP rule the sandbox cannot send DNS queries at all, which closes section 1’s DNS covert channel. Two consequences follow from checking by address. A service that changes IPs mid-run breaks. And allowing a CDN-fronted host admits every other site on those addresses, unless an HTTP rule also checks the host name.
🔏 HTTP rules and credentials. --http-allow "POST api.openai.com/v1/chat/completions" and --http-deny "* */admin/*" send matching traffic through a transparent proxy that checks method, host and path. For HTTPS the proxy must terminate TLS. --http-inject-ca generates a throwaway CA, keeps its private key in memory, and splices its certificate into the trust bundles you name when they are opened. Runtimes with their own certificate store, such as Node, need NODE_EXTRA_CA_CERTS pointed at it. Without this, --net-allow host:443 allows raw TLS with no inspection, the same gap as OpenShell’s tls: skip. Credential injection keeps a secret in the supervisor and adds it in the proxy after the rule check, so the agent never holds it:
$ sandlock run \
--http-allow "POST api.openai.com/v1/*" \
--http-inject-ca /etc/ssl/certs/ca-certificates.crt \
--credential openai=env:OPENAI_API_KEY \
--http-auth "POST api.openai.com/* bearer openai" \
-r /usr -r /lib -r /etc -- python3 agent.py
That is section 8’s credential brokering, without a separate gateway.
📂 Filesystem: Landlock plus a copy-on-write workdir. Landlock grants the read and write paths. With --workdir, the supervisor also stages writes in an upper directory (reads resolve upper first, then lower), with no mount or user namespace. On exit it commits, aborts on error, or, with deferred handling, leaves the changes for a trusted caller to inspect and then commit or abort. --dry-run shows what a command would change, then throws it away. This is the most direct answer in this post to section 8’s persistence after the sandbox exits: an injected edit to .git/hooks inside the workdir can be reviewed before it lands. The limits: only regular files, directories and symlinks are staged (FIFOs, sockets and device nodes go straight to the kernel and are never reverted), writes outside the workdir are not staged, and network effects are never rolled back.
📉 Resource limits without cgroups, and their weakness. Memory is capped by counting address-space requests (mmap, brk, mremap, shmget), not resident pages, and stack growth is not counted. Processes are capped by refusing clone with EAGAIN, CPU is throttled with SIGSTOP/SIGCONT duty cycles, and file descriptors use RLIMIT_NOFILE. The paper calls these limits cooperative and says cgroups remain stronger against adversarial workloads. They stop a runaway build, not a determined fork-and-allocate attack.
👀 Virtualised views. Because openat is intercepted, the supervisor can rewrite what /proc shows. The docs detail /proc/net: a loopback-only topology, and socket tables that list only sockets held by sandbox tasks. That is section 1’s “views leak the host”, handled for the network tables.
🧩 Composition: pipelines and planner/executor. Pipelines give each stage its own sandbox, joined by kernel pipes (planner.cmd(...) | executor.cmd(...)). The execute-only agent pattern splits an agent in two: the planner can reach the model but cannot read the data; the executor can read the data but has no network. That is a structural answer to the lethal trifecta from section 8, because no single process holds private data, untrusted content and a way out. The prompt-injection guide is candid about the limits: the split bounds capabilities, not what flows through the pipe, and the executor’s output can still carry sensitive data. A related COW fork initialises a process once and forks sandboxed clones from it, about 530 µs each.
📜 Least privilege by observation. sandlock learn runs a workload under observation and writes a profile of the paths, connections, HTTP methods and paths, and resource peaks it used, ready for sandlock run -p. It is a practical start on section 6’s least privilege per task. Review the result: a profile learned from a run that an injected agent influenced records whatever that agent did.
🐳 Namespace-less containers. sandlock-oci implements the OCI runtime interface, so containerd, CRI-O and Kubernetes can run workloads under Sandlock unprivileged, with checkpoint and restore. The isolation is Landlock and seccomp, not namespaces, so treat it as the shared-kernel tier, not as a stronger runc.
🪆 Nesting costs features. The kernel allows one seccomp listener per task, so a Sandlock inside another Sandlock must run no_supervisor: Landlock plus a kernel-only deny filter, with no IP allow-list, resource limits, copy-on-write, /proc virtualisation or custom handlers.
📏 The numbers. On the paper’s workstation (Ryzen 5 5500U, Linux 6.18), Sandlock adds about 5 ms to startup, Redis throughput and p99 latency are within noise of bare metal, and the on-behalf network path adds about 35 µs per round trip. Rootful Docker is the only other system the paper measures; its figures for bubblewrap, gVisor and Firecracker are approximate, not measured.
🛠️ How it measures against this post.
| Theme in this post | Sandlock’s answer |
|---|---|
| Shared kernel (section 1) | Unchanged, by design. Kernel bugs and side channels are out of scope, and the authors say adversarial multi-tenant workloads still need a microVM. |
| Egress is the real prize (section 1) | Deny by default, a Landlock fast path, supervisor-mediated destinations, HTTP method and path rules, pinned DNS. |
| Exfiltration through allowed channels (section 8) | Partly: HTTP rules limit methods and paths when TLS is intercepted. Raw TLS to an allowed host is not inspected, and an allowed endpoint can still receive data. |
| Credential brokering (section 8) | Yes: injected in the proxy after the rule check. |
| Fail closed (decision guide) | Yes by default; allow_degraded and disable weaken it per protection. |
| Persistence after exit (section 8) | The strongest answer in this post: a copy-on-write workdir with dry-run and deferred review. Writes outside the workdir are not covered. |
| Least privilege per task (section 6) |
sandlock learn profiles, plus a separate sandbox per pipeline stage. |
| Resource exhaustion (section 8) | Cooperative limits without cgroups; weaker than cgroups. |
| The model as an exfiltration channel (section 8) | Not addressed by the sandbox. The planner/executor split reduces what the model sees. |
OpenShell or Sandlock? They implement the same idea at different scales. OpenShell is a control plane for fleets: containers or microVMs as an outer fence, per-binary rules, a policy prover. Sandlock is one unprivileged process wrapper with about 5 ms of overhead, a copy-on-write workdir and composable pipelines. On a laptop, Sandlock is the lighter fit; for strangers’ code, either one belongs inside a VM.
⚠️ Caveats worth knowing before relying on it:
python3, make, node, pytest), and I found no independent audit.Yes. Both put the guest behind hardware virtualisation with its own kernel, so the kind of boundary is the same. The difference is what it costs to get it:
QEMU is the better choice when you need hardware Firecracker lacks (GPU passthrough, USB, other CPU architectures, Windows or macOS guests) or you want the most mature, widely supported tool. QEMU’s microvm machine type with no firmware and a stripped config gets most of Firecracker’s benefits, and Cloud Hypervisor and crosvm are other minimal VMMs. Firecracker is not the only way; it is the one managed platforms standardised on.
Stock Firecracker cannot give a guest a GPU. Its minimal device model has no PCI bus, which rules out PCIe passthrough, and that is a consequence of the design, not an oversight. (Firecracker’s PCI story has been changing; check the current release notes.) For GPU workloads, sources point to QEMU or Cloud Hypervisor instead. The options in use:
| Model | Boundary | Catch |
|---|---|---|
| VFIO passthrough into a VM (QEMU, Cloud Hypervisor, Kata) | Hypervisor, with exclusive device ownership | One GPU per tenant. Naive passthrough can re-open the boundary through GPU DMA, so it needs an IOMMU and care. |
gVisor + nvproxy |
Filtered NVIDIA ioctls forwarded to the host driver | Weaker than a VM: the boundary is the driver’s ioctl surface. Supports only selected driver versions and CUDA commands. |
| NVIDIA MIG | Hardware-level partitions of one card | Supported data-centre GPUs only (for example A100, H100, H200, B200). Each partition can go to a different VM. |
| Time-slicing / vGPU | Little (time-slicing); licensed vendor driver (vGPU) | Time-slicing suits only low-sensitivity workloads. |
| Plain container + NVIDIA Container Toolkit | Shared host kernel and driver | Fine for trusted code, poor for untrusted. |
| No GPU in the sandbox | n/a | Run agent glue code in a microVM and call inference over an API. Often enough. |
Why it is hard: GPU drivers are large and complex, and passing a device through means the guest drives real hardware over DMA. That widens exactly the surface a microVM was chosen to shrink, and it makes cheap snapshot and migration much harder. The shared-kernel tier has the mirror-image property: a GPU is just /dev/nvidia* files you allow through Landlock, which is simple but leaves the driver reachable from the sandboxed process.
Either a whole VM or bare-metal instance with a GPU attached (the EC2 model: one customer per GPU, isolation is the hypervisor’s), or a managed GPU sandbox:
For a GPU SaaS with mutually untrusted tenants: dedicated-GPU VMs with VFIO (Kata or Cloud Hypervisor), plus confidential computing if tenants distrust the operator; MIG where a card must be subdivided; gVisor with nvproxy as the cheaper middle option whose boundary is the ioctl surface.
wasi:webgpu) is a Phase 2 proposal.Arm CCA brings “realms” with the same aim to Arm and is the next place to watch.
Gaps: I found no paper proposing microVM snapshots specifically for agent isolation, and none on seL4- or CHERI-based agent sandboxes.
OpenBSD is one of the most security-focused commodity OSes, so it is a fair question. It is a strong answer for the OS-level tier, but it does not change the tiering in section 3.
Two framing points. The tiers are about where the trust boundary sits, not only about CPU-level isolation. And OpenBSD is also a shared-kernel design, so a kernel bug still breaks every sandbox on the machine.
pledge(2) and unveil(2) are arguably better designed than seccomp and Landlock. Coarse promises such as stdio rpath inet are hard to get wrong, where seccomp filters are easy to write badly. unveil limits which parts of the filesystem a process can see.pledge: the second argument, execpromises, sets the promises the next execve starts with. But unveil does not survive execve (“the new program starts running without any unveils”), so there is no external way to limit which files an arbitrary binary, such as an agent’s pip install and shell, can see. On Linux, bubblewrap and Landlock launchers do both from outside.login.conf, but nothing like cgroups v2 for memory, PIDs and CPU, and no unprivileged user-namespace-style containers. It has no seccomp user-notify equivalent for a supervisor that vets syscalls. Egress control is pf, which is good but needs root and is not per-process or per-domain.vmm(4)/vmd, is much less mature than KVM, and the microVM tooling (Firecracker, gVisor, Kata) and snapshot/restore are all Linux and KVM.OpenBSD works well as the host or guest for a small, auditable control plane or an egress proxy. For the part that runs untrusted agent code, the tooling is on Linux. For one trusted-ish agent on your own machine, pledge and unveil are a good fit and arguably simpler than the Linux stack; they just do not scale to the multi-tenant case.
The other BSD answer is worth knowing because it is a different model, not just a cleaner syscall filter. Linux confines a process by subtracting authority with a pile of independent mechanisms: namespaces, seccomp, Landlock, and the confusingly named POSIX capabilities, which are privilege bits, not object capabilities. Capsicum (FreeBSD 9.0, 2012, from the University of Cambridge and Google) instead gives a process authority only through the file descriptors it already holds. It has two primitives:
cap_enter(2), capability mode. An irreversible, inherited switch that cuts the process off from every global namespace: no open() by absolute path, no PIDs, no sysctl by name, no mount table. From then on it can only act on descriptors it was handed, and new files come via openat() relative to a directory descriptor it was delegated.cap_rights_limit(2). Narrows each descriptor to specific rights (CAP_READ, CAP_WRITE, CAP_SEEK and so on), so a descriptor becomes an unforgeable token for exactly the operations you granted.This is deny-by-default expressed as a coherent object-capability model: the same shape the research in section 6 (learned capability governance, MAC for agents) keeps reaching for, shipped in a production OS over a decade ago. Because a process in capability mode cannot resolve DNS or open files by name, Capsicum pairs it with Casper, a helper process that performs those actions on the sandbox’s behalf, vetted by policy. That broker pattern is the seccomp-notify supervisor from section 2, the filtering proxy from section 4 and the credential brokering from section 8. Capsicum got there first.
Why it still does not change the tiering:
pledge, unveil and Landlock self-restriction. A program must be refactored to call cap_enter and to receive its resources as descriptors. That is excellent for compartmentalising a program you control, such as a broker or an agent runtime, and poor at confining an arbitrary untrusted binary from the outside: an agent’s pip install and shell expect to open new paths by name, which is the one thing capability mode forbids. FreeBSD’s answer for untrusted code is the same two-tier story as Linux: jails and bhyve VMs.Its influence outlived its reach. Capsicum for Linux (Google, around 2015) never merged into mainline, and Landlock is the closest Linux has, but Landlock is path-based self-restriction, not a true descriptor-capability model. If you are building the broker in front of an agent sandbox, Capsicum is the cleanest template in a shipping OS. It is not a drop-in confiner for the untrusted code itself.
Sections 1 to 7 are about confining the process. Checked against a real agent deployment, they leave gaps that no layer in the table closes, because the damage happens outside the box or through a channel the box is meant to allow.
📦 Persistence after the sandbox exits. The agent may write only inside the project, but the project is full of things that later run unsandboxed: .git/hooks/*, .git/config (core.fsmonitor, core.sshCommand), .envrc for direnv, .vscode/tasks.json and settings.json, the Makefile, package.json scripts, and .bashrc if $HOME is writable. The confinement is perfect, and the payload runs the next time you type git commit or open the editor. Deny writes to these paths (Landlock rules or read-only bind mounts over them) and review the diff before running anything outside the sandbox.
🔒 Exfiltration through allowed channels. A domain allow-list that contains github.com also allows pushing your code to an attacker’s repo or a gist, and pypi.org allows uploading a package. Policy has to be per destination identity (this org, this repo, read-only methods), not per domain.
📦 The model is an exfiltration channel the sandbox cannot see. Data the agent reads goes into the model’s context, and a prompt-injected agent can leak it in its output (for example through a rendered Markdown image URL) without a single syscall from inside the sandbox. Simon Willison’s lethal trifecta is the frame: private data, untrusted content and a way to communicate out. Remove one leg, because no sandbox removes it for you.
📦 Credential brokering instead of credential stripping. Stripping secrets is the minimum. The stronger pattern is that secrets never enter the sandbox: the egress proxy injects a short-lived, narrowly scoped token on the way out, so a compromised agent can only use the credential for what the proxy allows, and cannot copy it.
🐳 Host-side session sockets. D-Bus, ssh-agent, gpg-agent, X11 and Wayland, PulseAudio, and the Docker or Podman socket. X11 alone allows keystroke injection into other windows. A fresh network namespace hides abstract unix sockets, because they are per network namespace, but any pathname socket you bind-mount in is a door. Landlock ABI 9 can gate those.
📝 Host kernel knobs. Every tier except the VM shares the kernel, so turn off what agents do not need: kernel.unprivileged_bpf_disabled=1, kernel.io_uring_disabled=2, vm.unprivileged_userfaultfd=0, dev.tty.legacy_tiocsti=0, a stricter kernel.yama.ptrace_scope, and kernel.perf_event_paranoid=3 where the distribution supports it. These are cheaper than any seccomp filter, and they cover processes you forgot to wrap.
🤖 Resource exhaustion beyond memory and PIDs. Disk fill (a tmpfs with size=, or project quotas), inodes, I/O bandwidth (io.max), CPU (cpu.max) and wall-clock time. An agent stuck in a loop is the common case, not the exotic one.
🧊 Lifecycle and reuse. Sandboxes should be ephemeral and never reused across tenants or tasks, and a warm pool must hand out clean snapshots. Reusing a sandbox turns one task’s prompt injection into the next task’s compromise. Cloned snapshots also share RNG state and ASLR layout unless the guest reseeds (VM generation ID), a known microVM snapshot pitfall.
🧱 Audit and approval. You need a record of what the agent did: Landlock audit (ABI 7), seccomp SECCOMP_RET_LOG, the proxy’s request log, and human approval for irreversible steps such as push, deploy and delete. Without a log you cannot tell an escape from a bug.
📦 Testing the sandbox itself. Run escape tests in CI (the x32, io_uring and TIOCSTI cases from section 1, plus something like SandboxEscapeBench from section 6) on every kernel and distribution you ship on, because the same sandbox can fail open elsewhere.
🧊 Side channels and physical access. Spectre-class leaks, SMT sharing and Rowhammer cross every software boundary, the VM one included. The mitigations are scheduling (no SMT sharing between tenants, core scheduling) and hardware. Physical attacks on the bus are out of scope for every tier here.
Not with isolation. Every boundary in this post is enforced by page tables, privilege levels, the kernel or the hypervisor, and none of them relies on a hard maths problem, so nothing labelled “quantum” makes them stronger or weaker. Quantum does touch agent sandboxing in a few narrower places:
| Quantum technology | Does it help? | Why |
|---|---|---|
| Quantum computers (Shor’s algorithm) | No, it is a threat | It breaks the RSA and elliptic-curve cryptography that the trust around a sandbox depends on. |
| Post-quantum cryptography (ML-KEM, ML-DSA) | Yes, indirectly | Classical algorithms that keep that trust intact once quantum computers arrive. |
| Quantum random number generators | No | The kernel’s CSPRNG is not the weak point. Cloned snapshots are, and a QRNG does not fix them. |
| Quantum key distribution (QKD) | No | It needs dedicated optical links between two sites and says nothing about code on one host. The NSA and the UK NCSC recommend post-quantum cryptography instead. |
| Cloud quantum computers | It is a new sandboxing problem | Tenants sharing one quantum processor leak to each other through crosstalk. |
⚛️ What a quantum computer would break. It would break the cryptography that lets you trust the sandbox, not the sandbox itself:
Symmetric cryptography such as AES-256 and SHA-256 stays safe. Grover’s algorithm only halves its effective strength.
💭 The two halves migrate on different clocks.
mlkem768x25519-sha256 by default, and OpenSSL 3.5 offers X25519MLKEM768 by default. A TLS-intercepting proxy has two TLS legs, so check both.🧊 Randomness: the real problem is not where a QRNG would help. The RNG risk in this post is the snapshot pitfall from earlier in this section: clones of one microVM snapshot start with the same RNG state. Better entropy hardware does not fix that. Reseeding on restore does, via the VM generation ID.
⚛️ The other direction: sandboxing quantum computers. If agents start submitting circuits to cloud quantum processors, the isolation problem comes back in new hardware. Crosstalk side channels in multi-tenant quantum computers (NDSS 2025) can identify which algorithm a co-located tenant is running, and the SWAP attack can both spy on a neighbouring circuit and corrupt its results. The proposed defences look familiar: do not place tenants next to each other, the way core scheduling keeps tenants off sibling SMT threads. The classical job-submission stack in front of the processor is still sandboxed with everything in this post.
~/.ssh), cap resources with cgroups (fork bombs are cheap), protect the files that run after the sandbox exits (git hooks, editor tasks, .envrc; see section 8), and fail closed. A sandbox that silently runs unconfined is the worst outcome.Point-in-time (checked October 2026); vendor docs and 2026 analyses, so verify before relying on specifics.
1. Encrypted bounce buffers. A confidential VM’s memory is encrypted by the CPU with a key only the CPU holds, so the GPU cannot DMA into it directly: it would read ciphertext. Instead, the driver inside the VM encrypts the data itself, with a session key agreed with the GPU during attestation, and copies it into a separate, shared region of ordinary system memory: the bounce buffer. The GPU fetches it from there over PCIe and decrypts it on the chip, and results come back the same way in reverse. The host, the hypervisor and anyone probing the PCIe bus only ever see ciphertext. The price is an extra copy plus a CPU-side encrypt or decrypt on every transfer, which hurts most for workloads that move a lot of data relative to the compute they do. The longer-term fix is PCIe link encryption (IDE) with TEE-I/O and TDISP, which would let a trusted device DMA straight into the VM’s private memory and remove the bounce. ↩