Un sandbox Linux léger et multicouche combinant les namespaces, pivot_root, seccomp-bpf, la suppression de capacités (capability dropping) et un moteur de verdict basé sur les preuves (Truthimatics Public Version) pour une exécution de code sécurisée et vérifiable.
Sandbox multicouche pour l'exécution de code natif sur Linux.
Sept couches de défense indépendantes — aucune dépendance externe, binaire PIE d'environ 73 Kio.
┌──────────────────────────────────────────────────────┐
│ Z-Jail │
├──────────────────────────────────────────────────────┤
│ Truthimatics PV (moteur de verdict factuel) │
│ Namespaces (mount, pid, net, ipc, uts) │
│ pivot_root (chroot amélioré) │
│ Capabilities (tout abandonner, verrouiller securebits) │
│ NO_NEW_PRIVS (pas d'élévation de privilèges) │
│ seccomp-BPF (liste blanche : 15 appels système uniquement) │
│ Audit (journalisation JSON + hachage BLAKE2b) │
└──────────────────────────────────────────────────────┘
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
Le répertoire --root doit contenir un système de fichiers minimal avec le binaire cible et ses dépendances (pour les binaires statiques, seul le binaire suffit).
Les solutions de sandboxing existantes font des compromis :
| Z-Jail | Firecracker | gVisor | bwrap | nsjail | |
|---|---|---|---|---|---|
| Dépendances externes | zéro | libc, seccomp | Runtime Go | libc | libc, protobuf |
| Taille du binaire | ~73 Kio | 20+ Mio | 40+ Mio | ~70 Kio | ~1 Mio |
| Isolation VM | non | oui (microVM) | non (sandbox) | non | non |
| Liste blanche seccomp | oui | non | oui | optionnel | oui |
| Hachage du contenu | oui | non | non | non | non |
| Audit JSON | oui | non | oui | non | partiel |
| Complexité de construction | un make | complexe | complexe | trivial | modérée |
Z-Jail comble le vide entre bwrap (minimal, pas de seccomp par défaut) et nsjail (riche en fonctionnalités, dépendances lourdes). Il est conçu pour les pipelines CI, les challenges de jail CTF et l'évaluation légère de code où vous avez besoin d'une défense en profondeur sans intégrer un environnement d'exécution de conteneurs.
flowchart LR
CLI[Arguments CLI] --> P[parse_args]
P --> C{clone espaces de noms}
C -->|enfant| CR[child_run]
C -->|parent| W[waitpid]
CR --> RL[setrlimit]
RL --> FD[fermer fd >= 3]
FD --> DUMP[PR_SET_DUMPABLE=0]
DUMP --> PV[pivot_root]
PV --> NNP[PR_SET_NO_NEW_PRIVS]
NNP --> CAP[abandonner capacités]
CAP --> SC[seccomp-BPF]
SC --> SIG[signaler au parent]
SIG --> EX[execve cible]
W --> A[audit JSON]
A --> EXIT[sortie]
Chaque couche est ordonnée de sorte qu'une couche ultérieure ne puisse pas être annulée par une précédente :
capset après ce pointsequenceDiagram
participant P as Parent
participant C as Enfant
P->>C: clone (NEWNS|NEWPID|NEWNET|NEWIPC|NEWUTS)
Note over C: setrlimit(CPU, AS, NOFILE, NPROC)
Note over C: close(tous fd > 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(tout à zéro) + securebits
Note over C: seccomp(SECCOMP_MODE_FILTER, liste blanche)
C->>P: write(pipe, ready=1)
Note over C: execve(cible)
P->>P: waitpid
P->>P: écrire audit JSON
Moteur de verdict fondé sur des preuves. Collecte des observations pondérées sur le binaire exécuté et détermine un verdict final (DETERMINISTIC, REJECT ou UNCERTAIN). Chaque observation porte un poids ; toute observation unique dont le poids > 50 % du total décide du verdict.
Cinq espaces de noms sont créés via clone() :
| Espace de noms | Drapeau | But |
|---|---|---|
| Montage | CLONE_NEWNS | Arbre de fichiers isolé |
| PID | CLONE_NEWPID | Espace d'ID de processus (l'enfant est pid 1) |
| Réseau | CLONE_NEWNET | Aucune interface réseau |
| IPC | CLONE_NEWIPC | Pas de mémoire partagée / sémaphores |
| UTS | CLONE_NEWUTS | Nom d'hôte séparé |
Nécessite CAP_SYS_ADMIN dans l'espace de noms initial.
Remplace la racine de l'espace de noms de montage par le répertoire --root :
MS_BIND|MS_REC)pivot_root(new_root, put_old) — échange l'arbre de montagechdir("/") — se déplacer dans la nouvelle racineumount2("/.pivot_old", MNT_DETACH) — détacher l'ancienne racinermdir("/.pivot_old") — nettoyerC'est strictement plus fort que chroot(2) — le processus sandboxé ne peut pas s'échapper vers la racine hôte, même avec CLONE_NEWNS depuis l'intérieur du sandbox (qui est déjà bloqué par seccomp).
Toutes les capacités sont abandonnées via :
capset(hdr, data) // data = {0, 0, 0}
prctl(SECBIT_KEEP_CAPS_LOCKED | SECBIT_NO_SETUID_FIXUP | ...)
Le processus abandonne setuid/setgid avant capset afin que le changement d'UID prenne effet alors que CAP_SETUID est encore détenu. Après capset, toutes les capacités ont disparu et les securebits sont verrouillés — aucune réactivation possible.
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
Empêche le processus ou ses enfants d'obtenir de nouveaux privilèges via les binaires setuid, les capacités de fichiers ou les transitions LSM. Irréversible.
Liste blanche de 15 appels système — tout ce qui n'est pas dans la liste reçoit SECCOMP_RET_KILL :