Files
Jing Chen 5867605e80 Make the bwrap alias a wrapper around the sandboxexec Go bindings.
bwrap previously built its own specs.Spec, wrote an OCI bundle and drove
runsc/container directly. It now only parses the bubblewrap command line
and translates it into sandbox.Options; //sandboxexec/sandbox owns the
bundle, the runsc invocation and the container lifecycle.
2026-08-31 01:51:10 +00:00

441 lines
12 KiB
Go

// Copyright 2026 The gVisor Authors.
//
// 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
//
// https://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 sandbox_test
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"gvisor.dev/gvisor/pkg/test/testutil"
"gvisor.dev/gvisor/sandboxexec/sandbox"
)
func TestMain(m *testing.M) {
if path, err := testutil.FindRunsc(); err == nil {
os.Setenv("RUNSC_PATH", path)
}
os.Exit(m.Run())
}
// execOutput runs argv in the sandbox and returns its standard output. It
// fails the test if the command could not be run or exited non-zero.
func execOutput(ctx context.Context, t *testing.T, sb *sandbox.Sandbox, argv ...string) string {
t.Helper()
res, err := sb.Exec(ctx, argv)
if err != nil {
t.Fatalf("failed to execute %v in sandbox: %v", argv, err)
}
if res.ExitCode != 0 {
t.Fatalf("%v exited with %d, stderr: %s", argv, res.ExitCode, res.Stderr)
}
return res.Stdout
}
func TestExecDmesg(t *testing.T) {
ctx := context.Background()
// Default network mode is NetworkModeNone which works in both root and rootless.
sb, err := sandbox.New(ctx)
if err != nil {
t.Fatalf("failed to create sandbox: %v", err)
}
defer func() {
if err := sb.Close(ctx); err != nil {
t.Fatalf("failed to clean up sandbox: %v", err)
}
}()
// Execute dmesg in the gVisor sandbox.
output := execOutput(ctx, t, sb, "dmesg")
if !strings.Contains(output, "Starting gVisor") {
t.Errorf("Exec(\"dmesg\") = %v; wanted: %v", output, "Starting gVisor")
}
}
func TestSandboxOptions(t *testing.T) {
ctx := context.Background()
runtimeDir := t.TempDir()
id := "iwillbeasandbox"
opts := []sandbox.Option{
sandbox.WithID(id),
sandbox.WithRuntimeDir(runtimeDir),
sandbox.WithNetwork(sandbox.NetworkModeHost),
}
sb, err := sandbox.New(ctx, opts...)
if err != nil {
t.Fatalf("failed to create sandbox: %v", err)
}
defer func() {
if err := sb.Close(ctx); err != nil {
t.Fatalf("failed to clean up sandbox: %v", err)
}
}()
if got := sb.Bundle(); !strings.HasPrefix(got, runtimeDir) {
t.Errorf("sb.Bundle() = %v; want prefix %v", got, runtimeDir)
}
}
func TestNonRootNetworkingError(t *testing.T) {
if os.Geteuid() == 0 {
t.Skip("Skipping test: this test must be run as non-root")
}
ctx := context.Background()
_, err := sandbox.New(ctx, sandbox.WithNetwork(sandbox.NetworkModeSandbox))
if err == nil {
t.Fatalf("sandbox.New succeeded as non-root with sandbox network mode; want error")
}
expectedErr := "sandbox networking requires running as root"
if !strings.Contains(err.Error(), expectedErr) {
t.Errorf("sandbox.New error = %v; want error containing %q", err, expectedErr)
}
}
func TestInvalidNetworkMode(t *testing.T) {
ctx := context.Background()
_, err := sandbox.New(ctx, sandbox.WithNetwork("invalid-mode"))
if err == nil {
t.Fatalf("sandbox.New succeeded with invalid network mode; want error")
}
expectedErr := "invalid network mode"
if !strings.Contains(err.Error(), expectedErr) {
t.Errorf("sandbox.New error = %v; want error containing %q", err, expectedErr)
}
}
func TestCustomBindMount(t *testing.T) {
ctx := context.Background()
tempHostDir := t.TempDir()
// Write witness file on host
witnessFile := filepath.Join(tempHostDir, "witness.txt")
expectedContent := "hello-mount"
if err := os.WriteFile(witnessFile, []byte(expectedContent), 0644); err != nil {
t.Fatalf("failed to write witness file: %v", err)
}
sb, err := sandbox.New(ctx,
sandbox.WithNetwork(sandbox.NetworkModeNone),
sandbox.WithMount(sandbox.Mount{
Type: sandbox.MountTypeBind,
Source: tempHostDir,
Destination: "/mnt/host_share",
ReadOnly: true,
}),
)
if err != nil {
t.Fatalf("failed to start sandbox: %v", err)
}
defer func() {
if err := sb.Close(ctx); err != nil {
t.Fatalf("failed to clean up sandbox: %v", err)
}
}()
// Read witness inside container
output := execOutput(ctx, t, sb, "cat", "/mnt/host_share/witness.txt")
if strings.TrimSpace(output) != expectedContent {
t.Errorf("got %q, want %q", output, expectedContent)
}
}
func TestCustomTmpfsMount(t *testing.T) {
ctx := context.Background()
sb, err := sandbox.New(ctx,
sandbox.WithNetwork(sandbox.NetworkModeNone),
sandbox.WithMount(sandbox.Mount{
Type: sandbox.MountTypeTmpfs,
Source: "tmpfs",
Destination: "/mnt/scratch",
}),
)
if err != nil {
t.Fatalf("failed to start sandbox: %v", err)
}
defer func() {
if err := sb.Close(ctx); err != nil {
t.Fatalf("failed to clean up sandbox: %v", err)
}
}()
// Write file to scratch space and cat it back
execOutput(ctx, t, sb, "sh", "-c", "echo hello-tmpfs > /mnt/scratch/tmp.txt")
output := execOutput(ctx, t, sb, "cat", "/mnt/scratch/tmp.txt")
if strings.TrimSpace(output) != "hello-tmpfs" {
t.Errorf("got %q, want %q", output, "hello-tmpfs")
}
}
func TestCustomBindMountWrite(t *testing.T) {
ctx := context.Background()
tempHostDir := t.TempDir()
sb, err := sandbox.New(ctx,
sandbox.WithNetwork(sandbox.NetworkModeNone),
sandbox.WithMount(sandbox.Mount{
Type: sandbox.MountTypeBind,
Source: tempHostDir,
Destination: "/mnt/host_share",
}),
)
if err != nil {
t.Fatalf("failed to start sandbox: %v", err)
}
defer func() {
if err := sb.Close(ctx); err != nil {
t.Fatalf("failed to clean up sandbox: %v", err)
}
}()
// Write file inside container
execOutput(ctx, t, sb, "sh", "-c", "echo hello-write > /mnt/host_share/file.txt")
// Verify file was written to the host
hostFile := filepath.Join(tempHostDir, "file.txt")
data, err := os.ReadFile(hostFile)
if err != nil {
t.Fatalf("failed to read witness file on host: %v", err)
}
if got, want := strings.TrimSpace(string(data)), "hello-write"; got != want {
t.Errorf("ReadFile(%q) = %q, want %q", hostFile, got, want)
}
}
func TestSandboxEnv(t *testing.T) {
ctx := context.Background()
sb, err := sandbox.New(ctx,
sandbox.WithNetwork(sandbox.NetworkModeNone),
sandbox.WithEnv(
"TEST_VAR=value1",
"TEST_VAR=value2", // Duplicate var to test append behavior
"ANOTHER_VAR=abc",
),
)
if err != nil {
t.Fatalf("failed to start sandbox: %v", err)
}
defer func() {
if err := sb.Close(ctx); err != nil {
t.Fatalf("failed to clean up sandbox: %v", err)
}
}()
// Read environment in the sandbox
output := execOutput(ctx, t, sb, "env")
lines := strings.Split(output, "\n")
envMap := make(map[string]string)
for _, l := range lines {
if l == "" {
continue
}
parts := strings.SplitN(l, "=", 2)
if len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}
// Verify duplicated variable is handled correctly (typically last value wins in most systems)
if got, want := envMap["TEST_VAR"], "value2"; got != want {
t.Errorf("TEST_VAR = %v, want %v", got, want)
}
// Verify normal variable injection
if got, want := envMap["ANOTHER_VAR"], "abc"; got != want {
t.Errorf("ANOTHER_VAR = %v, want %v", got, want)
}
// Verify default env vars
if _, ok := envMap["PATH"]; !ok {
t.Errorf("PATH is missing but no unset was requested")
}
}
func TestSandboxInvalidEnvFormat(t *testing.T) {
ctx := context.Background()
_, err := sandbox.New(ctx,
sandbox.WithNetwork(sandbox.NetworkModeNone),
sandbox.WithEnv("MALFORMED_INPUT_NO_EQUALS"),
)
if err == nil {
t.Fatalf("sandbox.New succeeded with malformed environment variable; want error")
}
if !strings.Contains(err.Error(), "invalid environment variable format") {
t.Errorf("sandbox.New error = %v; want error containing %q", err, "invalid environment variable format")
}
}
func TestRootfsTarSnapshot(t *testing.T) {
ctx := context.Background()
tempDir := t.TempDir()
storageDir := filepath.Join(tempDir, "storage")
if err := os.MkdirAll(storageDir, 0700); err != nil {
t.Fatalf("failed to create storage dir: %v", err)
}
storage, err := sandbox.NewFilesystemStorage(storageDir)
if err != nil {
t.Fatalf("failed to create storage: %v", err)
}
runtimeDirA := filepath.Join(tempDir, "runtime-a")
sbA, err := sandbox.New(ctx,
sandbox.WithRuntimeDir(runtimeDirA),
sandbox.WithNetwork(sandbox.NetworkModeNone),
)
if err != nil {
t.Fatalf("failed to start sandbox A: %v", err)
}
defer sbA.Close(ctx)
execOutput(ctx, t, sbA, "sh", "-c", "echo 'hello' > /test.txt")
snapshot, err := sbA.Snapshot(ctx, sandbox.RootfsTarSnapshot, storage)
if err != nil {
t.Fatalf("failed to take snapshot: %v", err)
}
runtimeDirB := filepath.Join(tempDir, "runtime-b")
sbB, err := sandbox.New(ctx,
sandbox.WithRuntimeDir(runtimeDirB),
sandbox.WithNetwork(sandbox.NetworkModeNone),
sandbox.WithSnapshot(snapshot),
)
if err != nil {
t.Fatalf("failed to start sandbox B: %v", err)
}
defer sbB.Close(ctx)
outB := execOutput(ctx, t, sbB, "cat", "/test.txt")
if strings.TrimSpace(outB) != "hello" {
t.Errorf("unexpected content in B: got %q, want %q", outB, "hello")
}
}
func TestNoSnapshotStorageError(t *testing.T) {
ctx := context.Background()
tempDir := t.TempDir()
runtimeDir := filepath.Join(tempDir, "runtime")
_, err := sandbox.New(ctx,
sandbox.WithRuntimeDir(runtimeDir),
sandbox.WithNetwork(sandbox.NetworkModeNone),
sandbox.WithSnapshot(&sandbox.Snapshot{ID: "some-snapshot-id"}),
)
if err == nil {
t.Fatalf("expected error when starting sandbox with SnapshotID but no SnapshotStore or default storage configured, got nil")
}
expectedErrSubstr := "no snapshot storage configured for restore"
if !strings.Contains(err.Error(), expectedErrSubstr) {
t.Errorf("unexpected error: %v, want it to contain %q", err, expectedErrSubstr)
}
}
func TestSandboxWorkingDir(t *testing.T) {
ctx := context.Background()
sb, err := sandbox.New(ctx,
sandbox.WithNetwork(sandbox.NetworkModeNone),
sandbox.WithWorkingDir("tmp/custom"),
)
if err != nil {
t.Fatalf("failed to start sandbox: %v", err)
}
defer sb.Close(ctx)
configPath := filepath.Join(sb.Bundle(), "config.json")
data, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("failed to read config.json: %v", err)
}
// Because of relative dir sanitization, it should be /tmp/custom
if !strings.Contains(string(data), `cwd": "/tmp/custom"`) {
t.Errorf("config.json does not contain custom working directory. got: %s", string(data))
}
}
func TestSandboxBadWorkingDir(t *testing.T) {
ctx := context.Background()
_, err := sandbox.New(ctx,
sandbox.WithNetwork(sandbox.NetworkModeNone),
sandbox.WithWorkingDir(""),
)
if err == nil {
t.Fatalf("sandbox.New succeeded with empty working directory; want error")
}
if !strings.Contains(err.Error(), "working directory cannot be empty") {
t.Errorf("sandbox.New error = %v; want error containing %q", err, "working directory cannot be empty")
}
}
func TestSandboxHostname(t *testing.T) {
ctx := context.Background()
sb, err := sandbox.New(ctx,
sandbox.WithNetwork(sandbox.NetworkModeNone),
sandbox.WithHostname("custom-sandbox-host"),
)
if err != nil {
t.Fatalf("failed to start sandbox: %v", err)
}
defer sb.Close(ctx)
output := execOutput(ctx, t, sb, "hostname")
if got, want := strings.TrimSpace(output), "custom-sandbox-host"; got != want {
t.Errorf("Exec(\"hostname\") = %q, want %q", got, want)
}
}
func TestSandboxCustomProcMount(t *testing.T) {
ctx := context.Background()
sb, err := sandbox.New(ctx,
sandbox.WithNetwork(sandbox.NetworkModeNone),
sandbox.WithMount(sandbox.Mount{
Type: sandbox.MountTypeProc,
Source: "proc",
Destination: "/custom_proc",
}),
)
if err != nil {
t.Fatalf("failed to start sandbox: %v", err)
}
defer sb.Close(ctx)
// Double check that it contains process information (e.g. status)
output := execOutput(ctx, t, sb, "cat", "/custom_proc/self/status")
if !strings.Contains(output, "Name:") {
t.Errorf("output of cat /custom_proc/self/status missing 'Name:', got: %v", output)
}
}