
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 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() :
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 :
Le filtre BPF est généré dynamiquement : pour chaque entrée de la liste blanche, une chaîne de sauts est émise qui soit autorise (si l'appel système correspond), soit tombe sur KILL. L'architecture est vérifiée en premier (AUDIT_ARCH_X86_64).
Le filtre est vérifié indépendamment par un test autonome (tests/seccomp_filter_test.c, 8/8 réussis) qui fork+exécute des cas de test contre un vrai prctl(PR_SET_SECCOMP) sans nécessiter root.
Chaque exécution produit un enregistrement d'audit 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..."
}
Écrit dans build/audits/<nom-binaire>.audit.json. Le content_fingerprint est un hachage BLAKE2b-256 du binaire cible, calculé par le parent après la fin de l'enfant.
z_jail --root=<rép> [--seccomp-enforce] [--self-hash=<hex>]
[--quiet] [--verbose] -- <programme> [args...]
# Exécuter un binaire statique avec toutes les protections
sudo z_jail --root=./roots --seccomp-enforce -- bin/hello_static
# Exécuter avec vérification d'intégrité du binaire
sudo z_jail --root=./roots --seccomp-enforce \
--self-hash=$(sha256sum z_jail | cut -c1-64) -- bin/program
# Mode silencieux (pas de JSON d'audit)
sudo z_jail --root=./roots --quiet -- bin/program
make # construire z_jail (~130 Kio binaire PIE)
make install # installer dans /usr/local/bin + page de manuel
make clean # supprimer les artefacts de construction
make dist # créer une archive de distribution
make check # test de fumée (--version + --help)
Le binaire est construit comme un exécutable indépendant de la position avec -fstack-protector-strong, -D_FORTIFY_SOURCE=2, RELRO complet et -z now.
make CC=clang CFLAGS="-O3 -march=native" # compilateur/drapeaux personnalisés
# Logique du filtre seccomp (8 tests)
tests/build/seccomp_filter_test
# Test de réponse connue BLAKE2b
tests/build/blake2b_known
Ces tests ne nécessitent pas root et s'exécutent en moins de 100 ms.
make -C tests setup # construire les charges utiles + racines de test
sudo bash tests/run_tests.sh # 17 scénarios
Nécessite root pour la création d'espaces de noms. La suite de tests couvre :
Mesurées sur WSL2 (noyau 6.18.x-microsoft-standard-WSL2, Kali Linux),
50 échantillons par outil, charge de travail uniforme (un binaire statique autonome dont
le corps est exit_group(0)), chronométré avec un harnais getrusage. Voir
docs/BENCHMARKS.md pour la méthodologie.
| Métrique | Valeur |
|---|---|
| Taille du binaire | ~73 Kio non strippé (~28 Kio strippé) |
| Latence moyenne du sandbox | 5,85 ± 1,45 ms (IC 95 % [5,45, 6,25]) |
| RSS de pointe | 1,62 Mio |
Dans les conditions testées, Z-Jail a le plus petit ensemble résident des
trois sandbox au niveau processus et une latence entre bwrap et nsjail.
Bubblewrap est le plus rapide mais n'effectue aucun filtrage seccomp par défaut, donc
il effectue moins de travail d'initialisation ; Z-Jail installe une liste blanche seccomp, abandonne
les capacités et effectue pivot_root à chaque exécution. gVisor (runsc)
provoque un segfault sur ce noyau WSL2 et n'a pas pu être mesuré ; Firecracker
isole via une microVM (démarrage à froid de VM, une métrique différente) et est exclu
du tableau fork-to-exec. Ce sont des chiffres sur un seul hôte — traitez-les comme relatifs.
Remarque : les chiffres documentés plus anciens (~8 ms, ~4 Mio, ~130 Kio) ne correspondent pas à une construction
makeactuelle (~73 Kio, ~5,9 ms) et semblent avoir été inexacts ; les chiffres ci-dessus ont été remesurés sur ce codebase. Truthimatics fait toujours partie du code et n'a pas été supprimée (les anciens rapportsaxiom_jailutilisaient simplement un nom de binaire et un harnais différents). Un bogue de propagation de montage découvert lors du benchmarking a été corrigé danssrc/sandbox.c(MS_REC|MS_PRIVATEavant le montage lié) ; le remontage récursif contribue en partie à la latence mesurée.
chroot, mount, ptrace, socket, process_vm_writevRLIMIT_CPU), épuisement mémoire (RLIMIT_AS)execvesetuid / éditeur de liens dynamique / LD_PRELOAD/proc, /sysCLONE_NEWNET + socket bloqué fournitclone(CLONE_NEWNS|CLONE_NEWPID|...) réussit (nécessite CAP_SYS_ADMIN)--root)--self-hash=<hex> est configuré dans les déploiements de productionMIT — voir LICENSE pour le texte complet.
Z-Jail a été construit sur WSL2 (Kali Linux, GCC 15.2.0), ciblant Linux 5.4+. Maintenu par Division-36. Signalez les problèmes sur le suivi de tickets.
| 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 |
| 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é |
| Appel système | Numéro | Notes |
|---|
read | 0 | stdin |
write | 1 | stdout/stderr + tube de rapport |
openat | 257 | accès fichiers (pas open) |
close | 3 | — |
lseek | 8 | — |
brk | 12 | gestion du tas |
mmap | 9 | arguments restreints : flags & 4 == 0 (pas de MAP_SHARED), flags == 0x22 (MAP_PRIVATE|MAP_ANONYMOUS) |
munmap | 11 | — |
execve | 59 | exec unique au démarrage |
exit_group | 231 | sortie propre du processus |
rt_sigaction | 13 | gestionnaires de signaux |
rt_sigprocmask | 14 | masquage de signaux |
getrandom | 318 | source de nombres aléatoires |
clock_gettime | 228 | chronométrage |
fstat | 5 | métadonnées de fichier |
| Drapeau | Description |
|---|
--root=<rép> | Répertoire racine du sandbox (obligatoire) |
--seccomp-enforce | Activer la liste blanche d'appels système seccomp-BPF |
--self-hash=<hex> | Vérifier que le binaire correspond au hachage BLAKE2b-256 attendu |
--quiet | Supprimer la sortie d'audit |
--verbose | Activer les journaux de débogage |
--version | Afficher l'ID de construction (Z-Jail/v1+dev) |
--help | Afficher l'utilisation et quitter |
| Code | Signification |
|---|
| 0 | L'enfant s'est terminé normalement (verdict : DETERMINISTIC) |
| 1 | L'enfant a été tué par un signal (verdict : REJECT) |
| 2 | Self-hash : chaîne hexadécimale invalide ou fichier illisible |
| 3 | Self-hash : non-concordance (le binaire a été modifié) |
| 101 | Erreur de configuration de l'enfant (rlimit, etc.) |
| 102 | Échec de l'installation du filtre seccomp de l'enfant |
| 103 | Échec de execve de l'enfant (binaire introuvable, pas de permission d'exécution) |
| 104 | Échec de pivot_root de l'enfant |
| 105 | Échec de l'abandon des capacités de l'enfant |
| 125 | Échec de la création de l'espace de noms (exécuter en root ? support du noyau ?) |
| # | Scénario | Type | Ce qu'il teste |
|---|
| 0 | blake2b_regress | réponse connue | Exactitude de l'implémentation BLAKE2b |
| 1 | seccomp_filter | BPF autonome | 8 sous-tests de la logique du filtre BPF |
| 2 | hello_static | ok | Exécution de binaire statique de base |
| 3 | hello_dynamic | ok | Binaire dynamique avec ld-linux + libc |
| 4 | execve_replacement | ok | execve dans le sandbox (bloqué par seccomp) |
| 5 | fd_inherited_read | ok | stdin/stdout hérités correctement |
| 6 | mmap_bad_flags | tué | mmap avec MAP_SHARED bloqué |
| 7 | mmap_good_allowed | ok | mmap avec MAP_PRIVATE|ANONYMOUS autorisé |
| 8 | mmap_prot_exec | tué | mmap avec PROT_EXEC bloqué |
| 9 | mmap_self_modify | tué | Code auto-modifiant bloqué |
| 10 | ptrace | tué | ptrace bloqué |
| 11 | socket | tué | Création de socket bloquée |
| 12 | chroot_escape | tué | Appel système chroot bloqué |
| 13 | double_chroot | tué | Double chroot bloqué |
| 14 | mount_replay | tué | Appel système mount bloqué |
| 15 | cpu_exhaust | tué | RLIMIT_NPROC bloque la bombe de fork |
| 16 | signal_parent | tué | Signal au parent bloqué |
| 17 | self_hash | ok | Vérification d'intégrité du binaire |
| Lignes de code (noyau) |
| ~900 |
| Outil | Latence moyenne ± écart-type | RSS de pointe | seccomp par défaut |
|---|
| Z-Jail | 5,85 ± 1,45 ms | 1,62 Mio | oui |
| bwrap | 3,56 ± 0,40 ms | 2,19 Mio | non |
| nsjail | 8,98 ± 1,68 ms | 7,91 Mio | oui |
| Fichier | Description |
|---|
README.md | Ce fichier |
docs/ARCHITECTURE.md | Aperçu de l'architecture |
docs/SANDBOX.md | Internes du sandbox couche par couche |
docs/SECCOMP.md | Conception de la liste blanche seccomp-BPF |
docs/AUDIT_SCHEMA.md | Référence du schéma JSON d'audit |
docs/THREAT_MODEL.md | Hypothèses de sécurité et périmètre |
docs/BLAKE2B.md | Détails de l'implémentation BLAKE2b |
docs/BENCHMARKS.md | Benchmark de performances |
docs/BUILD.md | Instructions de construction |
docs/adr/ | Enregistrements de décisions d'architecture (4 docs) |
man/z_jail.1 | Page de manuel |
SECURITY.md | Politique de sécurité et signalement |
CONTRIBUTING.md | Comment contribuer |
CHANGELOG.md | Historique des versions |
ROADMAP.md | Plans futurs |
TODO.md | Lacunes connues et travaux prévus |