
Un sandbox Linux ligero y multicapa que combina namespaces, pivot_root, seccomp-bpf, supresión de capacidades (capability dropping) y un motor de veredictos basado en evidencia (Truthimatics Public Version) para una ejecución de código segura y auditable.
Sandbox multicapa para ejecución de código nativo en Linux.
Siete capas de defensa independientes — sin dependencias externas, binario PIE de ~73 KiB.
┌──────────────────────────────────────────────────────┐
│ Z-Jail │
├──────────────────────────────────────────────────────┤
│ Truthimatics PV (evidence-based verdict engine) │
│ Namespaces (mount, pid, net, ipc, uts) │
│ pivot_root (chroot on steroids) │
│ Capabilities (drop all, lock securebits) │
│ NO_NEW_PRIVS (no privilege escalation) │
│ seccomp-BPF (whitelist: 15 syscalls only) │
│ Audit (JSON logging + BLAKE2b hashing) │
└──────────────────────────────────────────────────────┘
git clone https://github.com/Division-36/Z-Jail.git
cd Z-Jail
make
sudo ./z_jail --root=/path/to/rootfs --seccomp-enforce -- /bin/ls
El directorio --root debe contener un sistema de archivos mínimo con el binario objetivo y sus dependencias (para binarios estáticos, basta con el binario).
Las soluciones de sandboxing existentes implican concesiones:
Z-Jail ocupa el nicho entre bwrap (mínimo, sin seccomp por defecto) y nsjail (con muchas funciones, dependencias pesadas). Está diseñado para pipelines de CI, retos CTF de jail y evaluación ligera de código donde necesitas defensa en profundidad sin incorporar un runtime de contenedores.
flowchart LR
CLI[CLI args] --> P[parse_args]
P --> C{clone namespaces}
C -->|child| CR[child_run]
C -->|parent| W[waitpid]
CR --> RL[setrlimit]
RL --> FD[close fds >= 3]
FD --> DUMP[PR_SET_DUMPABLE=0]
DUMP --> PV[pivot_root]
PV --> NNP[PR_SET_NO_NEW_PRIVS]
NNP --> CAP[drop capabilities]
CAP --> SC[seccomp-BPF]
SC --> SIG[signal parent]
SIG --> EX[execve target]
W --> A[audit JSON]
A --> EXIT[exit]
Cada capa está ordenada de modo que una capa posterior no pueda ser deshecha por una anterior:
capset a partir de este puntosequenceDiagram
participant P as Parent
participant C as Child
P->>C: clone (NEWNS|NEWPID|NEWNET|NEWIPC|NEWUTS)
Note over C: setrlimit(CPU, AS, NOFILE, NPROC)
Note over C: close(all fds > 2)
Note over C: PR_SET_DUMPABLE=0
Note over C: pivot_root → chdir("/") → umount -l
Note over C: PR_SET_NO_NEW_PRIVS
Note over C: capset(all zero) + securebits
Note over C: seccomp(SECCOMP_MODE_FILTER, whitelist)
C->>P: write(pipe, ready=1)
Note over C: execve(target)
P->>P: waitpid
P->>P: write audit JSON
Motor de veredictos basado en evidencia. Recopila observaciones ponderadas sobre el binario ejecutado y determina un veredicto final (DETERMINISTIC, REJECT o UNCERTAIN). Cada observación lleva un peso; cualquier observación individual con un peso >50% del total decide el veredicto.
Se crean cinco espacios de nombres mediante clone():
Requiere CAP_SYS_ADMIN en el espacio de nombres inicial.
Reemplaza la raíz del espacio de nombres de montaje con el directorio --root:
MS_BIND|MS_REC)pivot_root(new_root, put_old) — intercambia el árbol de montajeschdir("/") — se mueve a la nueva raízumount2("/.pivot_old", MNT_DETACH) — desmonta la raíz antiguarmdir("/.pivot_old") — limpiaEsto es estrictamente más fuerte que chroot(2) — no hay forma de que el proceso en el sandbox escape de vuelta a la raíz del host, ni siquiera con CLONE_NEWNS desde dentro del sandbox (que ya está bloqueado por seccomp).
Todas las capacidades se eliminan mediante:
capset(hdr, data) // data = {0, 0, 0}
prctl(SECBIT_KEEP_CAPS_LOCKED | SECBIT_NO_SETUID_FIXUP | ...)
El proceso elimina setuid/setgid antes de capset para que el cambio de uid tenga efecto mientras aún se mantiene CAP_SETUID. Después de capset, todas las capacidades desaparecen y los securebits quedan bloqueados; no es posible volver a habilitarlas.
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
Impide que el proceso o sus hijos obtengan nuevos privilegios mediante binarios setuid, capacidades de archivo o transiciones LSM. Irreversible.
Lista blanca de 15 syscalls: cualquier cosa que no esté en la lista recibe SECCOMP_RET_KILL:
El filtro BPF se genera dinámicamente: para cada entrada de la lista blanca se emite una cadena de saltos que permite (si la syscall coincide) o cae hasta KILL. La arquitectura se comprueba primero (AUDIT_ARCH_X86_64).
El filtro se verifica de forma independiente mediante una prueba autónoma (tests/seccomp_filter_test.c, 8/8 pasan) que hace fork+execve de casos de prueba contra un prctl(PR_SET_SECCOMP) real sin necesidad de root.
Cada ejecución produce un registro de auditoría JSON:
{
"schema": "z-jail.audit/v1",
"build_id": "Z-Jail/v1+dev",
"timestamp": 1749000000,
"duration_ns": 8500000,
"executable": "/bin/ls",
"verdict": "DETERMINISTIC",
"exit_code": 0,
"sandbox": {
"seccomp_filter": "whitelist-v1",
"seccomp_whitelist_size": 15,
"seccomp_arg_rules_size": 2,
"namespaces": ["mount","pid","net","ipc","uts"],
"pivot_root": "/var/run/z-jail/roots/default",
"no_new_privs": true,
"capabilities_dropped": true
},
"content_fingerprint": "0e5751c026e543b2e8ab2eb06099daa1..."
}
Se escribe en build/audits/<binary-name>.audit.json. El content_fingerprint es un hash BLAKE2b-256 del binario objetivo, calculado por el padre cuando el hijo termina.
z_jail --root=<dir> [--seccomp-enforce] [--self-hash=<hex>]
[--quiet] [--verbose] -- <program> [args...]
# Run a static binary with all protections
sudo z_jail --root=./roots --seccomp-enforce -- bin/hello_static
# Run with binary integrity verification
sudo z_jail --root=./roots --seccomp-enforce \
--self-hash=$(sha256sum z_jail | cut -c1-64) -- bin/program
# Quiet mode (no audit JSON)
sudo z_jail --root=./roots --quiet -- bin/program
make # build z_jail (~130 KiB PIE binary)
make install # install to /usr/local/bin + man page
make clean # remove build artifacts
make dist # create release tarball
make check # smoke test (--version + --help)
El binario se compila como un ejecutable independiente de posición (PIE) con -fstack-protector-strong, -D_FORTIFY_SOURCE=2, RELRO completo y -z now.
make CC=clang CFLAGS="-O3 -march=native" # custom compiler/flags
# seccomp filter logic (8 tests)
tests/build/seccomp_filter_test
# BLAKE2b known-answer test
tests/build/blake2b_known
Estas no requieren root y se ejecutan en menos de 100 ms.
make -C tests setup # build payloads + test roots
sudo bash tests/run_tests.sh # 17 scenarios
Requiere root para la creación de espacios de nombres. La suite de pruebas cubre:
Medido en WSL2 (kernel 6.18.x-microsoft-standard-WSL2, Kali Linux), 50 muestras por herramienta, carga de trabajo uniforme (un binario estático independiente cuyo cuerpo es exit_group(0)), cronometrado con un harness getrusage. Consulta docs/BENCHMARKS.md para conocer la metodología.
| Métrica | Valor |
|---|---|
| Tamaño del binario | ~73 KiB sin strip (~28 KiB con strip) |
| Latencia media del sandbox | 5.85 ± 1.45 ms (95% CI [5.45, 6.25]) |
| RSS máximo | 1.62 MiB |
En las condiciones probadas, Z-Jail tiene el conjunto residente más pequeño de los tres sandboxes a nivel de proceso y una latencia entre bwrap y nsjail. Bubblewrap es el más rápido, pero no realiza filtrado seccomp por defecto, por lo que hace menos trabajo de preparación; Z-Jail instala una lista blanca seccomp, elimina capacidades y hace pivot_root en cada ejecución. gVisor (runsc) falla con segfault en este kernel WSL2 y no pudo medirse; Firecracker aísla mediante una microVM (arranque en frío de la VM, una métrica diferente) y queda excluido de la tabla fork-to-exec. Estas cifras son de un solo host: trátalas como relativas.
Nota: las cifras documentadas anteriormente (~8 ms, ~4 MiB, ~130 KiB) no coinciden con una compilación actual de
make(~73 KiB, ~5.9 ms) y parecen haber sido inexactas; los números anteriores se volvieron a medir en este código base. Truthimatics sigue siendo parte del código y no se eliminó (los informes antiguos deaxiom_jailsimplemente usaban un nombre de binario y un harness distintos). Un error de propagación de montajes encontrado durante las pruebas comparativas se corrigió ensrc/sandbox.c(MS_REC|MS_PRIVATEantes del bind mount); el remontaje recursivo contribuye a parte de la latencia medida.
chroot, mount, ptrace, socket, process_vm_writevRLIMIT_CPU), agotamiento de memoria (RLIMIT_AS)execvesetuid / enlazador dinámico / LD_PRELOAD/proc, /sysCLONE_NEWNET + socket bloqueadoclone(CLONE_NEWNS|CLONE_NEWPID|...) tiene éxito (requiere CAP_SYS_ADMIN)--root)--self-hash=<hex> está configurado en despliegues de producciónMIT — consulta LICENSE para ver el texto completo.
Z-Jail se construyó en WSL2 (Kali Linux, GCC 15.2.0), orientado a Linux 5.4+. Mantenido por Division-36. Reporta problemas en el rastreador de incidencias.
| Z-Jail | Firecracker | gVisor | bwrap | nsjail |
|---|
| Dependencias externas | cero | libc, seccomp | Go runtime | libc | libc, protobuf |
| Tamaño del binario | ~73 KiB | 20+ MiB | 40+ MiB | ~70 KiB | ~1 MiB |
| Aislamiento de VM | no | sí (microVM) | no (sandbox) | no | no |
| Lista blanca seccomp | sí | no | sí | opcional | sí |
| Hash de contenido | sí | no | no | no | no |
| Auditoría JSON | sí | no | sí | no | parcial |
| Complejidad de compilación | un solo make | compleja | compleja | trivial | moderada |
| Espacio de nombres | Flag | Propósito |
|---|
| Mount | CLONE_NEWNS | Árbol de sistema de archivos aislado |
| PID | CLONE_NEWPID | Espacio de ID de proceso (el hijo es el pid 1) |
| Net | CLONE_NEWNET | Sin interfaces de red |
| IPC | CLONE_NEWIPC | Sin memoria compartida / semáforos |
| UTS | CLONE_NEWUTS | Nombre de host separado |
| Syscall | Número | Notas |
|---|
read | 0 | stdin |
write | 1 | stdout/stderr + pipe de reporte |
openat | 257 | acceso a archivos (no open) |
close | 3 | — |
lseek | 8 | — |
brk | 12 | gestión del heap |
mmap | 9 | con restricción de argumentos: flags & 4 == 0 (sin MAP_SHARED), flags == 0x22 (MAP_PRIVATE|MAP_ANONYMOUS) |
munmap | 11 | — |
execve | 59 | ejecución única al inicio |
exit_group | 231 | salida limpia del proceso |
rt_sigaction | 13 | manejadores de señales |
rt_sigprocmask | 14 | enmascaramiento de señales |
getrandom | 318 | fuente de números aleatorios |
clock_gettime | 228 | temporización |
fstat | 5 | metadatos de archivo |
| Flag | Descripción |
|---|
--root=<dir> | Directorio raíz del sandbox (obligatorio) |
--seccomp-enforce | Habilita la lista blanca de syscalls seccomp-BPF |
--self-hash=<hex> | Verifica que el binario coincide con el hash BLAKE2b-256 esperado |
--quiet | Suprime la salida de auditoría |
--verbose | Habilita el registro de depuración |
--version | Muestra el ID de compilación (Z-Jail/v1+dev) |
--help | Muestra el uso y sale |
| Código | Significado |
|---|
| 0 | El hijo salió normalmente (veredicto: DETERMINISTIC) |
| 1 | El hijo fue eliminado por una señal (veredicto: REJECT) |
| 2 | Self-hash: cadena hex inválida o archivo ilegible |
| 3 | Self-hash: discrepancia (el binario ha sido manipulado) |
| 101 | Error de configuración del hijo (rlimit, etc.) |
| 102 | Falló la instalación del filtro seccomp del hijo |
| 103 | Falló execve del hijo (binario no encontrado, sin permiso de ejecución) |
| 104 | Falló pivot_root del hijo |
| 105 | Falló la eliminación de capacidades del hijo |
| 125 | Falló la creación del espacio de nombres (¿ejecutar como root? ¿soporte del kernel?) |
| # | Escenario | Tipo | Qué prueba |
|---|
| 0 | blake2b_regress | respuesta conocida | Corrección de la implementación de BLAKE2b |
| 1 | seccomp_filter | BPF autónomo | 8 subpruebas de la lógica del filtro BPF |
| 2 | hello_static | correcto | Ejecución básica de un binario estático |
| 3 | hello_dynamic | correcto | Binario dinámico con ld-linux + libc |
| 4 | execve_replacement | correcto | execve en el sandbox (bloqueado por seccomp) |
| 5 | fd_inherited_read | correcto | stdin/stdout heredados correctamente |
| 6 | mmap_bad_flags | eliminado | mmap con MAP_SHARED bloqueado |
| 7 | mmap_good_allowed | correcto | mmap con MAP_PRIVATE|ANONYMOUS permitido |
| 8 | mmap_prot_exec | eliminado | mmap con PROT_EXEC bloqueado |
| 9 | mmap_self_modify | eliminado | Código automodificable bloqueado |
| 10 | ptrace | eliminado | ptrace bloqueado |
| 11 | socket | eliminado | creación de socket bloqueada |
| 12 | chroot_escape | eliminado | syscall chroot bloqueada |
| 13 | double_chroot | eliminado | doble chroot bloqueado |
| 14 | mount_replay | eliminado | syscall mount bloqueada |
| 15 | cpu_exhaust | eliminado | RLIMIT_NPROC bloquea fork bomb |
| 16 | signal_parent | eliminado | Señal al padre bloqueada |
| 17 | self_hash | correcto | Verificación de integridad del binario |
| Líneas de código (núcleo) |
| ~900 |
| Herramienta | Latencia media ± DE | RSS máximo | seccomp por defecto |
|---|
| Z-Jail | 5.85 ± 1.45 ms | 1.62 MiB | sí |
| bwrap | 3.56 ± 0.40 ms | 2.19 MiB | no |
| nsjail | 8.98 ± 1.68 ms | 7.91 MiB | sí |
| Archivo | Descripción |
|---|
README.md | Este archivo |
docs/ARCHITECTURE.md | Descripción general de la arquitectura |
docs/SANDBOX.md | Detalles internos del sandbox capa por capa |
docs/SECCOMP.md | Diseño de la lista blanca seccomp-BPF |
docs/AUDIT_SCHEMA.md | Referencia del esquema JSON de auditoría |
docs/THREAT_MODEL.md | Suposiciones y alcance de seguridad |
docs/BLAKE2B.md | Detalles de implementación de BLAKE2b |
docs/BENCHMARKS.md | Pruebas de rendimiento |
docs/BUILD.md | Instrucciones de compilación |
docs/adr/ | Registros de decisiones de arquitectura (4 documentos) |
man/z_jail.1 | Página del manual |
SECURITY.md | Política de seguridad y cómo reportar |
CONTRIBUTING.md | Cómo contribuir |
CHANGELOG.md | Historial de versiones |
ROADMAP.md | Planes futuros |
TODO.md | Brechas conocidas y trabajo planificado |