Self Checks
Dify version
Dify:v1.14.2 ;dify-sandbox:0.2.15
Cloud or Self Hosted
Self Hosted (Docker)
Steps to reproduce
The intermittent production symptom has a deterministic reproduction. dify-sandbox applies a seccomp whitelist before running user code; the default whitelist is missing syscalls that modern glibc / CPython emit. Any of those syscalls triggers SIGSYS ("bad system call"), which the sandbox surfaces as error: operation not permitted.
Repro 1 — default whitelist gap (deterministic)
# Start a stock sandbox (no custom env). The image runs standalone.
docker run --rm -d --name sbx -p 8194:8194 langgenius/dify-sandbox:0.2.15
# Trigger a syscall that modern glibc/CPython use but that is NOT in the
# default whitelist, e.g. sched_getaffinity (amd64 nr 204):
curl -s -X POST http://localhost:8194/v1/sandbox/run \
-H "X-Api-Key: ${SANDBOX_API_KEY:-dify-sandbox}" \
-H "Content-Type: application/json" \
-d '{"language":"python3","code":"import os; print(len(os.sched_getaffinity(0)))"}'
Actual response:
{"code":0,"message":"success","data":{"error":"error: operation not permitted\n","stdout":""}}
Sandbox container log at the same instant:
{"level":"ERROR","msg":"process finished with error","status":"signal: bad system call (core dumped)"}
A baseline print(42) succeeds on the same container, proving the failure is specific to the blocked syscall, not the container itself.
Repro 2 — the ALLOWED_SYSCALLS workaround is a footgun (REPLACE semantics)
Operators naturally try to extend the whitelist via the ALLOWED_SYSCALLS env var (the field exists in internal/types/config.go and is read in internal/static/config.go). It replaces the default whitelist instead of appending to it:
docker run --rm -d --name sbx2 -e ALLOWED_SYSCALLS=204 langgenius/dify-sandbox:0.2.15
curl -s -X POST http://localhost:8194/v1/sandbox/run \
-H "X-Api-Key: ${SANDBOX_API_KEY:-dify-sandbox}" \
-H "Content-Type: application/json" \
-d '{"language":"python3","code":"print(42)"}'
Actual response:
{"code":0,"message":"success","data":{"error":"error: operation not permitted\n","stdout":""}}
Even print(42) fails, because write(1) / exit(60) / exit_group(231) etc. are no longer allowed — the default whitelist was dropped. Applied to a real deployment this turns a ~0.1% intermittent failure into a 100% sandbox outage.
Probe method (for maintainers)
The full set of syscalls blocked on the 0.2.15 default whitelist was enumerated by probing each number directly:
import ctypes
ctypes.CDLL(None).syscall(N) # blocked -> process SIGSYS -> empty stdout + "operation not permitted"
Empirically blocked on amd64 (each is routinely emitted by modern glibc/CPython):
| 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 |
In production this manifests as ~0.1% of Code-node executions failing non-deterministically (the same code succeeds 99.9% of the time; the blocked syscall is only hit on certain glibc code paths / timing).
✔️ Expected Behavior
- Python Code nodes should execute reliably. Modern glibc (2.34+) and CPython increasingly use
statx, clone3, sched_getaffinity, prlimit64, pipe2, etc.; the default seccomp whitelist should cover these so that ordinary Python works out of the box.
- If
ALLOWED_SYSCALLS (env) / allowed_syscalls (config field) is intended to let operators extend the whitelist — as FAQ.md §2 and the declarative config field imply — setting it should add to the default whitelist, not replace it. An extension mechanism that bricks the sandbox when used is worse than no mechanism.
❌ Actual Behavior
-
Code nodes intermittently (~0.1% under normal load, more under concurrency) fail with error: operation not permitted. The sandbox child is killed by SIGSYS ("bad system call") because the default ALLOW_SYSCALLS whitelist in internal/static/python_syscall/syscalls_amd64.go omits modern syscalls. Deterministic repro shown above (os.sched_getaffinity).
-
The ALLOWED_SYSCALLS env var / allowed_syscalls config field uses REPLACE semantics. Source — internal/core/lib/python/add_seccomp.go:
allowed_syscall := os.Getenv("ALLOWED_SYSCALLS")
if allowed_syscall != "" {
// allowed_syscalls = ONLY the user-supplied numbers
// (default ALLOW_SYSCALLS + ALLOW_NETWORK_SYSCALLS are DROPPED)
} else {
allowed_syscalls = append(allowed_syscalls, python_syscall.ALLOW_SYSCALLS...)
if enable_network {
allowed_syscalls = append(allowed_syscalls, python_syscall.ALLOW_NETWORK_SYSCALLS...)
}
}
So the natural workaround (ALLOWED_SYSCALLS=...) replaces the entire whitelist. If the supplied list omits any essential syscall (read/write/exit/mmap/network…), every Code node fails — the container stays "healthy" because the healthcheck only hits /health in the parent process, not the seccomp'd child, so the outage is silent at the infrastructure layer.
Self Checks
Dify version
Dify:v1.14.2 ;dify-sandbox:0.2.15
Cloud or Self Hosted
Self Hosted (Docker)
Steps to reproduce
The intermittent production symptom has a deterministic reproduction.
dify-sandboxapplies a seccomp whitelist before running user code; the default whitelist is missing syscalls that modern glibc / CPython emit. Any of those syscalls triggersSIGSYS("bad system call"), which the sandbox surfaces aserror: operation not permitted.Repro 1 — default whitelist gap (deterministic)
Actual response:
{"code":0,"message":"success","data":{"error":"error: operation not permitted\n","stdout":""}}Sandbox container log at the same instant:
A baseline
print(42)succeeds on the same container, proving the failure is specific to the blocked syscall, not the container itself.Repro 2 — the
ALLOWED_SYSCALLSworkaround is a footgun (REPLACE semantics)Operators naturally try to extend the whitelist via the
ALLOWED_SYSCALLSenv var (the field exists ininternal/types/config.goand is read ininternal/static/config.go). It replaces the default whitelist instead of appending to it:Actual response:
{"code":0,"message":"success","data":{"error":"error: operation not permitted\n","stdout":""}}Even
print(42)fails, becausewrite(1)/exit(60)/exit_group(231)etc. are no longer allowed — the default whitelist was dropped. Applied to a real deployment this turns a ~0.1% intermittent failure into a 100% sandbox outage.Probe method (for maintainers)
The full set of syscalls blocked on the 0.2.15 default whitelist was enumerated by probing each number directly:
Empirically blocked on amd64 (each is routinely emitted by modern glibc/CPython):
In production this manifests as ~0.1% of Code-node executions failing non-deterministically (the same code succeeds 99.9% of the time; the blocked syscall is only hit on certain glibc code paths / timing).
✔️ Expected Behavior
statx,clone3,sched_getaffinity,prlimit64,pipe2, etc.; the default seccomp whitelist should cover these so that ordinary Python works out of the box.ALLOWED_SYSCALLS(env) /allowed_syscalls(config field) is intended to let operators extend the whitelist — asFAQ.md§2 and the declarative config field imply — setting it should add to the default whitelist, not replace it. An extension mechanism that bricks the sandbox when used is worse than no mechanism.❌ Actual Behavior
Code nodes intermittently (~0.1% under normal load, more under concurrency) fail with
error: operation not permitted. The sandbox child is killed bySIGSYS("bad system call") because the defaultALLOW_SYSCALLSwhitelist ininternal/static/python_syscall/syscalls_amd64.goomits modern syscalls. Deterministic repro shown above (os.sched_getaffinity).The
ALLOWED_SYSCALLSenv var /allowed_syscallsconfig field uses REPLACE semantics. Source —internal/core/lib/python/add_seccomp.go:So the natural workaround (
ALLOWED_SYSCALLS=...) replaces the entire whitelist. If the supplied list omits any essential syscall (read/write/exit/mmap/network…), every Code node fails — the container stays "healthy" because the healthcheck only hits/healthin the parent process, not the seccomp'd child, so the outage is silent at the infrastructure layer.