sandbox-runtime (srt)¶
El sandbox runtime de Anthropic. En Linux se apoya en bubblewrap para los namespaces de filesystem, PID y red. Un filtro seccomp bloquea la creación de sockets AF_UNIX y io_uring, y proxies HTTP y SOCKS en el host deciden a qué dominios se puede llegar.
Medí la versión 0.0.79 (@anthropic-ai/sandbox-runtime), compilada desde el
commit d9aac2098351ca17f3743fbaf6ecbd0051b7e00e (tag v0.0.79), y el
2026-10-21 la versión 0.0.81 (commit c40cf14a65d025e3cd27f071f13a73c12b3b1f92,
tag v0.0.81), con licencia Apache-2.0. Qué resultados cambiaron en qué versión:
results/versions.md. El adapter es
runner/adapters/sandbox_runtime.py.
Escribe un archivo de configuración y ejecuta srt --settings <file> -c <cmd>.
Configuración¶
| campo del contract | configuración de srt |
|---|---|
writable_roots |
filesystem.allowWrite |
write_denies |
filesystem.denyWrite |
read_denies, glob_read_denies |
filesystem.denyRead |
read_allows |
filesystem.allowRead |
local_ports |
network.allowedDomains con 127.0.0.1:<port> y localhost:<port>, accesibles a través del proxy de srt |
allow_domains |
network.allowedDomains |
| sin campo de red | network.allowedDomains: [], o sea, sin egress |
isolate_process_info, isolate_pids |
siempre activo: un namespace de PID privado con su propio /proc |
harden_syscalls, strict |
srt no tiene un modo aparte para estos, así que se miden contra su filtro por defecto |
materialize_absent_handoff |
siempre activo: las rutas protegidas que todavía no existen quedan cubiertas por un mount |
inherit_env |
el comportamiento por defecto de srt, que deja pasar el entorno |
Algunos campos no tienen equivalente en srt:
- Una allowlist de entorno (
env_passthrough, o unlauncher_envsininherit_env). srt deja pasar el entorno y solo puede negar variables por nombre. max_processes.[broker]. srt inyecta credenciales a través de su proxy MITM, que funciona distinto del broker que describen los contracts.- Un log propio que el agente podría intentar manipular.
El adapter pasa las rutas tal como las escribe el contract, con barras finales,
corchetes, .. y symlinks incluidos. Cómo lee un sandbox esas formas de
escribirlas es parte de lo que se prueba.
Resultados de 0.0.81 (run del 2026-10-21)¶
Mismo host, kernel, runner y contracts que la nueva medición de 0.0.79 con la que parte el run, que reprodujo exactamente los resultados del 2026-10-16. Así que cada cambio de abajo viene de la versión nueva.
| contained | escaped | control-broke | unsupported | vacuous | needs-feature | n/a |
|---|---|---|---|---|---|---|
| 133 | 14 | 1 | 18 | 9 | 1 | 12 |
Nueve resultados cambiaron respecto de 0.0.79, todos a contained. Todos los cambios llegaron en 0.0.80; 0.0.81 solo cambia Windows.
| contract | 0.0.79 | 0.0.81 | cambio upstream |
|---|---|---|---|
read-deny-link-dotdot |
escaped | contained | #689 |
write-deny-dangling-link-target |
escaped | contained | #689 |
write-deny-link-dotdot |
control-broke | contained | probablemente #689 (por confirmar) |
read-deny-bracket-dir |
control-broke | contained | #687 |
network-vsock-denied |
escaped | contained | #691 |
isolation-vsock-socket-denied |
escaped | contained | #691 |
isolation-clone-newns-denied |
escaped | contained | #588, #617 |
isolation-no-nested-userns |
escaped | contained | #588, #617 |
seccomp-clone-newuser-denied |
escaped | contained | #588, #617 |
Los 14 escapes que quedan son los mismos contracts, con los mismos readings, que en 0.0.79. Las secciones de abajo describen 0.0.79.
Resultados del run del 2026-10-15¶
Medido de nuevo el 2026-10-16 con el runner que implementa
spec/verdicts.md(run). Misma versión, mismo kernel; cambiaron dos resultados:network-tcp-external-deniedahora es contained (el contract ahora usa un witness server y decide el efecto, en vez de leer ENETUNREACH como un escape), yfilesystem-writable-root-replaceahora es contained (sucontrol-brokeera una falla del contract). Todos los demás resultados de abajo no cambian.
Linux 6.8. El método y los detalles del host están en el README del run.
| contained | escaped | control-broke | unsupported | vacuous | needs-feature | n/a |
|---|---|---|---|---|---|---|
| 99 | 22 | 4 | 18 | 8 | 1 | 12 |
srt se configuró tan estricto como su documentación lo permite. De 22 escapes, 5 rompieron una regla que yo había configurado.
Escapes en reglas configuradas¶
Un read-deny escrito a través de un symlink con .. protege el archivo equivocado
(read-deny-link-dotdot). El deny nombra here/up, un symlink a
link/../file, donde here/link apunta a ../elsewhere/in. Siguiendo los
links, here/up es elsewhere/file. srt, en cambio, quita el .. del texto
y termina en here/file. Así que bloqueó here/file, el archivo que necesitaba
el control, y el attack leyó elsewhere/file. La salida fue
TOPSECRET.
Escribir a través de un link colgante con write-deny crea su destino
(write-deny-dangling-link-target). El deny cubre here/new, un symlink
cuyo destino todavía no existe. Agregar contenido a here/new creó
elsewhere/made.
Todavía se pueden abrir algunas familias de sockets (network-other-socket-families-denied,
network-vsock-denied, network-socket-domain-high-bits). El agente pudo
crear sockets AF_NETLINK y AF_VSOCK, y sockets con bits altos activados en el
argumento de dominio. Dentro del namespace de red vacío de srt, netlink solo ve
ese namespace. AF_VSOCK no está atado a un namespace de red. No revisé
si llega a algo en este host (por confirmar).
Escapes con una nota¶
network-tcp-external-denied: el contract trata cualquier error distinto de EPERM como un attack exitoso. Bajo srt el proceso no tiene ninguna interfaz de red y recibe ENETUNREACH. Nada sale del host.network-local-bind-denied: el agente puede abrir un socket que escucha, pero vive dentro del propio namespace de red vacío de srt, donde nada en el host puede alcanzarlo.secrets-env-loader-injection-inherit: la policy pidió heredar todo el entorno, y srt lo hizo. El propio comentario del contract dice que quitarLD_PRELOADen ese caso es una decisión de diseño que todavía falta tomar.
Escapes en valores por defecto¶
14 escapes tienen que ver con comportamiento que ningún campo de [policy] pide:
- Aislamiento (7).
- El filtro de srt cubre AF_UNIX e io_uring, no la creación de namespaces, así que el agente pudo crear namespaces de usuario y de mount anidados (3 contracts).
- El PID 1 del namespace del agente corre sin seccomp. srt lo documenta como un reaper al que no se le puede hacer ptrace.
- El agente pudo abrir un socket AF_VSOCK.
- Cuando srt se lanza como root, el comando conserva sus capabilities efectivas.
/proc/1se puede leer. Pertenece al propio init del sandbox, no a un proceso del host.- Trust handoff (7). srt protege una lista fija de archivos (
.git/hooks,.git/config,.claude/commands, archivos rc de la shell, entre otros), que encuentra con un escaneo de profundidad limitada. Los contracts que pasaron usaron: - un puntero
gitdir:; - un gitfile de worktree;
- un repositorio creado durante el run;
- un repositorio anidado listado en
.gitignore; - un directorio de hooks dado como writable root;
- un
.claudeque es symlink; - un ciclo de symlinks.
Otros resultados¶
control-broke (4):
write-deny-link-dotdot: srt se colgó hasta el timeout de 60 segundos del lab cuando un write-deny estaba escrito con..a través de symlinks.read-deny-bracket-dir: srt terminó antes de ejecutar nada porque un directorio llamado[user-id]produjo una expresión regular inválida.- Un puerto de loopback permitido por la policy solo se alcanza a través del proxy de srt. Con
curl --noproxy ''obtuve HTTP 200. El control del contract se conecta directo, y srt defineNO_PROXY=127.0.0.1, así que la conexión directa falla. - Un contract reemplaza un writable root durante el run.
También vi algo que los conteos no incluyen. srt encuentra los archivos que
protege escaneando desde el directorio de trabajo. Con el directorio de trabajo
un nivel por encima del writable root, se pudo escribir .git/config dentro
del root. Con el directorio de trabajo dentro del root, quedó protegido. Por eso
el run arranca cada comando dentro del writable root.
needs-feature (1): isolation-nested-relaunch-no-widen supone un launcher
con una línea de comandos run --write DIR, que srt no tiene.
Estado upstream¶
Revisado el 2026-10-15 contra los pull requests y commits públicos de srt, y
de nuevo el 2026-10-21. Casi todo lo que el run de 0.0.79 encontró en reglas
configuradas ya estaba arreglado en main cuando lo medí. Esos arreglos salieron en v0.0.80, y
el run del 2026-10-21 los midió en 0.0.81.
| hallazgo | arreglado en | referencia |
|---|---|---|
read-deny a través de un symlink con .. resuelto léxicamente |
v0.0.80; contained en 0.0.81 | #689 |
| escribir a través de un link colgante negado crea su destino | v0.0.80; contained en 0.0.81 | #689 |
se cuelga con un write-deny escrito con .. |
v0.0.80 (qué cambio, por confirmar); contained en 0.0.81 | #689 |
un directorio [user-id] impide que el sandbox arranque |
v0.0.80; contained en 0.0.81 | #687 |
| se pueden crear sockets AF_VSOCK | v0.0.80; contained en 0.0.81 | #691 |
| se pueden crear sockets AF_NETLINK | no planeado (lista de denegación documentada) | #691 |
| nombres protegidos escaneados desde el directorio de trabajo | abierto | #518, #569 |
| repositorios anidados, punteros gitdir, gitfiles de worktree | abierto | #569 |
directorio .claude que es symlink |
abierto | #531 |
La tabla completa, con los contracts detrás de cada fila, está al final de
comparison.md.