
A lightweight, multi-layer Linux sandbox combining namespaces, pivot_root, seccomp-bpf, capability dropping, and an evidence-based verdict engine (Truthimatics Public Version) for secure, auditable code execution.
Multi-layer sandbox for native code execution on Linux.
Seven ordered isolation layers — no external dependencies, ~81 KiB PIE binary.
┌──────────────────────────────────────────────────────┐
│ Z-Jail │
├──────────────────────────────────────────────────────┤
│ rlimits (CPU, AS, NOFILE, NPROC caps) │
│ Namespaces (mount, pid, net, ipc, uts) │
│ pivot_root (chroot on steroids) │
│ NO_NEW_PRIVS (no privilege escalation) │
│ Capabilities (drop all, lock securebits) │
│ seccomp-BPF (whitelist-v1: 24 syscalls) │
│ 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
The --root directory should contain a minimal filesystem with the target binary and its dependencies (for static binaries, just the binary is enough).
Existing sandboxing solutions make trade-offs:
| Z-Jail | Firecracker | gVisor | bwrap | nsjail | |
|---|---|---|---|---|---|
| External deps | zero | libc, seccomp | Go runtime | libc | libc, protobuf |
| Binary size | ~81 KiB | 20+ MiB | 40+ MiB | ~70 KiB | ~1 MiB |
| VM isolation | no | yes (microVM) | no (sandbox) | no | no |
| seccomp whitelist | yes | no | yes | optional | yes |
| Content hashing | yes | no | no | no | no |
| Audit JSON | yes | no | yes | no | partial |
| Build complexity | one make | complex | complex | trivial | moderate |
Z-Jail fills the niche between bwrap (minimal, no seccomp-by-default) and nsjail (featureful, heavy deps). It is designed for CI pipelines, CTF jail challenges, and lightweight code evaluation where you need defence-in-depth without pulling in a container runtime.
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]
Each layer is ordered so that a later layer can't be undone by an earlier one:
capset escalation after this pointsequenceDiagram
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
Z-Jail applies seven ordered isolation mechanisms in the child before execve,
each chosen so that a later step cannot be undone by re-executing an earlier one.
The parent creates the child with clone() requesting five namespaces
(mount, PID, network, IPC, UTS), then the child runs the following pipeline:
setrlimit caps CPU time, address space, open files, and process count
before any guest-influenced code runs, bounding fork bombs and memory exhaustion.
All inherited descriptors except the parent report pipe are closed,
preventing leaked handles from crossing execve.
PR_SET_DUMPABLE=0 disables core dumps and restricts /proc/self/mem access.
pivot_rootThe mount namespace root is replaced with the supplied root directory:
the directory is bind-mounted onto itself, pivot_root swaps the mount tree,
the process chdirs to the new root, and the old root is lazily detached
and removed. This is strictly stronger than chroot(2) because the previous
root is unmounted rather than merely hidden.
NO_NEW_PRIVSPR_SET_NO_NEW_PRIVS prevents any subsequent privilege gain through setuid
binaries, file capabilities, or LSM transitions. Irreversible.
User and group IDs are changed while CAP_SETUID is still held, after which
capset zeroes all capability sets and the securebits are locked, so
capabilities cannot be re-enabled.
A whitelist filter is installed; non-whitelisted calls are terminated. Allow-list of 24 syscalls:
| Syscall | Number | Notes |
|---|---|---|
read | 0 | stdin |
write | 1 | stdout/stderr + report pipe |
openat | 257 | file access (not open) |
close | 3 | — |
lseek | 8 | — |
brk | 12 | heap management |
mmap | 9 | arg-restricted: flags == 0x22 exactly (MAP_PRIVATE|MAP_ANONYMOUS) and prot & PROT_EXEC == 0 |
munmap | 11 | — |
execve | 59 | single exec at startup |
exit_group | 231 | clean process exit |
rt_sigaction | 13 | signal handlers |
rt_sigprocmask | 14 | signal masking |
getrandom | 318 | random number source |
clock_gettime | 228 | timing |
fstat | 5 | file metadata |
arch_prctl | 158 | TLS setup |
mprotect | 10 | arg-restricted: prot & PROT_EXEC == 0 (preserves W^X) |
prlimit64 | 302 | arg-restricted: new_limit == NULL full 64-bit (low + high word both zero; read-only) |
readlinkat | 267 | — |
rseq | 334 | glibc restartable sequences |
set_robust_list | 273 | glibc thread init |
set_tid_address | 218 | glibc init |
access | 21 | — |
pread64 | 17 | — |