Files
epha 43ab6bbb68 feat(execd): execd as sandbox init with hardening floor (OSEP-0018) (#1474)
* feat(execd): execd as sandbox init (OSEP-0018 phase 1)

Make execd the sandbox init (PID 1): it becomes the parent of the user
entrypoint, reaps every child through a single reaper, forwards
application signals, and owns the container lifecycle (entrypoint exit
code is propagated to the runtime).

- --init flag + EXECD_INIT bootstrap.sh exec branch (default off,
  classic background-and-wait topology unchanged)
- single reaper (only wait4-family caller) with pre-reap WNOWAIT
  barrier; managedProcess abstraction replaces Cmd.Wait across all
  launch paths (command, bash session, PTY, isolated session)
- signal forwarding (HUP/USR1/USR2/WINCH) and SIGTERM graceful
  shutdown; subreaper fallback when not PID 1; PR_SET_DUMPABLE(0)
- hardening.init_mode/signal_shield reported on the capabilities
  endpoint (spec + Go/Python SDKs aligned)
- unit tests for the reaper and lifecycle; bootstrap contract test

* feat(execd): pre-exec hardening floor (OSEP-0018 phase 2)

Route every user-code launch (entrypoint, /command, /code, PTY) through
a native launcher that applies the privilege floor between fork and exec:
execd credential env stripped, bounding set trimmed to keep_capabilities
(default none), no_new_privs, identity drop to the image user, ambient
caps raised, and the seccomp filter installed last.

- native/launcher.c: static prelude helper, fail-closed on malformed
  policy, fail-open per step; built/installed by the Makefile and image
- [hardening] enabled + keep_capabilities in the isolation TOML; the
  launcher's execve is reserved and rejected at config time (execveat
  stays valid); [seccomp] deny is reused as the floor filter
- isolated sessions exempt: the bwrap workload is already reduced inside
  the namespace, and flooring the bwrap process would deny unshare and
  strip the caps it needs to build the namespace
- hardening.cap_drop/seccomp/landlock/ebpf layer states on the
  capabilities endpoint (spec + Go/Python SDKs aligned)
- all layers fail open: missing launcher or CAP_SETPCAP degrades with a
  reason instead of blocking startup

* feat(server): runtime.execd_run_as_init config injects EXECD_INIT

Single server-side switch (default false) that sets EXECD_INIT=1 in the
sandbox environment across all paths (Docker, K8s Batch/Agent, Pool
taskTemplate). bootstrap.sh then execs execd --init, so topology and the
--init flag stay in lockstep by construction (OSEP-0018 open question 1).
Execd init mode and hardening remain independently controllable; the
switch is intended to be flipped on by default in a later release.

* test(execd): real-container init-mode regression (OSEP-0018)

Runs the execd image with EXECD_INIT=1 in Docker and verifies the init
contract end to end: execd is PID 1 with the workload as its direct
child, orphans are reaped (no zombie accumulation), in-namespace
kill -9 1 is inert (signal shield), entrypoint exit codes are
propagated, runtime SIGTERM is forwarded with the workload status
preserved, and with [hardening] enabled the floor applies (CapEff=0,
no_new_privs, seccomp filter, env strip) while the capabilities
endpoint reports pid1 + active layers. Also covers the Pool-style
backgrounded topology reporting subreaper mode.

Wired into execd-test.yml (ubuntu smoke job); EXECD_TEST_IMAGE allows
local iteration against a prebuilt image.

* feat(execd): landlock confinement (OSEP-0018 phase 3)

Add [landlock] enabled/extra_writable/extra_readable on top of the
hardening floor. The launcher applies the filesystem allowlist between
the identity drop and seccomp: system paths and /proc/self read+exec,
writable device files and the controlling tty, /tmp, /run, and
allowed_writable — everything else denied. A root EXECUTE-only rule
covers traversal and execve of any binary without granting reads, and
all of /proc is never granted (would re-expose /proc/1).

The kernel ABI is probed (v1-v4) and the launcher trims access bits
(REFER/TRUNCATE) accordingly; ABI < 1 reports unsupported and skips.
The container regression now enables landlock and asserts that
/proc/1/environ is unreadable by the workload when the layer is
active.

* feat(execd): eBPF observation variant (OSEP-0018 phase 4)

Opt-in exec/connect/privilege audit, scoped to the sandbox cgroup and
written as rotating JSONL (stable envelope: ts/event/sandbox_id/pid/comm
plus per-kind fields). Ships as the execd-ebpf build variant (CGO +
cilium/ebpf); the default image is unchanged and reports disabled.

