Status: Phase 0 working document. Every invariant here needs an executable test before Phase 1.
This model covers:
- the Rust editor process;
- the per-session Go policy gateway;
- untrusted plugin-host processes running Lua/QuickJS;
- plugin manifests, updates, and permission grants;
- file, network, and child-process access mediated by
sys.*; - UDS transport between the processes.
It does not cover kernel compromise, a malicious editor binary, a compromised terminal emulator, physical access, or supply-chain compromise of the compiler and package registries. Those are separate risks.
| Asset | Why it matters |
|---|---|
| Workspace files | Source code, secrets, configuration |
| Files outside the workspace | SSH keys, cloud credentials, browser profiles |
| User terminal | Input injection, clipboard, escape sequences |
| Network identity | Cookies, tokens, source IP, DNS metadata |
| Editor memory | Unsaved buffers and secrets |
| Child processes | Shell, language servers, rg, fd |
| User attention | Malicious notifications, fake permission prompts |
| Plugin grants | Capability durability and update integrity |
trusted: user, terminal, editor
────────────────────────────────────────
untrusted: plugin code, plugin host after compromise
────────────────────────────────────────
policy: Go gateway
────────────────────────────────────────
external: filesystem, network, child processes
The important correction to the original whitepaper:
The Go gateway is a policy enforcement point, not the plugin sandbox. The plugin-host process boundary and its kernel sandbox are the isolation boundary.
A plugin running in the editor process cannot be contained by a policy gateway: it can call the Lua/QuickJS standard library, native C functions, or syscalls directly.
- Malicious plugin author: ships a plugin that requests narrow permissions and then attempts to escape them.
- Compromised dependency: a Lua/QuickJS native dependency or Rust crate contains a memory-safety bug.
- Malicious project: a repository contains a crafted
Anvil.toml, symlinks, or a workspace root designed to confuse path resolution. - Network attacker: DNS rebinding, redirect, or a malicious endpoint.
- Local process: another process on the same machine tries to connect to a gateway/editor socket.
- Resource attacker: a plugin tries to exhaust memory, file descriptors, CPU, or child processes.
Attack: ../../.ssh/id_ed25519, absolute paths, symlink swap between policy
check and read, magic links under /proc.
Required:
- One workspace-root
O_PATHfd per session. openat2withRESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS.RESOLVE_NO_SYMLINKSis a configurable hardening option; when not enabled, symlink targets must still remain beneath the root.- Retry
EAGAINinstead of treating it as a file error. - Open once, inspect the resulting fd, read from that fd. No second resolution.
- Reject absolute paths, empty components, and non-UTF-8 path policies before the syscall.
- Test workspace root itself being a symlink.
Attack: fetch https://allowed.example which resolves to 127.0.0.1,
169.254.169.254, a private address, or changes its answer between validation
and connection.
Required:
- Exact host allowlist; no wildcard in v1.
- HTTPS and port 443 only.
GET/HEADby default;POSTrequiresnetwork:post.- Resolve all A and AAAA answers; reject if any answer is non-global.
- Connect to a validated address while preserving Host, TLS SNI, and certificate verification.
- Disable redirects. If enabled later, validate every hop.
- Disable proxy environment variables.
- Cap body after decompression, header size, total time, and concurrency.
- Reject userinfo, IDN ambiguity, trailing-dot variants, IPv4-mapped IPv6, CGNAT, link-local, multicast, unspecified, NAT64, 6to4, and Teredo.
- Treat parser disagreement as rejection.
Reference: OWASP SSRF Prevention Cheat Sheet.
Attack: use an allowlisted tool's own execution features, for example
rg --pre <command> or fd --exec <command>, or inject PATH, LD_PRELOAD,
RIPGREP_CONFIG_PATH, or FD_*.
Required:
- No shell, no shell string, no shell pipeline AST in v1.
- Argv array only.
- Per-executable argument policy; reject flags that spawn commands or load arbitrary configuration.
env_clear()plus a fixed minimal environment.- Fixed executable path opened as a file descriptor; no
PATHlookup. - Child in its own process group; timeout/cancel kills the whole tree.
- The gateway must not spawn
rg/fddirectly. It invokesanvil-exec-runner, which applies Landlock (workspace + system runtime read-only), seccomp (no network/ptrace/privilege changes), andno_new_privsbeforeexecveat. - Landlock must deny a symlink swapped after gateway validation;
openat2is only defense in depth because the child reopens paths itself. - A symlink-race test must prove that a workspace link pointing outside cannot
be read by
rgorfd. - Output and process count limits.
Attack: plugin calls io.open, os.execute, package.loadlib, a native C
module, a QuickJS native binding, or directly invokes a syscall.
Required:
- Plugin host is a separate process.
- Lua/QuickJS standard libraries are reduced to an explicit allowlist; no
io,os.execute,package.loadlib,debugescape surface, or bytecode loading. - No native plugin modules in v1.
- Landlock (filesystem, and network where available), seccomp,
no_new_privs. - Plugin host starts with one plugin's capabilities only.
- If sandbox setup fails, do not start the plugin.
Attack: another local process connects to the gateway or editor socket and pretends to be a plugin.
Required:
- Per-session runtime directory with
0700; socket0600. SO_PEERCREDto identify the peer process.- Per-plugin capability token issued by the editor and registered with the gateway.
- Gateway checks token, plugin id, workspace, and requested operation.
- One plugin per host process so a compromised host cannot impersonate a sibling.
Attack: infinite loop, allocation flood, thousands of child processes, unbounded response bodies, file descriptor exhaustion.
Required:
- Lua/QuickJS instruction and memory limits.
- Wall-clock deadline and cancellation.
- Per-plugin process, memory, fd, and output quotas.
- Gateway body/header/request limits.
- Bounded queues and backpressure; reject rather than buffer without limit.
Attack: reload a plugin while it holds a file, socket, or child process; leave a zombie or a process writing files after revocation.
Required:
- Reload is: stop new requests, cancel in-flight ids, kill process group, wait for host exit, revoke token, start a new host, issue a new token.
- No
Drop-only cleanup for external resources. - The gateway records
cancelled_by_reload. - Leak test: repeated reloads with RSS, fd, and child-process assertions.
Attack: a plugin update adds permissions, changes its own manifest, or reuses an old approval.
Required:
- Manifest permissions are requests.
- User grants per workspace; grants are keyed by plugin id and manifest hash.
- New permissions require new consent.
- Signed manifest or at minimum a lockfile with hash.
- Display requested file roots, hosts, and executables before approval.
- No plugin can read/write outside the workspace through
sys.*. - No plugin can reach a non-granted host, port, or method.
- No plugin can execute a shell or a non-granted executable/argument pattern.
- No plugin can outlive cancellation or leave a child process behind.
- No plugin can bypass the host sandbox by using a language standard library.
- No local process can impersonate a plugin without its capability token.
- A failed sandbox setup fails closed.
- A stale snapshot cannot mutate the editor buffer.
| Area | Tests |
|---|---|
| Paths | .., absolute, symlink escape, magic link, root symlink, EAGAIN race, hard link |
| Network | private IPv4/IPv6, metadata IP, CGNAT, DNS rebinding, redirect, gzip bomb, wrong SNI, proxy env |
| Exec | shell string, --pre, --exec, env injection, PATH swap, symlink race, runner sandbox failure, timeout, descendant kill |
| Plugin host | removed stdlib functions, native module load, memory limit, CPU interrupt |
| IPC | wrong token, replayed token, wrong workspace, concurrent cancel |
| Reload | 1000 reloads, fd/RSS/child checks |
Even with the design above:
- Landlock is unavailable before Linux 5.13; untrusted plugins must be disabled or the baseline raised.
- Landlock authorizes objects, not names; hard links can intentionally share access.
- Lua/QuickJS are C/C++ runtimes; a memory-safety bug in the VM can break the host. Kernel sandboxing limits the impact but does not make the host trusted.
- HTTPS certificate validation ultimately trusts the system CA store.
- A compromised editor binary can bypass every policy in this document.
These are accepted only if documented in the release notes and covered by the Phase 0 risk review.