Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
fae9470
feat(execd): execd as sandbox init (OSEP-0018 phase 1)
Pangjiping Aug 11, 2026
bbe5de0
feat(execd): pre-exec hardening floor (OSEP-0018 phase 2)
Pangjiping Aug 11, 2026
fb1e3fc
feat(server): runtime.execd_run_as_init config injects EXECD_INIT
Pangjiping Aug 12, 2026
8818ba4
test(execd): real-container init-mode regression (OSEP-0018)
Pangjiping Aug 12, 2026
81924c3
feat(execd): landlock confinement (OSEP-0018 phase 3)
Pangjiping Aug 12, 2026
2111654
feat(execd): eBPF observation variant (OSEP-0018 phase 4)
Pangjiping Aug 12, 2026
ab3263f
docs(spec): drop phase markers from hardening layer descriptions
Pangjiping Aug 12, 2026
a81109c
feat(sdks): surface hardening status across all execd-capabilities SDKs
Pangjiping Aug 12, 2026
f06f1ff
feat(server,k8s): pool tasks run execd as the task root (OSEP-0018 ph…
Pangjiping Aug 12, 2026
9f9db05
docs(oseps): mark 0018 as implementing with granular phase status
Pangjiping Aug 12, 2026
dc1f48f
fix: address Codex/human review feedback across five rounds + CI repairs
Pangjiping Aug 12, 2026
fa7a494
fix: address Codex round 6 (credential identity, audit sink, /opt, sa…
Pangjiping Aug 12, 2026
d5ac089
test(e2e): dedicated execd-as-init real-e2e for python, plus nightly job
Pangjiping Aug 12, 2026
b88cd97
ci(e2e): fix execd-init e2e failures in python and k8s nightly jobs
Pangjiping Aug 12, 2026
be2dc8a
feat(execd): ship execd-ebpf in default image; docs and e2e sync (OSE…
Pangjiping Aug 12, 2026
8ae0b49
fix(execd): address Codex round 7 (landlock jupyter log, init-mode re…
Pangjiping Aug 12, 2026
cba75bc
docs(oseps): fold remaining-work tracking into OSEP-0018 status section
Pangjiping Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/execd-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ jobs:
run: |
go test -v -coverpkg=./... -coverprofile=coverage.out -covermode=atomic ./pkg/...

- name: Run execd-ebpf variant tests
working-directory: components/execd
run: |
CGO_ENABLED=1 go test -tags ebpf -count=1 ./pkg/ebpf/

- name: Calculate coverage and generate summary
working-directory: components/execd
id: coverage
Expand Down Expand Up @@ -130,6 +135,14 @@ jobs:
chmod +x components/execd/tests/smoke_bwrap.sh
bash components/execd/tests/smoke_bwrap.sh

- name: Init-mode container regression (OSEP-0018)
if: matrix.os == 'ubuntu-latest'
shell: bash
timeout-minutes: 30
run: |
chmod +x components/execd/tests/init_container.sh
bash components/execd/tests/init_container.sh

- name: Show logs
if: always()
run: |
Expand Down
44 changes: 44 additions & 0 deletions components/execd/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ RUN apk add --no-cache git musl-dev meson ninja gcc libcap-dev libcap-static pkg
COPY components/execd/native/session-gate.c /build/session-gate.c
RUN gcc -Os -static -s -Wall -Wextra -Werror \
-o /build/opensandbox-session-gate /build/session-gate.c
COPY components/execd/native/launcher.c /build/launcher.c
RUN gcc -Os -static -s -Wall -Wextra -Werror \
-o /build/opensandbox-launcher /build/launcher.c
RUN git clone --depth 1 --branch v0.11.2 \
https://github.com/containers/bubblewrap /build/bwrap
WORKDIR /build/bwrap
Expand All @@ -93,11 +96,52 @@ RUN rm /usr/lib/libcap.so /usr/lib/libcap.so.2 && \
ninja -C builddir && \
cp builddir/bwrap /build/bwrap/bwrap

# execd-ebpf: observation variant (OSEP-0018 §5). The default image below
# never contains eBPF code; this stage builds a CGO variant with the
# exec/connect/privilege audit hooks attached.
FROM golang:1.25.9 AS ebpf-builder
Comment thread
Pangjiping marked this conversation as resolved.
ARG VERSION=dev
ARG GIT_COMMIT=unknown
ARG BUILD_TIME=unknown
RUN apt-get update && apt-get install -y --no-install-recommends clang
WORKDIR /build
COPY components/internal/go.mod components/internal/go.sum ./components/internal/
COPY components/execd/go.mod components/execd/go.sum ./components/execd/
RUN cd components/internal && go mod download
RUN cd components/execd && go mod download
COPY components/internal ./components/internal
COPY components/execd ./components/execd
WORKDIR /build/components/execd
# cilium/ebpf is pure Go, so the variant builds fully static.
RUN CGO_ENABLED=0 go build -tags ebpf -trimpath -buildvcs=false \
-ldflags "-buildid= -B none \
-X 'github.com/alibaba/opensandbox/internal/version.Version=${VERSION}' \
-X 'github.com/alibaba/opensandbox/internal/version.BuildTime=${BUILD_TIME}' \
-X 'github.com/alibaba/opensandbox/internal/version.GitCommit=${GIT_COMMIT}'" \
-o /build/execd-ebpf ./main.go

# The observation variant keeps the exact layout of the default image so it
# can be used as runtime.execd_image: /execd, /bootstrap.sh and the native
# helpers must be present for the server distribution paths.
FROM alpine:latest AS ebpf
COPY --from=bwrap-builder /build/bwrap/bwrap /usr/local/bin/bwrap
COPY --from=bwrap-builder --chown=0:0 --chmod=0555 /build/opensandbox-session-gate /usr/local/libexec/opensandbox-session-gate
COPY --from=bwrap-builder --chown=0:0 --chmod=0555 /build/opensandbox-session-gate /opt/opensandbox/opensandbox-session-gate
COPY --from=bwrap-builder --chown=0:0 --chmod=0555 /build/opensandbox-launcher /usr/local/libexec/opensandbox-launcher
COPY --from=bwrap-builder --chown=0:0 --chmod=0555 /build/opensandbox-launcher /opt/opensandbox/opensandbox-launcher
COPY --from=ebpf-builder /build/execd-ebpf ./execd
COPY --from=ebpf-builder /build/execd-ebpf ./execd-ebpf
COPY components/execd/bootstrap.sh ./bootstrap.sh
COPY components/execd/install.bat ./install.bat
ENTRYPOINT ["./execd"]

FROM alpine:latest

COPY --from=bwrap-builder /build/bwrap/bwrap /usr/local/bin/bwrap
COPY --from=bwrap-builder --chown=0:0 --chmod=0555 /build/opensandbox-session-gate /usr/local/libexec/opensandbox-session-gate
COPY --from=bwrap-builder --chown=0:0 --chmod=0555 /build/opensandbox-session-gate /opt/opensandbox/opensandbox-session-gate
COPY --from=bwrap-builder --chown=0:0 --chmod=0555 /build/opensandbox-launcher /usr/local/libexec/opensandbox-launcher
COPY --from=bwrap-builder --chown=0:0 --chmod=0555 /build/opensandbox-launcher /opt/opensandbox/opensandbox-launcher
COPY --from=builder /build/execd .
COPY --from=builder /build/execd.exe ./execd.exe
COPY --from=builder /build/opensandbox-supervisor ./opensandbox-supervisor
Expand Down
58 changes: 56 additions & 2 deletions components/execd/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ SESSION_GATE_SOURCE_INSTALL_DIR := /usr/local/libexec
SESSION_GATE_RUNTIME_DIR := /opt/opensandbox
SESSION_GATE_CFLAGS ?= $(CFLAGS) -O2 -Wall -Wextra -Werror
SESSION_GATE_LDFLAGS ?= -static -s
LAUNCHER_BINARY := bin/opensandbox-launcher
LAUNCHER_SOURCE := native/launcher.c
LAUNCHER_RUNTIME_DIR := /opt/opensandbox
INSTALL ?= install
DESTDIR ?=
ifeq ($(strip $(DESTDIR)),)
Expand All @@ -49,7 +52,7 @@ PROJECT_LDFLAGS := -buildid= -B none -X 'github.com/alibaba/opensandbox/internal
GO_BUILD_FLAGS := $(strip $(GOFLAGS) $(PROJECT_GOFLAGS))
GO_LDFLAGS := $(strip $(LDFLAGS) $(PROJECT_LDFLAGS))

.PHONY: build-session-gate install-session-gate
.PHONY: build-session-gate install-session-gate build-launcher install-launcher
build-session-gate:
@set -eu; \
host_goos="$$(go env GOHOSTOS)"; \
Expand Down Expand Up @@ -99,11 +102,62 @@ install-session-gate:
$(INSTALL) $(SESSION_GATE_INSTALL_OWNER_ARGS) -m 0555 "$(SESSION_GATE_BINARY)" \
"$(DESTDIR)$(SESSION_GATE_RUNTIME_DIR)/opensandbox-session-gate"

build-launcher:
@set -eu; \
host_goos="$$(go env GOHOSTOS)"; \
host_goarch="$$(go env GOHOSTARCH)"; \
target_goos="$(if $(GOOS),$(GOOS),$$(go env GOOS))"; \
target_goarch="$(if $(GOARCH),$(GOARCH),$$(go env GOARCH))"; \
if [ "$$target_goos" != "linux" ]; then \
echo "Skipping launcher: hardening requires Linux (target=$$target_goos/$$target_goarch)"; \
exit 0; \
fi; \
if [ "$$host_goos/$$host_goarch" != "$$target_goos/$$target_goarch" ]; then \
echo "launcher cross-build is unsupported (host=$$host_goos/$$host_goarch, target=$$target_goos/$$target_goarch)" >&2; \
echo "use the execd Docker build for multi-architecture Linux artifacts" >&2; \
exit 1; \
fi; \
mkdir -p bin; \
$(CC) $(CPPFLAGS) $(SESSION_GATE_CFLAGS) "$(LAUNCHER_SOURCE)" \
$(SESSION_GATE_LDFLAGS) -o "$(LAUNCHER_BINARY).tmp"; \
mv -f "$(LAUNCHER_BINARY).tmp" "$(LAUNCHER_BINARY)"

install-launcher:
@if [ "$$(uname -s)" != "Linux" ]; then \
echo "install-launcher requires Linux" >&2; \
exit 1; \
fi
@if [ ! -x "$(LAUNCHER_BINARY)" ]; then \
echo "$(LAUNCHER_BINARY) is missing; run make build-launcher first" >&2; \
exit 1; \
fi
@if [ -z "$(DESTDIR)" ] && [ "$$(id -u)" -ne 0 ]; then \
echo "install-launcher requires root unless DESTDIR is set" >&2; \
exit 1; \
fi
@umask 022; mkdir -p "$(DESTDIR)$(LAUNCHER_RUNTIME_DIR)"
@if [ -z "$(DESTDIR)" ]; then \
chown root:root "$(DESTDIR)$(LAUNCHER_RUNTIME_DIR)"; \
fi
chmod go-w "$(DESTDIR)$(LAUNCHER_RUNTIME_DIR)"
$(INSTALL) $(SESSION_GATE_INSTALL_OWNER_ARGS) -m 0555 "$(LAUNCHER_BINARY)" \
"$(DESTDIR)$(LAUNCHER_RUNTIME_DIR)/opensandbox-launcher"

.PHONY: build
build: vet build-session-gate ## Build execd and the Linux session gate.
build: vet build-session-gate build-launcher ## Build execd and the Linux native helpers.
@mkdir -p bin
go build $(GO_BUILD_FLAGS) -ldflags "$(GO_LDFLAGS)" -o bin/execd main.go

.PHONY: build-ebpf
build-ebpf: ## Build the execd-ebpf observation variant (CGO + cilium/ebpf).
@if [ "$$(uname -s 2>/dev/null || echo non-linux)" != "Linux" ]; then \
echo "execd-ebpf requires Linux (BPF attachable host)" >&2; \
exit 1; \
fi
@mkdir -p bin
CGO_ENABLED=1 go build -tags ebpf $(GO_BUILD_FLAGS) -ldflags "$(GO_LDFLAGS)" -o bin/execd-ebpf main.go
@echo "built bin/execd-ebpf"

.PHONY: test-integration
test-integration: ## Run integration tests (Linux + bwrap required).
go test -v -tags="linux,bwrap" -run Integration ./pkg/runtime/bwrap_test/
Expand Down
26 changes: 17 additions & 9 deletions components/execd/bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,6 @@ if [ -n "${EXECD_BOOTSTRAP_PRE_SCRIPT:-}" ]; then
fi

echo "starting OpenSandbox Execd daemon at $EXECD."
$EXECD &
EXECD_PID=$!

# Allow chained shell commands (e.g., /test1.sh && /test2.sh)
# Usage:
Expand All @@ -337,17 +335,27 @@ if [ -z "$SHELL_BIN" ]; then
fi
fi

# Resolve the user command into a concrete argv shared by both branches.
if [ "$CMD" != "" ]; then
"$SHELL_BIN" -c "$CMD" &
CMD_PID=$!
set -- "$SHELL_BIN" -c "$CMD"
elif [ $# -eq 0 ]; then
"$SHELL_BIN" &
CMD_PID=$!
else
"$@" &
CMD_PID=$!
set -- "$SHELL_BIN"
fi

# Init mode (OSEP-0018): exec into execd so it becomes the sandbox init (PID 1
# on the direct paths) and supervises the user command itself. The shell must
# exec, never background, or execd would run as a subreaper without the kernel
# signal shield.
if is_truthy "${EXECD_INIT:-}"; then
exec "$EXECD" --init -- "$@"
fi

"$EXECD" &
EXECD_PID=$!

"$@" &
Comment thread
Pangjiping marked this conversation as resolved.
CMD_PID=$!

set +e
wait "$CMD_PID" 2>/dev/null
CMD_STATUS=$?
Expand Down
50 changes: 50 additions & 0 deletions components/execd/configs/isolation.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,53 @@ allowed_writable = ["/workspace", "/mnt", "/media", "/data"]
# # Other potentially dangerous
# "userfaultfd", "kexec_load", "kexec_file_load", "acct",
# ]

# Hardening floor (OSEP-0018 §4). Default OFF — omit the section for today's
# behavior. When enabled, every user-code process (entrypoint, /command,
# /code, PTY) is launched through the opensandbox-launcher native helper with
# reduced capabilities, no_new_privs, and the seccomp floor; combined with
# init mode via bootstrap.sh EXECD_INIT + --init.
#
# [hardening]
# enabled = true
#
# # Capabilities the workload retains (raised in the ambient set).
# # Default: drop all. Names use the CAP_ prefix.
# keep_capabilities = []
#
# NOTE: with [hardening] enabled, [seccomp] deny must NOT list "execve" —
# it is reserved for the launcher's final exec (execveat stays allowed).
# Missing runtime support (no CAP_SETPCAP, launcher binary absent) is
# reported on GET /v1/isolated/capabilities under "hardening" and skipped,
# never fatal.

# Landlock filesystem confinement (OSEP-0018 §5) on top of [hardening].
# Default OFF. When enabled, user-code processes are allowlisted: system
# paths read+exec, /proc/self and well-known read-only proc files, the
# needed /dev device files, /tmp, /run, and allowed_writable — everything
# else is denied. A kernel without Landlock (ABI < 1) degrades to
# "unsupported" and is skipped.
#
# [landlock]
# enabled = true
#
# # Extra writable paths beyond the built-in set (read+write+create).
# extra_writable = ["/var/cache"]
#
# # Extra read-only paths beyond the built-in set (read+exec).
# extra_readable = ["/opt/readonly"]

# eBPF observation (OSEP-0018 §5). Default OFF. Requires the execd-ebpf
# build variant (CGO + cilium/ebpf), CAP_BPF + CAP_PERFMON, and a
# BTF-capable kernel (5.10+ with CONFIG_DEBUG_INFO_BTF). Observation is
# scoped to the sandbox cgroup and written as JSONL to a rotating audit
# file. Missing prerequisites degrade to "unsupported" and are skipped.
#
# [ebpf]
# enabled = true
#
# # Event kinds to record. Default: all three.
# observe = ["exec", "connect", "privilege"]
#
# # Append-only JSONL audit sink (rotated). Default below.
# audit_file = "/var/log/opensandbox/ebpf-audit.jsonl"
2 changes: 2 additions & 0 deletions components/execd/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ require (
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cilium/ebpf v0.16.0
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
Expand Down Expand Up @@ -68,6 +69,7 @@ require (
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2
golang.org/x/text v0.37.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
Expand Down
4 changes: 4 additions & 0 deletions components/execd/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
Expand Down Expand Up @@ -156,6 +158,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI=
golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
Expand Down
34 changes: 32 additions & 2 deletions components/execd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
_ "go.uber.org/automaxprocs/maxprocs"

"github.com/alibaba/opensandbox/execd/pkg/clone3compat"
"github.com/alibaba/opensandbox/execd/pkg/ebpf"
"github.com/alibaba/opensandbox/execd/pkg/flag"
"github.com/alibaba/opensandbox/execd/pkg/isolation"
"github.com/alibaba/opensandbox/execd/pkg/log"
Expand Down Expand Up @@ -69,6 +70,22 @@ func run() int {
return 1
}

// Activate the pre-exec hardening floor ([hardening] enabled, OSEP-0018).
// Config errors (unknown capability, reserved execve) are fatal; missing
// runtime support degrades and is reported on the capabilities endpoint.
if err := runtime.InitHardening(isoCfg); err != nil {
log.Error("hardening: %v", err)
return 1
}

// Start the eBPF observation layer ([ebpf] enabled, OSEP-0018 §5).
// The stub build reports disabled; the execd-ebpf variant attaches the
// exec/connect/privilege hooks.
{
ebpfState, ebpfMessage := ebpf.Init(isoCfg.Ebpf, os.Getenv("OPENSANDBOX_ID"))
runtime.SetEbpfState(runtime.LayerState{State: ebpfState, Message: ebpfMessage})
}

// Probe isolation runtime capabilities.
isolationProbe := isolation.Probe(isolation.ProbeConfig{
UpperRoot: isoCfg.UpperRoot,
Expand All @@ -79,6 +96,13 @@ func run() int {

log.Init(flag.ServerLogLevel)

if flag.InitMode {
// OSEP-0018: execd is the sandbox init. Must start after the startup
// probes (which run short-lived children via cmd.Run) so the reaper is
// the only wait4 caller from here on.
runtime.StartInitMode(flag.Args())
}

ctrl := controller.InitCodeRunner()

// Always store probe result for capabilities endpoint.
Expand Down Expand Up @@ -134,10 +158,16 @@ func run() int {
return 1
}
log.Info("execd listening on %s (IPv4)", addr)
// In init mode SIGTERM belongs to the init lifecycle (forward + graceful
// shutdown with the entrypoint's exit status); only SIGINT cancels the
// HTTP server there.
ctxSignals := []os.Signal{os.Interrupt}
if !flag.InitMode {
ctxSignals = append(ctxSignals, syscall.SIGTERM)
}
serverCtx, stopSignals := signal.NotifyContext(
context.Background(),
os.Interrupt,
syscall.SIGTERM,
ctxSignals...,
)
defer stopSignals()
if err := serveHTTPUntilShutdown(serverCtx, listener, engine); err != nil {
Expand Down
Loading
Loading