- BPF programs (CO-RE): sched_process_exec (filename+argv via __data_loc),
  inet_sock_set_state (dst ip:port), commit_creds kprobe (uid/gid deltas,
  cap_added)
- kernel compatibility from 5.10: the exec trace event layout change in
  5.16 is handled with a bpf_core_field_exists discriminator, the
  kernel_cap_t shape change in 6.3 with a layout-agnostic 8-byte read;
  Landlock (phase 3) still requires >= 5.13 and degrades to unsupported
- [ebpf] enabled/observe/audit_file config; capabilities endpoint layer
  state (active/unsupported/degraded/disabled), all fail-open
- Dockerfile ebpf target + make build-ebpf; generated bytecode committed;
  unit tests for event decoding; CI runs the ebpf-tagged tests

* docs(spec): drop phase markers from hardening layer descriptions

* feat(sdks): surface hardening status across all execd-capabilities SDKs

Regenerate the JS execd client from the spec and extend the handwritten
adapters so every SDK exposes the hardening object (init_mode,
signal_shield, cap_drop/seccomp/landlock/ebpf layer states):

- javascript: generated execd.ts gains hardening + HardeningLayerState
- kotlin: IsolatedCapabilities.hardening domain model + adapter mapping
  (with a mock-server test) — the generated sandbox-api client picks the
  spec up at build time
- csharp: IsolatedCapabilities gains Hardening/HardeningStatus/
  HardeningLayerState records

Go and Python already carried the field; MCP and code-interpreter SDKs do
not consume the capabilities endpoint.

* feat(server,k8s): pool tasks run execd as the task root (OSEP-0018 phase 5)

With runtime.execd_run_as_init enabled, the pool taskTemplate no longer
backgrounds bootstrap: the task-executor shim's shell execs bootstrap.sh,
which execs `execd --init` (EXECD_INIT=1), so execd becomes the root of
the task process tree — orphaned task children are reaped (subreaper)
and the entrypoint exit code propagates to the task status. Classic
background-and-wait topology is preserved when the switch is off.

The K8s Restart recycle contract (kill 1 via pod exec) is confirmed
compatible with init-mode execd under the current SIGTERM-forward
semantics: execd forwards the signal and exits with the workload status,
so the kubelet restarts the container. A note marks the reconciliation
required once a trusted out-of-band stop channel replaces signal-driven
stop (OSEP-0018 §3).

Docs: Pool pod template guidance for running execd as the pod's PID 1.

* docs(oseps): mark 0018 as implementing with granular phase status

Record the phased implementation state (phases 1-5 + server switch done),
the resolutions of open questions 1-5, and the remaining work (trusted
stop channel, Pool pod-level PID 1, 5.10 validation, e2e).

* fix: address Codex/human review feedback across five rounds + CI repairs

Squashed review-fix series:
- CI: Dockerfile stage order (ebpf variant was the default image), license
  headers on bpf2go output, verify-license skip for generated files,
  golangci (gci/predeclared/unused), kotlin ktlint
- landlock: required-vs-best-effort rule semantics, mount-expanded rules
  (bind-mounted workspaces), dynamic rule counts, preflight degradation
  for operator-explicit grants, per-launch fail-closed ruleset
- hardening: trusted launcher path first, MFD_CLOEXEC policy memfd,
  per-request uid/gid folded into the policy, entrypoint keeps bootstrap
  env but never EXECD_ACCESS_TOKEN, bash-session env snapshot scrub
- init: reaper drops reaped pids (bounded map, no stale pgid signalling),
  SIGTERM/SIGKILL sent under the reaper lock, synchronous signal.Notify
  before the entrypoint starts
- eBPF: sandbox_id in records, cgroup-id under the cgroup v2 mount,
  requested-hook attach failures degrade, IPv4-mapped event format,
  cap-only privilege events, argv dropped from exec events
- distribution: launcher shipped on docker/k8s paths, pool taskTemplate
  exec + needs_task_template fix, ebpf image keeps the default layout and
  builds static
- container regression: workdir world-writable (no CAP_DAC_OVERRIDE under
  hardening), /proc/self read via the entrypoint process (Landlock
  descendant limitation)

* fix: address Codex round 6 (credential identity, audit sink, /opt, sandbox_id)

- hardening: per-request credentials now force the launcher's UID_DROP even
  when execd is not root (a /command uid/gid request previously ran as the
  image user), and supplementary groups are serialized into the policy so
  the launcher applies setgroups(groups) instead of clearing them
- audit: loaded eBPF objects are retained on the Observer (GC could close
  the fd and detach the programs) and closed on Close(); ringbuf reader
  failure now also closes already-attached links; audit write errors are
  logged instead of dropped
