Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 33 additions & 0 deletions .github/workflows/execd-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,42 @@ jobs:
chmod +x tests/sigterm_forward.sh
./tests/sigterm_forward.sh

- name: Smoke test bwrap (Docker image build + extraction)
if: matrix.os == 'ubuntu-latest'
shell: bash
run: |
chmod +x components/execd/tests/smoke_bwrap.sh
bash components/execd/tests/smoke_bwrap.sh

- name: Show logs
if: always()
run: |
set -x
cat components/execd/startup.log || true
cat components/execd/execd.log || true

bwrap-smoke:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.25.9'

- name: Build latest bubblewrap (v0.11.2)
run: |
sudo apt-get install -y meson ninja-build libcap-dev pkg-config
git clone --depth 1 --branch v0.11.2 https://github.com/containers/bubblewrap /tmp/bwrap
cd /tmp/bwrap
meson setup builddir -Dprefix=/usr -Dman=disabled -Dtests=false
ninja -C builddir
sudo cp builddir/bwrap /usr/local/bin/bwrap
bwrap --version

- name: Run bwrap integration tests
working-directory: components/execd
run: sudo -E env "PATH=$PATH" go test -tags="linux,bwrap" -v -count=1 -timeout=5m ./pkg/runtime/bwrap_test/
2 changes: 2 additions & 0 deletions components/execd/.golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -317,3 +317,5 @@ issues:
text: 'exported (.+) should have comment( \(or a comment on this block\))? or be unexported'
- path: \.go$
text: "fmt.Sprintf can be replaced with string concatenation"
- path: \.go$
text: "fmt.Errorf can be replaced with errors.New"
18 changes: 18 additions & 0 deletions components/execd/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,26 @@ RUN CGO_ENABLED=0 GOOS=windows go build ${GOFLAGS} -trimpath -buildvcs=false \
-X 'github.com/alibaba/opensandbox/internal/version.GitCommit=${GIT_COMMIT}'" \
-o /build/execd.exe ./main.go

# Build static bubblewrap with musl.
FROM alpine:latest AS bwrap-builder
RUN apk add --no-cache git musl-dev meson ninja gcc libcap-dev libcap-static pkgconfig bash
RUN git clone --depth 1 --branch v0.11.2 \
https://github.com/containers/bubblewrap /build/bwrap
WORKDIR /build/bwrap
RUN rm /usr/lib/libcap.so /usr/lib/libcap.so.2 && \
meson setup builddir \
-Dprefix=/usr \
-Dman=disabled \
-Dtests=false \
-Dsupport_setuid=false \
-Ddefault_library=static \
-Dc_link_args='-static' && \
ninja -C builddir && \
cp builddir/bwrap /build/bwrap/bwrap

FROM alpine:latest

COPY --from=bwrap-builder /build/bwrap/bwrap /usr/local/bin/bwrap
COPY --from=builder /build/execd .
COPY --from=builder /build/execd.exe ./execd.exe
COPY --from=builder /build/opensandbox-supervisor ./opensandbox-supervisor
Expand Down
6 changes: 5 additions & 1 deletion components/execd/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ install-golint:

.PHONY: golint
golint: fmt install-golint
golangci-lint run -v --fix ./...
"$$(go env GOPATH)/bin/golangci-lint" run -v ./...

VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || git rev-parse --short HEAD 2>/dev/null || echo "dev")
GIT_COMMIT ?= $(shell git rev-parse HEAD 2>/dev/null || echo "unknown")
Expand All @@ -41,6 +41,10 @@ build: vet ## Build the binary.
@mkdir -p bin
go build $(GO_BUILD_FLAGS) -ldflags "$(GO_LDFLAGS)" -o bin/execd main.go

.PHONY: test-integration
test-integration: ## Run integration tests (Linux + bwrap required).
go test -v -tags="linux,bwrap" -run Integration ./pkg/runtime/bwrap_test/

.PHONY: multi-build
multi-build: vet ## Cross-compile for linux/windows/darwin amd64/arm64.
@mkdir -p bin
Expand Down
66 changes: 66 additions & 0 deletions components/execd/configs/isolation.example.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Isolation configuration for execd.
#
# Usage:
# execd --isolation-config /etc/execd/isolation.toml
# EXECD_ISOLATION_CONFIG=/etc/execd/isolation.toml execd
#
# All fields are optional. Missing fields use the defaults shown below.
# If no config file is specified, all built-in defaults are used.

# Parent directory for per-session overlay upper directories.
upper_root = "/var/lib/execd/isolation"

# Hard limit on total upper directory size across all sessions (bytes).
upper_max_bytes = 8589934592 # 8 GiB

# Maximum tar.gz diff output size (bytes). Phase 2.
diff_max_bytes = 4294967296 # 4 GiB

# Paths that callers may request as extra_writable. Empty = reject all.
allowed_writable = []

# Seccomp is ALWAYS ACTIVE by default — the built-in denylist blocks ~30
# dangerous syscalls (mount, ptrace, bpf, etc.) even without this section.
#
# Only add [seccomp] if you need to REPLACE the built-in denylist with a
# custom one. When present, deny COMPLETELY REPLACES the built-in list —
# no merging. Syscalls not present on the current architecture are silently
# skipped.
#
# Built-in default denylist for reference:
#
# [seccomp]
# deny = [
# # Filesystem manipulation
# "mount", "umount2", "chroot", "pivot_root",
#
# # Process introspection / manipulation
# "ptrace", "process_vm_readv", "process_vm_writev", "kcmp",
#
# # Kernel module loading
# "init_module", "finit_module", "delete_module",
#
# # BPF / seccomp manipulation
# "bpf", "seccomp",
#
# # Execution domain
# "personality",
#
# # Kernel key management
# "add_key", "request_key", "keyctl",
#
# # I/O privilege
# "iopl", "ioperm",
#
# # System state
# "reboot", "syslog", "swapon", "swapoff",
#
# # Namespace manipulation
# "setns", "unshare",
#
# # Handle-based operations
# "name_to_handle_at", "open_by_handle_at",
#
# # Other potentially dangerous
# "userfaultfd", "kexec_load", "kexec_file_load", "acct",
# ]
35 changes: 34 additions & 1 deletion components/execd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import (

"github.com/alibaba/opensandbox/execd/pkg/clone3compat"
"github.com/alibaba/opensandbox/execd/pkg/flag"
"github.com/alibaba/opensandbox/execd/pkg/isolation"
"github.com/alibaba/opensandbox/execd/pkg/log"
"github.com/alibaba/opensandbox/execd/pkg/runtime"
"github.com/alibaba/opensandbox/execd/pkg/telemetry"
"github.com/alibaba/opensandbox/execd/pkg/web"
"github.com/alibaba/opensandbox/execd/pkg/web/controller"
Expand All @@ -41,7 +43,39 @@ func main() {

flag.InitFlags()

// Load isolation config.
isoCfg, err := isolation.LoadConfig(flag.IsolationConfigPath)
if err != nil {
log.Error("isolation: config: %v", err)
os.Exit(1)
}

// Probe isolation runtime capabilities.
isolationProbe := isolation.Probe(isolation.ProbeConfig{
UpperRoot: isoCfg.UpperRoot,
UpperMaxBytes: isoCfg.UpperMaxBytes,
})
log.Info("isolation: available=%v isolator=%s version=%s",
isolationProbe.Available, isolationProbe.Isolator, isolationProbe.Version)

log.Init(flag.ServerLogLevel)

ctrl := controller.InitCodeRunner()
Comment thread
Pangjiping marked this conversation as resolved.

// Always store probe result for capabilities endpoint.
controller.InitIsolatedProbe(&isolationProbe)

// Init isolation runner if probe succeeded.
if isolationProbe.Available {
iso := isolation.NewBwrap(isoCfg)
runner, err := runtime.NewIsolatedRunner(ctrl, iso, isoCfg)
if err != nil {
log.Error("isolation: runner init failed (continuing without isolation): %v", err)
} else {
controller.InitIsolatedRunner(runner)
log.Info("isolation: runner ready, upper_root=%s", isoCfg.UpperRoot)
}
}
if clone3Compat {
log.Warn("execd running with clone3 compatibility (seccomp returns ENOSYS for clone3)")
}
Expand All @@ -58,7 +92,6 @@ func main() {
}()
}

controller.InitCodeRunner()
engine := web.NewRouter(flag.ServerAccessToken)
addr := fmt.Sprintf(":%d", flag.ServerPort)
listener, err := net.Listen("tcp4", addr)
Expand Down
4 changes: 4 additions & 0 deletions components/execd/pkg/flag/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ var (
// JupyterIdlePollInterval controls how often ExecuteCodeStream checks for
// late execute_result/error messages after receiving idle status.
JupyterIdlePollInterval time.Duration

// IsolationConfigPath points to the TOML isolation config file.
// Empty means use built-in defaults.
IsolationConfigPath string
)
8 changes: 8 additions & 0 deletions components/execd/pkg/flag/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
accessTokenEnv = "EXECD_ACCESS_TOKEN"
gracefulShutdownTimeoutEnv = "EXECD_API_GRACE_SHUTDOWN"
jupyterIdlePollIntervalEnv = "EXECD_JUPYTER_IDLE_POLL_INTERVAL"
isolationConfigEnv = "EXECD_ISOLATION_CONFIG"
)

// InitFlags registers CLI flags and env overrides.
Expand All @@ -40,6 +41,7 @@ func InitFlags() {
ServerAccessToken = ""
ApiGracefulShutdownTimeout = time.Second * 1
JupyterIdlePollInterval = 100 * time.Millisecond
IsolationConfigPath = ""

// First, set default values from environment variables
if jupyterFromEnv := os.Getenv(jupyterHostEnv); jupyterFromEnv != "" {
Expand Down Expand Up @@ -87,6 +89,12 @@ func InitFlags() {
flag.DurationVar(&ApiGracefulShutdownTimeout, "graceful-shutdown-timeout", ApiGracefulShutdownTimeout, "API graceful shutdown timeout duration (default: 1s)")
flag.DurationVar(&JupyterIdlePollInterval, "jupyter-idle-poll-interval", JupyterIdlePollInterval, "Polling interval after Jupyter idle status before closing stream (default: 100ms)")

// Isolation config
if v := os.Getenv(isolationConfigEnv); v != "" {
IsolationConfigPath = v
}
flag.StringVar(&IsolationConfigPath, "isolation-config", IsolationConfigPath, "Path to isolation TOML config file (default: built-in defaults)")

// Parse flags - these will override environment variables if provided
flag.Parse()
if JupyterIdlePollInterval <= 0 {
Expand Down
Loading
Loading