docs/NsjailSandbox.md
nsjail is a lightweight Linux process isolation tool that uses namespaces, cgroups, rlimits, and seccomp-bpf to sandbox processes. Compiler Explorer uses nsjail on its production servers to isolate both compiler execution and user binary execution. You can enable it on your local instance too, for added security when running untrusted code.
Without sandboxing (sandboxType=none, the default for local development), compiled user binaries run directly on
your host with the same permissions as the Compiler Explorer process. This is fine when you're the only user, but
risky if you expose your instance to others. nsjail provides:
Compiler Explorer maintains its own fork of nsjail
(compiler-explorer/nsjail) which adds features used by the included
nsjail configs (e.g. needs_mount_propagation for bind mounts). Always use this fork rather than upstream
Google nsjail.
sudo apt-get install autoconf bison flex gcc g++ git libprotobuf-dev \
libnl-route-3-dev libtool make pkg-config protobuf-compiler
git clone https://github.com/compiler-explorer/nsjail.git
cd nsjail && git checkout ce && make
Then copy the resulting binary somewhere on your PATH:
sudo cp nsjail /usr/local/bin/nsjail
nsjail uses cgroups to enforce memory, CPU, and process count limits. Two cgroup hierarchies are needed:
| Cgroup name | Used for |
|---|---|
ce-compile | Compiler and tool execution (more permissive) |
ce-sandbox | User binary execution (more restrictive) |
The setup differs depending on whether your system uses cgroups v1 or v2. You can check with:
# If this directory exists with controller files, you have cgroups v2:
ls /sys/fs/cgroup/cgroup.controllers
# If you see /sys/fs/cgroup/memory/, /sys/fs/cgroup/pids/, etc., you have cgroups v1:
ls /sys/fs/cgroup/memory/
# Install cgroup tools if needed:
sudo apt-get install cgroup-tools # Debian/Ubuntu
# or: sudo dnf install libcgroup-tools # Fedora
# Create cgroups owned by your user:
sudo cgcreate -a $USER:$USER -g memory,pids,cpu:ce-sandbox
sudo cgcreate -a $USER:$USER -g memory,pids,cpu:ce-compile
# Allow your user to migrate processes into the root cgroup:
sudo chown $USER:root /sys/fs/cgroup/cgroup.procs
On Ubuntu 24.04+ and other distributions with AppArmor restricting unprivileged user namespaces, you also need to relax those restrictions for nsjail to work:
sudo sysctl -w kernel.apparmor_restrict_unprivileged_unconfined=0
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
sudo cgcreate -a $USER:$USER -g memory,pids,cpu,net_cls:ce-sandbox
sudo cgcreate -a $USER:$USER -g memory,pids,cpu,net_cls:ce-compile
Here is a minimal script that handles all of the above for cgroups v2. Save it (e.g. as init-cgroups.sh) and run
it with sudo after each reboot:
#!/bin/sh
CE_USER=your-username
cgcreate -a ${CE_USER}:${CE_USER} -g memory,pids,cpu:ce-sandbox
cgcreate -a ${CE_USER}:${CE_USER} -g memory,pids,cpu:ce-compile
chown ${CE_USER}:root /sys/fs/cgroup/cgroup.procs
# Needed on Ubuntu 24.04+ / systems with AppArmor user namespace restrictions:
sysctl -w kernel.apparmor_restrict_unprivileged_unconfined=0
sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
The cgroup directories are lost on reboot. You can either run the script above manually after each boot, or
automate it with a systemd oneshot service. Create /etc/systemd/system/ce-cgroups.service:
[Unit]
Description=Create Compiler Explorer cgroups
After=local-fs.target
[Service]
Type=oneshot
# Replace 'ce' with your username:
ExecStart=/bin/bash -c "cgcreate -a ce:ce -g memory,pids,cpu:ce-sandbox && cgcreate -a ce:ce -g memory,pids,cpu:ce-compile && chown ce:root /sys/fs/cgroup/cgroup.procs && sysctl -w kernel.apparmor_restrict_unprivileged_unconfined=0 && sysctl -w kernel.apparmor_restrict_unprivileged_userns=0"
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
Then enable it:
sudo systemctl daemon-reload
sudo systemctl enable ce-cgroups.service
By default, sandboxing is disabled for local development (sandboxType=none and executionType=none in
etc/config/execution.defaults.properties). In production, etc/config/execution.amazon.properties enables nsjail:
sandboxType=nsjail
executionType=nsjail
wine=
wineServer=
firejail=
To do the same locally, create or edit etc/config/execution.local.properties:
# Enable nsjail for both compiler execution and user binary execution,
# matching the production configuration in execution.amazon.properties:
sandboxType=nsjail
executionType=nsjail
# Path to nsjail binary (default: 'nsjail', i.e. found on PATH):
#nsjail=/usr/local/bin/nsjail
The nsjail configuration files are already included in the repository:
| Property | Default value | Purpose |
|---|---|---|
sandboxType | none | Sandbox engine for user binaries |
executionType | none | Sandbox engine for compiler/tools |
nsjail | nsjail | Path to nsjail binary |
nsjail.config.sandbox | etc/nsjail/user-execution.cfg | Config for user binary sandboxing |
nsjail.config.execute | etc/nsjail/compilers-and-tools.cfg | Config for compiler sandboxing |
The included nsjail configs (etc/nsjail/user-execution.cfg and etc/nsjail/compilers-and-tools.cfg) are written
for the CE production environment but should work as-is on most local setups. Mounts for paths that don't exist on
your system (CEFS, NVIDIA devices, Intel/ARM/QNX compilers, etc.) are silently skipped by nsjail, so there is no
need to comment them out.
The main reason you might need to edit the configs is if your compilers are installed outside
/opt/compiler-explorer. In that case, add a bind mount for the relevant path:
mount {
src: "/home/youruser/compilers"
dst: "/home/youruser/compilers"
is_bind: true
}
Start Compiler Explorer:
make dev
Then try compiling and executing a simple program. If everything is working, you should see normal output. If nsjail fails, you'll typically see an error like:
Launching child process failed
"runChild():486 Launching child process failed"
This usually means the cgroups aren't set up correctly. Verify:
# Cgroups v2:
ls -la /sys/fs/cgroup/ce-sandbox/
ls -la /sys/fs/cgroup/ce-compile/
# Cgroups v1:
ls -la /sys/fs/cgroup/memory/ce-sandbox/
ls -la /sys/fs/cgroup/pids/ce-sandbox/
The directories should exist and be owned by your user.
"No such file or directory" for a mount source
A non-optional mount source doesn't exist on your system. Either install the missing package, create the path, or comment out the mount in the nsjail config.
User namespaces not enabled
Some distributions disable unprivileged user namespaces by default. Check:
sysctl kernel.unprivileged_userns_clone
If it returns 0, enable it:
sudo sysctl -w kernel.unprivileged_userns_clone=1
# To make permanent:
echo 'kernel.unprivileged_userns_clone=1' | sudo tee /etc/sysctl.d/99-userns.conf
Permission denied errors
nsjail needs to be able to create namespaces. If you're running inside a container (e.g. Docker), you may need
--privileged or specific capabilities (CAP_SYS_ADMIN, CAP_SYS_PTRACE).
Compiler Explorer uses two separate nsjail configurations with different security profiles:
compilers-and-tools.cfg)Used when running compilers themselves. More permissive because compilers need significant resources:
| Resource | Limit |
|---|---|
| Memory | 1.25 GiB |
| Max processes | 72 |
| CPU | 100% of one core |
| Max file size | 1 GiB |
| Open files | 300 |
| Filesystem access | /bin, /lib, /usr, /opt/compiler-explorer (read-only) |
user-execution.cfg)Used when running user-compiled binaries. Much more restrictive:
| Resource | Limit |
|---|---|
| Memory | 200 MiB |
| Max processes | 14 |
| CPU | 50% of one core |
| Max file size | 16 MiB |
| Open files | 100 |
| Filesystem access | /lib, /usr/lib only (no /bin, no /usr/bin) |
/tmp | 20 MiB tmpfs, noexec |
If you can't use nsjail (e.g. on macOS or in a restricted environment), the default sandboxType=none works fine
for local development. For some extra safety without nsjail, you can restrict dangerous compiler flags by setting
optionsForbiddenRe in etc/config/compiler-explorer.local.properties:
optionsForbiddenRe=^(-W[alp],)?((--?(wrapper|fplugin.*|specs|load|plugin|include|fmodule-mapper)|(@.*)|-I|-i)(=.*)?|--)$
This blocks flags like --plugin, -fplugin, and --wrapper that could be used to execute arbitrary code via the
compiler.