Skip to content

[dify-sandbox:0.2.15] Code execution nodes intermittently fail with "operation not permitted" (seccomp SIGSYS) — incomplete default whitelist + ALLOWED_SYSCALLS uses REPLACE (not APPEND) semantics #38105

Description

@6mvp6

Self Checks

  • I have read the Contributing Guide and Language Policy.
  • This is only for bug report, if you would like to ask a question, please head to Discussions.
  • I have searched for existing issues search for existing issues, including closed ones.
  • I confirm that I am using English to submit this report, otherwise it will be closed.
  • 【中文用户 & Non English User】请使用英语提交,否则会被关闭 :)
  • Please do not modify this template :) and fill in all the required fields.

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

  1. 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.
  2. 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

  1. 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).

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions