mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
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.
441 lines
12 KiB
Go
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)
|
|
}
|
|
}
|