codex (codex sandbox)¶
El sandbox de Linux del Codex CLI de OpenAI. Usa bubblewrap para los namespaces
de filesystem, PID, IPC y red, define no_new_privs y aplica un filtro seccomp
que bloquea io_uring y ptrace, más las llamadas al sistema de red cuando la
red está restringida.
Medí codex-cli 0.162.1 (@openai/codex), compilado desde el commit
092d3acd6bec3e3a14bdc7e7a2810ab628ab759d (tag rust-v0.162.1), y el
2026-10-21 codex-cli 0.163.0 (commit 83086188ae0614bd882818ff1aa23f3194962012,
tag rust-v0.163.0), con licencia Apache-2.0. Qué resultados cambiaron en qué
versión: results/versions.md. Llamé directamente a su binario nativo x86_64-unknown-linux-musl
en vez del launcher de npm. Ese launcher es un script de Node, y un contract
que define NODE_OPTIONS en el entorno rompería el launcher antes de que el
sandbox llegara a arrancar. El adapter es runner/adapters/codex.py.
Escribe un profile de permisos con nombre en un $CODEX_HOME/config.toml temporal
y ejecuta codex sandbox -P conformance -C <workspace> -- sh -c <cmd>.
Configuración¶
| campo del contract | profile de permisos |
|---|---|
| acceso de lectura a todo lo que no esté negado (el valor por defecto de los contracts) | ":root" = "read" |
writable_roots |
"<path>" = "write" |
write_denies |
"<path>" = "read". Cuando las entradas se superponen, gana la ruta más específica |
read_denies, glob_read_denies |
"<path>" = "deny" |
read_allows |
"<path>" = "read" |
| sin campo de red | network.enabled = false |
allow_domains |
el experimental features.network_proxy, más network.domains |
allowlist de entorno, env_passthrough |
shell_environment_policy con include_only en PATH, HOME, LANG y TERM, más las variables que se dejen pasar, e ignore_default_excludes = false |
inherit_env |
shell_environment_policy.inherit = "all" |
isolate_process_info, isolate_pids |
siempre activo: --unshare-pid con su propio /proc |
harden_syscalls, strict |
codex no tiene un modo aparte para estos, así que se miden contra su filtro por defecto |
Algunos campos no tienen equivalente en codex:
local_ports. La red puede estar activada, desactivada o pasar por un proxy por dominio, pero codex no puede abrir un solo puerto de loopback.max_processes.materialize_absent_handoff.[broker].- Un log propio que el agente podría intentar manipular.
Resultados de 0.163.0 (run del 2026-10-21)¶
Ningún resultado cambió respecto de 0.162.1: el run primero volvió a medir 0.162.1 en el mismo host, runner y contracts, y las dos versiones dan el mismo resultado en los 188 contracts. Todo lo de abajo vale para 0.163.0.
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; cambió un resultado:filesystem-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 |
|---|---|---|---|---|---|---|
| 74 | 24 | 27 | 18 | 8 | 1 | 12 |
codex se configuró tan estricto como su documentación lo permite. De los escapes, tres rompieron una regla que yo había configurado.
Escapes en reglas configuradas¶
Un writable root dentro de un directorio con write-deny sigue siendo escribible
(write-deny-covers-nested-root). codex resuelve las entradas superpuestas quedándose con
la más específica, así que el root anidado gana sobre el deny que lo cubre.
Se sigue un writable root que es un symlink (fs-writable-root-symlink-out,
fs-writable-root-symlink-parent). Si un writable root es un symlink que apunta
fuera de los otros roots, su destino pasa a ser escribible. El contract espera que el
sandbox rechace un root así. codex lo acepta.
Escapes con una nota¶
secrets-env-loader-injection-inherit: la policy pidió heredar todo el entorno, y codex 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¶
20 escapes tienen que ver con comportamiento que ningún campo de [policy] pide:
- Trust handoff (13). codex deja
.git,.codex,.agentsy.awscomo solo lectura en el nivel superior de cada writable root. En estos contracts el repositorio está un nivel por debajo del root, como estaría en un workspace con varios repositorios, y la protección no llega hasta ahí. - Aislamiento (5).
- El agente pudo crear namespaces de usuario anidados (3 contracts).
- El agente pudo abrir
/proc/<pid>/memoenvironde un ancestro. /proc/1se puede leer. Pertenece al propio init del sandbox, no a un proceso del host.- Ciclo de vida (2).
.mcp.jsonno está en la lista de archivos protegidos de codex.- Después de que le envié SIGTERM a
codex sandbox, los procesos de adentro siguieron corriendo y escribieron el archivo marcador.
Otros resultados¶
codex tuvo 27 resultados control-broke, más que cualquiera de los otros dos. En
21 de ellos, codex se negó a construir el sandbox y no ejecutó nada. Eso pasó
cuando una entrada era un symlink, pasaba por uno, nombraba una ruta que no
existía, era un solo archivo o contenía caracteres de glob como
[WIP] project. Los errores fueron cannot enforce sandbox read-only path… y
bwrap: Can't mount tmpfs…. El attack quedó detenido, pero también el trabajo
legítimo, y en la práctica el usuario recibiría un error en vez de un sandbox. Los
otros 6 fallaron de otras maneras:
- En dos contracts, un read-allow dentro de un read-deny siguió sin poder leerse.
- Tres controls necesitan llamadas al sistema o familias de sockets que el filtro de codex bloquea.
- Un contract reemplaza un writable root durante el run.
needs-feature (1): isolation-nested-relaunch-no-widen supone un launcher
con una línea de comandos run --write DIR, que codex no tiene.