e0e362d700
SDK Tests / SDK CI (push) Blocked by required conditions
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
SDK Tests / Python SDK Tests (sandbox) (push) Waiting to run
SDK Tests / CLI Quality (push) Waiting to run
SDK Tests / CLI Tests (push) Waiting to run
SDK Tests / Python SDK Quality (code-interpreter) (push) Waiting to run
SDK Tests / Python SDK Quality (sandbox) (push) Waiting to run
SDK Tests / Python SDK Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Go SDK Quality And Tests (push) Waiting to run
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
184 lines
4.9 KiB
Go
184 lines
4.9 KiB
Go
// 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.
|
|
|
|
package isolation
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/alibaba/opensandbox/execd/pkg/log"
|
|
)
|
|
|
|
// ProbeResult holds the result of startup isolation probing.
|
|
type ProbeResult struct {
|
|
Available bool
|
|
Isolator string
|
|
Version string
|
|
Message string // diagnostic message when unavailable
|
|
CommitSupported bool // Phase 2
|
|
DiffSupported bool // Phase 2
|
|
PersistAvailable bool // Phase 2 — requires emptyDir
|
|
}
|
|
|
|
// ProbeConfig controls Probe behaviour.
|
|
type ProbeConfig struct {
|
|
UpperRoot string
|
|
UpperMaxBytes int64
|
|
}
|
|
|
|
// Probe runs startup detection. Returns a ProbeResult describing what
|
|
// isolation capabilities are available in the current environment.
|
|
//
|
|
// On Linux with working bwrap:
|
|
//
|
|
// Available=true, Isolator="bwrap", Version="0.10.0"
|
|
//
|
|
// Otherwise:
|
|
//
|
|
// Available=false
|
|
func Probe(cfg ProbeConfig) ProbeResult {
|
|
result := ProbeResult{}
|
|
|
|
// Check if bwrap binary is available.
|
|
version, err := probeBwrapVersion()
|
|
if err != nil {
|
|
result.Message = fmt.Sprintf("bwrap not found: %v (searched: $PATH, /opt/opensandbox/bwrap, /usr/bin/bwrap, /usr/local/bin/bwrap)", err)
|
|
log.Warn("isolation probe: %s", result.Message)
|
|
return result
|
|
}
|
|
|
|
result.Available = true
|
|
result.Isolator = "bwrap"
|
|
result.Version = version
|
|
|
|
// Smoke test: verify bwrap can actually create a namespace.
|
|
if err := probeBwrapSmoke(); err != nil {
|
|
result.Message = fmt.Sprintf("bwrap found (v%s) but smoke test failed: %v", version, err)
|
|
log.Warn("isolation probe: %s", result.Message)
|
|
result.Available = false
|
|
return result
|
|
}
|
|
|
|
if probeOverlayMount(cfg.UpperRoot) {
|
|
result.CommitSupported = true
|
|
result.DiffSupported = true
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// probeBwrapVersion returns the bwrap version string if available.
|
|
func probeBwrapVersion() (string, error) {
|
|
p := findBwrap()
|
|
if p == "" {
|
|
return "", fmt.Errorf("bwrap not found")
|
|
}
|
|
|
|
var stdout bytes.Buffer
|
|
cmd := exec.Command(p, "--version")
|
|
cmd.Stdout = &stdout
|
|
if err := cmd.Run(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// bwrap prints version to stdout, e.g.:
|
|
// "bubblewrap 0.8.0" or "bwrap 0.10.0"
|
|
out := stdout.String()
|
|
return parseBwrapVersion(out), nil
|
|
}
|
|
|
|
var bwrapVersionRe = regexp.MustCompile(`b(?:ubble)?wrap\s+(\d+\.\d+\.\d+)`)
|
|
|
|
// parseBwrapVersion extracts the version number from bwrap --version output.
|
|
func parseBwrapVersion(out string) string {
|
|
match := bwrapVersionRe.FindStringSubmatch(out)
|
|
if len(match) < 2 {
|
|
return ""
|
|
}
|
|
return match[1]
|
|
}
|
|
|
|
// probeBwrapSmoke verifies bwrap can create a minimal namespace.
|
|
func probeBwrapSmoke() error {
|
|
p := findBwrap()
|
|
if p == "" {
|
|
return fmt.Errorf("bwrap not found")
|
|
}
|
|
cmd := exec.Command(p,
|
|
"--unshare-pid", "--unshare-uts", "--unshare-ipc", "--unshare-cgroup",
|
|
"--ro-bind", "/", "/",
|
|
"--proc", "/proc",
|
|
"--", "true",
|
|
)
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("bwrap smoke test failed: %w (stderr: %s)", err, strings.TrimSpace(stderr.String()))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// probeOverlayMount tests whether bwrap can create an overlay mount.
|
|
func probeOverlayMount(upperRoot string) bool {
|
|
p := findBwrap()
|
|
if p == "" {
|
|
return false
|
|
}
|
|
|
|
// Probe on the upper root filesystem (typically tmpfs/emptyDir) rather
|
|
// than /tmp, because overlayfs cannot nest on Docker's overlay2 layer
|
|
// but works fine on tmpfs.
|
|
base := upperRoot
|
|
if base == "" {
|
|
base = os.TempDir()
|
|
}
|
|
tmpDir, err := os.MkdirTemp(base, "execd-probe-overlay-*")
|
|
if err != nil {
|
|
log.Warn("isolation probe: overlay: MkdirTemp(%s): %v", base, err)
|
|
return false
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
lowerDir := filepath.Join(tmpDir, "lower")
|
|
upperDir := filepath.Join(tmpDir, "upper")
|
|
workDir := filepath.Join(tmpDir, "work")
|
|
for _, d := range []string{lowerDir, upperDir, workDir} {
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
log.Warn("isolation probe: overlay: MkdirAll(%s): %v", d, err)
|
|
return false
|
|
}
|
|
}
|
|
|
|
cmd := exec.Command(p,
|
|
"--ro-bind", "/", "/",
|
|
"--proc", "/proc",
|
|
"--overlay-src", lowerDir,
|
|
"--overlay", upperDir, workDir, "/mnt",
|
|
"--", "true",
|
|
)
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
if err := cmd.Run(); err != nil {
|
|
log.Warn("isolation probe: overlay mount failed: %v (stderr: %s)", err, strings.TrimSpace(stderr.String()))
|
|
return false
|
|
}
|
|
return true
|
|
}
|