Sandbox Seccomp Security#
langgenius/dify-sandbox is a standalone Go service that executes user-submitted Python 3 and Node.js code on behalf of Dify's Code-execution nodes. Every code run passes through two independent security layers:
- Docker: container-level network and filesystem isolation (configured in
docker-compose.yamlvia thesandboxservice) - seccomp BPF: a kernel-level syscall filter installed inside the container on each child process before user code runs
The seccomp filter defaults to ActKillProcess for any syscall not on the whitelist — a blocked call delivers SIGSYS to the process, which the sandbox surfaces as error: operation not permitted.
Dify API ──► dify-sandbox (HTTP :8194)
│
├── parent process (healthcheck, routing)
│
└── fork child per request
├── chroot(".") + chdir("/")
├── SetNoNewPrivs()
├── seccomp BPF (ActKillProcess default)
└── execve(python3 / node, user_code)
The parent process's /health endpoint is always reachable; only the seccomp'd child is affected by filter failures — so seccomp-related outages are silent at the infrastructure layer (container stays "healthy").
Default Syscall Whitelist#
The per-language whitelist for Python/amd64 lives in internal/static/python_syscall/syscalls_amd64.go and is split into three lists:
| Variable | Action | Included when |
|---|---|---|
ALLOW_SYSCALLS | ActAllow | Always |
ALLOW_ERROR_SYSCALLS | ActErrno (no kill) | Always — covers CLONE, CLONE3, MKDIRAT, MKDIR |
ALLOW_NETWORK_SYSCALLS | ActAllow | enable_network: true only |
ALLOW_SYSCALLS covers file I/O (read, write, openat, pread64, readv, writev, readahead), thread operations (futex, sched_getaffinity, sched_setaffinity, set_tid_address), memory (mmap, brk, mprotect, madvise, mbind, set_mempolicy, get_mempolicy), process lifecycle (exit, exit_group, prlimit64), time/event (clock_gettime, epoll_create, epoll_create1, epoll_pwait, pipe2), and modern extensions (statx nr 332, getrandom nr 318, rseq nr 334).
The filter is loaded via SECCOMP_SET_MODE_FILTER with SECCOMP_FILTER_FLAG_TSYNC so it applies to all threads of the child process.
ALLOWED_SYSCALLS — Append Semantics#
Operators can extend the whitelist via two equivalent mechanisms:
ALLOWED_SYSCALLSenv var: comma-separated syscall numbers (e.g.ALLOWED_SYSCALLS=204,332)allowed_syscallsinconfig.yaml: list of integers under that key
The AllowedSyscalls []int field is declared in internal/types/config.go and populated from the env var in internal/static/config.go.
In internal/core/lib/python/add_seccomp.go, InitSeccomp() builds the final profile as:
allowed_syscalls = MergeSyscalls(ALLOW_SYSCALLS, [ALLOW_NETWORK_SYSCALLS], SyscallsFromEnv("ALLOWED_SYSCALLS"))
MergeSyscalls() takes a variadic list of slices and returns their deduplicated union — operator-supplied numbers are merged on top of the defaults, not substituted for them.
⚠️ Pre-fix behavior (dify-sandbox ≤ 0.2.15)#
Before PR #275, the ALLOWED_SYSCALLS env var used replace semantics: setting it dropped ALLOW_SYSCALLS and ALLOW_NETWORK_SYSCALLS entirely. Any list that omitted write/exit/mmap etc. caused a 100% sandbox outage while the container's healthcheck continued to report healthy.
Known Issues#
dify-sandbox 0.2.15 regression — panic: could not create filter#
On kernels ≤ 3.10 (e.g. CentOS/RHEL 7), DifySeccomp() fails at filter-creation time with signal: aborted (core dumped). Workaround: pin the sandbox image to langgenius/dify-sandbox:0.2.14.
Incomplete default whitelist — intermittent operation not permitted#
Modern glibc (2.34+) and CPython increasingly call syscalls that were absent from the pre-fix ALLOW_SYSCALLS list. The failure rate is ~0.1% under normal load (same code succeeds 99.9% of the time because the blocked syscall is only hit on certain glibc code paths or timing).
Empirically blocked on amd64 with dify-sandbox 0.2.15 defaults:
| nr | name | nr | name |
|---|---|---|---|
| 17 | pread64 | 204 | sched_getaffinity |
| 19 | readv | 213 | epoll_create |
| 20 | writev | 218 | set_tid_address |
| 28 | madvise | 237 | mbind |
| 63 | uname | 238 | set_mempolicy |
| 79 | getcwd | 239 | get_mempolicy |
| 99 | sysinfo | 281 | epoll_pwait |
| 187 | readahead | 293 | pipe2 |
| 203 | sched_setaffinity | 302 | prlimit64 |
| 332 | statx | 435 | clone3 |
All of these are now present in the current ALLOW_SYSCALLS list in main.
Operator Configuration Reference#
config.yaml (example):
enable_network: true/false— gatesALLOW_NETWORK_SYSCALLSallowed_syscalls: [nr, ...]— extra syscalls merged into the whitelist
Environment variables (internal/static/config.go):
ALLOWED_SYSCALLS=204,332,...— comma-separated numbers, merged (not replaced) on top of defaultsENABLE_NETWORK=true/false— same as yaml field
Key source files:
| File | Purpose |
|---|---|
internal/static/python_syscall/syscalls_amd64.go | Built-in Python/amd64 whitelist (ALLOW_SYSCALLS, ALLOW_ERROR_SYSCALLS, ALLOW_NETWORK_SYSCALLS) |
internal/core/lib/python/add_seccomp.go | InitSeccomp() — assembles final profile, applies chroot + setuid |
internal/core/lib/seccomp.go | BPF filter compilation and SECCOMP_SET_MODE_FILTER load |
internal/core/lib/syscalls.go | MergeSyscalls(), SyscallsFromEnv(), ParseSyscallNumbers() |
internal/types/config.go | AllowedSyscalls []int config struct field |
docker/volumes/sandbox/conf/config.yaml.example | Canonical operator config template |
To change the built-in default whitelist (not just extend it), edit syscalls_amd64.go and rebuild the image — the env var and yaml field can only add syscalls, not remove them.