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
310 lines
8.4 KiB
Go
310 lines
8.4 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.
|
||
|
||
//go:build linux
|
||
|
||
package isolation
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// buildArgv constructs the bwrap command line from wrap options.
|
||
func buildArgv(opts WrapOptions, seccompFd string) ([]string, error) {
|
||
if err := validateWrapOptions(opts); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
useUserns := opts.UidMode == UidModeUserns
|
||
|
||
var argv []string
|
||
|
||
// 1. Namespace flags.
|
||
if useUserns {
|
||
argv = append(argv, "--unshare-user")
|
||
// --disable-userns is unsupported by the setuid build of bwrap;
|
||
// only add it for the non-setuid binary.
|
||
if !bwrapIsSetuid {
|
||
argv = append(argv, "--disable-userns")
|
||
}
|
||
}
|
||
argv = append(argv, "--unshare-pid", "--unshare-uts", "--hostname", "sandbox", "--unshare-ipc", "--unshare-cgroup")
|
||
if !opts.ShareNet {
|
||
argv = append(argv, "--unshare-net")
|
||
}
|
||
if useUserns {
|
||
uid := uint32(os.Getuid())
|
||
gid := uint32(os.Getgid())
|
||
if opts.Uid != nil {
|
||
uid = *opts.Uid
|
||
}
|
||
if opts.Gid != nil {
|
||
gid = *opts.Gid
|
||
}
|
||
argv = append(argv,
|
||
"--uid", strconv.FormatUint(uint64(uid), 10),
|
||
"--gid", strconv.FormatUint(uint64(gid), 10),
|
||
)
|
||
}
|
||
|
||
// 2. Root filesystem (read-only).
|
||
argv = append(argv, "--ro-bind", "/", "/")
|
||
|
||
// 3. /tmp — skip if workspace is /tmp (workspace bind would override).
|
||
if filepath.Clean(opts.Workspace.Path) != "/tmp" {
|
||
argv = append(argv, bwrapTmpSegment(opts.Profile)...)
|
||
}
|
||
|
||
// 4–6. Virtual filesystems.
|
||
argv = append(argv, "--tmpfs", "/run", "--dev", "/dev", "--proc", "/proc")
|
||
|
||
// 7. Workspace.
|
||
wsArgv, err := bwrapWorkspaceSegment(opts)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
argv = append(argv, wsArgv...)
|
||
|
||
// Hide upper root to prevent cross-session access.
|
||
if opts.UpperDir != "" {
|
||
upperRoot := filepath.Dir(filepath.Dir(opts.UpperDir))
|
||
argv = append(argv, "--tmpfs", upperRoot)
|
||
}
|
||
|
||
// 8. Extra writable paths.
|
||
for _, p := range opts.ExtraWritable {
|
||
argv = append(argv, "--bind", p, p)
|
||
}
|
||
|
||
// 8b. Explicit source→dest bind mounts.
|
||
for _, b := range opts.Binds {
|
||
dest := b.Dest
|
||
if dest == "" {
|
||
dest = b.Source
|
||
}
|
||
flag := "--bind"
|
||
if b.ReadOnly {
|
||
flag = "--ro-bind"
|
||
}
|
||
argv = append(argv, flag, b.Source, dest)
|
||
}
|
||
|
||
// 9. Environment.
|
||
argv = append(argv, bwrapEnvSegment(opts.EnvPassthrough)...)
|
||
|
||
// 10. Seccomp.
|
||
if seccompFd != "" {
|
||
argv = append(argv, "--seccomp", seccompFd)
|
||
}
|
||
|
||
// 11. Lifecycle: kill sandbox when execd dies.
|
||
// Note: --new-session is intentionally omitted. bwrap is launched with
|
||
// SysProcAttr{Setpgid: true}, making it a process-group leader, and
|
||
// setsid(2) returns EPERM for a group leader — it would fail every
|
||
// session start. Process-group isolation from Setpgid is sufficient.
|
||
argv = append(argv, "--die-with-parent")
|
||
|
||
// 12. Separator + identity switch.
|
||
argv = append(argv, "--")
|
||
|
||
// In userns mode, uid/gid are set via --uid/--gid in segment 1.
|
||
if !useUserns {
|
||
uid := uint32(os.Getuid())
|
||
gid := uint32(os.Getgid())
|
||
if opts.Uid != nil {
|
||
uid = *opts.Uid
|
||
}
|
||
if opts.Gid != nil {
|
||
gid = *opts.Gid
|
||
}
|
||
|
||
if uid != 0 || gid != 0 {
|
||
setprivArgv := []string{
|
||
"setpriv",
|
||
fmt.Sprintf("--reuid=%d", uid),
|
||
fmt.Sprintf("--regid=%d", gid),
|
||
"--clear-groups",
|
||
}
|
||
argv = append(argv, setprivArgv...)
|
||
}
|
||
}
|
||
|
||
return argv, nil
|
||
}
|
||
|
||
// validateWrapOptions checks for invalid or conflicting options.
|
||
func validateWrapOptions(opts WrapOptions) error {
|
||
if opts.Workspace.Path == "" {
|
||
return errors.New("isolation: workspace.path is required")
|
||
}
|
||
if !opts.Profile.Valid() {
|
||
return fmt.Errorf("isolation: unknown profile %q", opts.Profile)
|
||
}
|
||
if !opts.Workspace.Mode.Valid() {
|
||
return fmt.Errorf("isolation: unknown workspace mode %q", opts.Workspace.Mode)
|
||
}
|
||
if !opts.EnvPassthrough.Mode.Valid() && opts.EnvPassthrough.Mode != "" {
|
||
return fmt.Errorf("isolation: unknown env mode %q", opts.EnvPassthrough.Mode)
|
||
}
|
||
if opts.UidMode != "" && !opts.UidMode.Valid() {
|
||
return fmt.Errorf("isolation: unknown uid mode %q", opts.UidMode)
|
||
}
|
||
for _, b := range opts.Binds {
|
||
if b.Source == "" {
|
||
return errors.New("isolation: bind.source is required")
|
||
}
|
||
if !filepath.IsAbs(b.Source) {
|
||
return fmt.Errorf("isolation: bind.source %q must be an absolute path", b.Source)
|
||
}
|
||
if b.Dest != "" && !filepath.IsAbs(b.Dest) {
|
||
return fmt.Errorf("isolation: bind.dest %q must be an absolute path", b.Dest)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// bwrapTmpSegment returns the /tmp mount args for the given profile.
|
||
func bwrapTmpSegment(p Profile) []string {
|
||
switch p {
|
||
case ProfileStrict:
|
||
return []string{"--tmpfs", "/tmp"}
|
||
default:
|
||
// balanced and others: share container /tmp.
|
||
return []string{"--bind", "/tmp", "/tmp"}
|
||
}
|
||
}
|
||
|
||
// bwrapWorkspaceSegment returns mount args for the workspace.
|
||
func bwrapWorkspaceSegment(opts WrapOptions) ([]string, error) {
|
||
ws := opts.Workspace
|
||
|
||
switch ws.Mode {
|
||
case WorkspaceRW:
|
||
return []string{"--bind", ws.Path, ws.Path}, nil
|
||
|
||
case WorkspaceRO:
|
||
return []string{"--ro-bind", ws.Path, ws.Path}, nil
|
||
|
||
case WorkspaceOverlay:
|
||
if opts.UpperDir == "" {
|
||
// tmpfs upper — ephemeral. --tmp-overlay DEST (bwrap v0.11.x).
|
||
return []string{"--overlay-src", ws.Path, "--tmp-overlay", ws.Path}, nil
|
||
}
|
||
workDir := opts.WorkDir
|
||
if workDir == "" {
|
||
workDir = opts.UpperDir + "-work"
|
||
}
|
||
// --overlay-src LOWER --overlay RWSRC WORKDIR DEST
|
||
return []string{"--overlay-src", ws.Path, "--overlay", opts.UpperDir, workDir, ws.Path}, nil
|
||
|
||
default:
|
||
return nil, fmt.Errorf("isolation: unknown workspace mode %q", ws.Mode)
|
||
}
|
||
}
|
||
|
||
// unsetBlacklistedEnv returns --unsetenv args for all env vars matching strictEnvBlacklist.
|
||
func unsetBlacklistedEnv() []string {
|
||
var argv []string
|
||
for _, pattern := range strictEnvBlacklist {
|
||
for _, env := range os.Environ() {
|
||
kv := strings.SplitN(env, "=", 2)
|
||
if matchEnvPattern(kv[0], pattern) {
|
||
argv = append(argv, "--unsetenv", kv[0])
|
||
}
|
||
}
|
||
}
|
||
return argv
|
||
}
|
||
|
||
// bwrapEnvSegment returns environment passthrough args.
|
||
func bwrapEnvSegment(spec EnvSpec) []string {
|
||
if spec.Mode == "" {
|
||
return unsetBlacklistedEnv()
|
||
}
|
||
|
||
switch spec.Mode {
|
||
case EnvModeDeny:
|
||
var argv []string
|
||
for _, key := range spec.Keys {
|
||
argv = append(argv, "--unsetenv", key)
|
||
}
|
||
if len(spec.Keys) == 0 {
|
||
argv = append(argv, unsetBlacklistedEnv()...)
|
||
}
|
||
return argv
|
||
|
||
case EnvModeAllow:
|
||
// Clear environment, inject only allow-listed keys.
|
||
argv := []string{"--clearenv"}
|
||
for _, key := range spec.Keys {
|
||
if val, ok := os.LookupEnv(key); ok {
|
||
argv = append(argv, "--setenv", key, val)
|
||
}
|
||
}
|
||
return argv
|
||
|
||
default:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
// strictEnvBlacklist defines glob patterns stripped in strict profile.
|
||
var strictEnvBlacklist = []string{
|
||
"*_API_KEY", "*_TOKEN", "*_SECRET", "*_PASSWORD",
|
||
"AWS_*", "ALI_*", "ALIYUN_*", "K8S_*", "KUBE_*",
|
||
}
|
||
|
||
// matchEnvPattern performs a simple case-insensitive glob match.
|
||
func matchEnvPattern(name, pattern string) bool {
|
||
name = strings.ToUpper(name)
|
||
pattern = strings.ToUpper(pattern)
|
||
|
||
// Wildcard-only: *TOKEN* → contains TOKEN
|
||
if strings.HasPrefix(pattern, "*") && strings.HasSuffix(pattern, "*") {
|
||
mid := pattern[1 : len(pattern)-1]
|
||
return strings.Contains(name, mid)
|
||
}
|
||
// Suffix wildcard: *_TOKEN → has suffix _TOKEN
|
||
if strings.HasPrefix(pattern, "*") {
|
||
suffix := pattern[1:]
|
||
return strings.HasSuffix(name, suffix)
|
||
}
|
||
// Prefix wildcard: AWS_* → has prefix AWS_
|
||
if strings.HasSuffix(pattern, "*") {
|
||
prefix := pattern[:len(pattern)-1]
|
||
return strings.HasPrefix(name, prefix)
|
||
}
|
||
// Exact match.
|
||
return name == pattern
|
||
}
|
||
|
||
// Wrap rewrites cmd to execute under bwrap.
|
||
func wrapWithArgv(cmd *exec.Cmd, bwrapPath string, argv []string) {
|
||
// Prepend bwrap argv before the original command.
|
||
// argv already ends with ["--", "setpriv", ...] and the original
|
||
// cmd.Args[0] is the user command after setpriv.
|
||
userArgs := cmd.Args
|
||
cmd.Args = make([]string, 0, len(argv)+len(userArgs))
|
||
cmd.Args = append(cmd.Args, bwrapPath)
|
||
cmd.Args = append(cmd.Args, argv...)
|
||
cmd.Args = append(cmd.Args, userArgs...)
|
||
cmd.Path = bwrapPath
|
||
}
|