- landlock: /opt joins the default read+exec set (bundled
  code-interpreter entrypoints and runtimes live under /opt)
- server: OPENSANDBOX_ID is injected on the Docker, K8s Batch/Agent and
  pool taskTemplate env paths so eBPF audit records carry the sandbox id

* test(e2e): dedicated execd-as-init real-e2e for python, plus nightly job

Add tests/python/tests/test_execd_init_e2e.py (sync SDK): PID 1 is
execd, the workload is its direct child, orphans are reaped, in-namespace
kill -9 1 is inert, and /v1/isolated/capabilities reports
hardening.init_mode=pid1. Runs against a server with
runtime.execd_run_as_init=true.

- scripts/python-execd-init-e2e.sh: docker-bridge runner for the new
  suite; wired as a dedicated real-e2e job (python-execd-init-e2e)
- scripts/python-k8s-execd-init-e2e.sh + E2E_EXECD_RUN_AS_INIT in
  k8s_e2e_write_server_helm_values: Kind/Kubernetes variant; wired as a
  dedicated kubernetes-nightly-build job (execd-init-e2e)

* ci(e2e): fix execd-init e2e failures in python and k8s nightly jobs

* feat(execd): ship execd-ebpf in default image; docs and e2e sync (OSEP-0018)

* fix(execd): address Codex round 7 (landlock jupyter log, init-mode report, launcher identity, ebpf build tags)

* docs(oseps): fold remaining-work tracking into OSEP-0018 status section

* fix(execd,server): resolve codex review on hardening report and pool sandbox id attribution

- ReportHardening keys the non-init topology correction off hardening being
  enabled instead of cap_drop's state, and only degrades layers that are
  actually in effect (previously a degraded cap_drop left seccomp/landlock
  claiming active, and a disabled landlock could be marked degraded)
- eBPF Init reports unsupported when OPENSANDBOX_ID is missing, so pool
  fast-path allocations without a task template cannot silently claim
  active attribution
- batchsandbox_provider logs the pool fast-path limitation (no per-allocation
  env injection without a task template)
- docs/components/execd.md documents the pool path attribution behavior
- tests: cover non-init degradation, landlock enabled case, and pool fast
  path

* fix(execd): report configured non-Linux layers as unsupported; fix golint goconst

- non-Linux: InitHardening/SetEbpfState now record requested layers so the
  capabilities endpoint reports configured hardening/landlock/ebpf as
  unsupported instead of disabled (codex review)
- hardening_linux.go: hoist repeated "disabled" state string into a const
  (golangci-lint goconst)
- docs: describe the dual-binary default image (execd + execd-ebpf) and the
  deferred server-side selection; no Dockerfile 'ebpf' target exists

---------

Co-authored-by: Sky <yutian.taoyt@alibaba-inc.com>
2026-08-17 18:59:16 +08:00
..
2026-06-30 18:13:57 +00:00

OpenSandbox Code Interpreter Environment

English | 中文

This directory contains the Docker build files for the Code Interpreter sandbox. The image is based on Ubuntu 24.04 and comes pre-installed with multiple mainstream programming languages and their multi-version environments, designed to provide an out-of-the-box multi-language code execution environment.

Features

  • Multi-Language Support: Pre-installed Python, Java, Node.js, and Go with multiple versions
  • Version Switching: Easy runtime version switching without rebuilding
  • Jupyter Integration: Built-in Jupyter Notebook with multi-language kernels
  • Multi-Architecture: Supports both amd64 and arm64 architectures
  • clone3-workaround (amd64): The image installs AkihiroSuda/clone3-workaround v1.0.0 as /usr/local/bin/clone3-workaround on linux/amd64 only (upstream ships no arm64 binary), plus libseccomp2 because the upstream binary is dynamically linked to libseccomp. Use it to wrap commands on very old Docker/containerd hosts, e.g. clone3-workaround apt-get update.
  • Production Ready: Optimized for containerized execution environments

Supported Languages & Versions

The image comes pre-installed with the following languages and versions:

Language Supported Versions Installation Path Notes
Python 3.10, 3.11, 3.12, 3.13, 3.14* /opt/python/versions Installed via uv; 3.14 is experimental
Java 8, 11, 17, 21 /usr/lib/jvm OpenJDK; includes Maven 3.9.2
Node.js v18, v20, v22 /opt/node Official Linux binaries
Go 1.23, 1.24, 1.25 /opt/go Official Linux binaries

> Note: Version numbers may be updated to the latest patch versions at build time.

Quick Start

1. Build the Image

