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

54 lines
1.6 KiB
Bash
Executable File

#!/bin/bash
# Copyright 2026 Alibaba Group Holding Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Runs the execd-as-init E2E (OSEP-0018) against a local Docker-bridge
# server. The server config (~/.sandbox.toml) is written by the workflow
# with runtime.execd_run_as_init = true.
set -euxo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SERVER_PID=""
cleanup() {
if [ -n "${SERVER_PID}" ]; then
kill "${SERVER_PID}" 2>/dev/null || true
wait "${SERVER_PID}" 2>/dev/null || true
fi
}
trap cleanup EXIT
docker build -f components/execd/Dockerfile -t opensandbox/execd:local "${REPO_ROOT}"
docker pull opensandbox/code-interpreter:${TAG:-latest}
mkdir -p /tmp/opensandbox-e2e/logs
echo "-------- EXECD INIT E2E test logs for execd --------" > /tmp/opensandbox-e2e/logs/execd.log
cd server
export OPENSANDBOX_INSECURE_SERVER=YES
uv sync
uv run python -m opensandbox_server.main > server.log 2>&1 &
SERVER_PID=$!
cd ..
sleep 10
cd sdks/sandbox/python && make generate-api
cd ../../..
cd tests/python
uv sync --all-extras --refresh
uv run pytest tests/test_execd_init_e2e.py -v