Since multi-architecture (amd64/arm64) is supported, it's recommended to use Docker Buildx:

# Navigate to the directory
cd sandboxes/code-interpreter

# Build local image
docker build -t opensandbox/code-interpreter:latest .

# For multi-architecture builds (requires Docker Buildx)
docker buildx build --platform linux/amd64,linux/arm64 \
  -t opensandbox/code-interpreter:latest .

2. Run the Container

With Custom Version Selection:

docker run -it --rm \
  -e PYTHON_VERSION=3.11 \
  -e JAVA_VERSION=17 \
  -e NODE_VERSION=20 \
  -e GO_VERSION=1.24 \
  opensandbox/code-interpreter:latest

EXECD_CLONE3_COMPAT (clone3-workaround)

If you set EXECD_CLONE3_COMPAT to 1, true, yes, on, or reexec (same semantics as execd), the entrypoint script re-executes itself under /usr/local/bin/clone3-workaround before Jupyter and kernel setup. That binary is included on linux/amd64 only; on arm64 builds the script prints a warning and continues without wrapping. After a successful wrap, the script unsets EXECD_CLONE3_COMPAT in the running process tree. Use 0, false, off, no, or leave unset to disable.

Version Switching

The image includes a built-in version switching script /opt/code-interpreter/code-interpreter-env.sh. You need to use the source command to load it to modify the current shell's environment variables.

Basic Usage

source /opt/code-interpreter/code-interpreter-env.sh <language> <version>

Examples

Switch Python Version:

# Switch to Python 3.11
source /opt/code-interpreter/code-interpreter-env.sh python 3.11
python3 --version
# Output: Python 3.11.x

Switch Java Version:

# Switch to Java 8
source /opt/code-interpreter/code-interpreter-env.sh java 8
java -version

Switch Node.js Version:

# Switch to Node 22
source /opt/code-interpreter/code-interpreter-env.sh node 22
node -v

Switch Go Version:

# Switch to Go 1.25
source /opt/code-interpreter/code-interpreter-env.sh go 1.25
go version

List Available Versions

If you don't specify a version number, the script will list all available versions installed in the current image:

# List all Python versions
source /opt/code-interpreter/code-interpreter-env.sh python

# List all Java versions
source /opt/code-interpreter/code-interpreter-env.sh java

# List all Node.js versions
source /opt/code-interpreter/code-interpreter-env.sh node

# List all Go versions
source /opt/code-interpreter/code-interpreter-env.sh go

Default Versions

The default version configuration when the container starts:

  • Python: 3.14
  • Java: 21
  • Node.js: 22
  • Go: 1.25

To permanently modify the default version at the Dockerfile level, adjust the ENV PATH settings at the bottom of the Dockerfile.

Jupyter Notebook Integration

Available Kernels

The image comes with pre-configured Jupyter kernels for all supported languages:

  • Python: ipykernel for all Python versions
  • Java: IJava kernel
  • TypeScript/JavaScript: tslab kernel
  • Go: gonb kernel
  • Bash: bash_kernel

Starting Jupyter

/opt/code-interpreter/code-interpreter.sh

Environment Variables

  • JUPYTER_HOST: Jupyter server host (default: http://127.0.0.1:44771)
  • JUPYTER_PORT: Jupyter server port (default: 44771)
  • JUPYTER_TOKEN: Access token (default: opensandboxcodeinterpreterjupyter)

Advanced Usage

Persistent Workspace

Mount a local directory to persist your work:

docker run -it --rm \
  -v $(pwd)/workspace:/workspace \
  opensandbox/code-interpreter:latest

Custom Configuration

Override Jupyter configuration:

docker run -it --rm \
  -v $(pwd)/jupyter_config.py:/root/.jupyter/jupyter_notebook_config.py \
  opensandbox/code-interpreter:latest

Install Additional Packages

Python:

python3 -m pip install pandas numpy --break-system-packages

Node.js:

npm install -g typescript

Go:

go install github.com/user/package@latest

Java:

mvn install dependency:copy-dependencies

Architecture

code-interpreter/
├── Dockerfile                          # Main build file
├── Dockerfile_base                     # Base build file
├── README.md                           # This file
├── README_zh.md                        # Chinese README
└── scripts/
    ├── code-interpreter-env.sh         # Version switching script
    ├── code-interpreter.sh             # Jupyter startup script
    └── jupyter_notebook_config.py      # Jupyter configuration

Troubleshooting

If a specific version is not found, list available versions:

source /opt/code-interpreter/code-interpreter-env.sh <language>

License

This project is part of the OpenSandbox suite. See the main LICENSE file for details.

Support

For issues and questions: