Files
visor/test/e2e/integration_test.go
Shailend ChandandgVisor bot c5cf1507f2 Try to deflake TestJobControl and exec_test.go tests
Keyword being "Try".

Instead of sleeping a fixed duration and then resuming with
the assumption that the launched process is up, we now poll
till the launched process is seen to be up.

PiperOrigin-RevId: 956087777
2026-07-29 14:24:25 -07:00

1549 lines
46 KiB
Go

// Copyright 2018 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
//
// 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 integration provides end-to-end integration tests for runsc.
//
// Each test calls docker commands to start up a container, and tests that it is
// behaving properly, with various runsc commands. The container is killed and
// deleted at the end.
//
// Setup instruction in test/README.md.
package integration
import (
"bytes"
"context"
"flag"
"fmt"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"testing"
"time"
"github.com/docker/docker/api/types/mount"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/test/dockerutil"
"gvisor.dev/gvisor/pkg/test/testutil"
"gvisor.dev/gvisor/runsc/specutils"
)
const (
// defaultWait is the default wait time used for tests.
defaultWait = time.Minute
memInfoCmd = "cat /proc/meminfo | grep MemTotal: | awk '{print $2}'"
)
func TestMain(m *testing.M) {
dockerutil.EnsureSupportedDockerVersion()
flag.Parse()
os.Exit(m.Run())
}
// httpRequestSucceeds sends a request to a given url and checks that the status is OK.
func httpRequestSucceeds(client http.Client, server string, port int) error {
url := fmt.Sprintf("http://%s:%d", server, port)
// Ensure that content is being served.
resp, err := client.Get(url)
if err != nil {
return fmt.Errorf("error reaching http server: %v", err)
}
if want := http.StatusOK; resp.StatusCode != want {
return fmt.Errorf("wrong response code, got: %d, want: %d", resp.StatusCode, want)
}
return nil
}
// TestLifeCycle tests a basic Create/Start/Stop docker container life cycle.
func TestLifeCycle(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
// Start the container.
port := 80
if err := d.Create(ctx, dockerutil.RunOpts{
Image: "basic/nginx",
}); err != nil {
t.Fatalf("docker create failed: %v", err)
}
if err := d.Start(ctx); err != nil {
t.Fatalf("docker start failed: %v", err)
}
ip, err := d.FindIP(ctx, false)
if err != nil {
t.Fatalf("docker.FindIP failed: %v", err)
}
if err := testutil.WaitForHTTP(ip.String(), port, defaultWait); err != nil {
t.Fatalf("WaitForHTTP() timeout: %v", err)
}
client := http.Client{Timeout: defaultWait}
if err := httpRequestSucceeds(client, ip.String(), port); err != nil {
t.Errorf("http request failed: %v", err)
}
if err := d.Stop(ctx); err != nil {
t.Fatalf("docker stop failed: %v", err)
}
if err := d.Remove(ctx); err != nil {
t.Fatalf("docker rm failed: %v", err)
}
}
func TestDisallowRootfsTarAnnotation(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
// --allow-rootfs-tar-annotation is false by default.
_, err := d.Run(ctx, dockerutil.RunOpts{
Image: "basic/alpine",
Annotations: map[string]string{specutils.AnnotationRootfsUpperTar: "foo"},
}, "sh", "-c", "ls")
errMsg := "rootfs tar annotation is disabled, use --allow-rootfs-tar-annotation to enable it"
if err == nil || !strings.Contains(err.Error(), errMsg) {
t.Errorf("expected error containing %q, got %v", errMsg, err)
}
}
func TestPauseResume(t *testing.T) {
if !testutil.IsCheckpointSupported() {
t.Skip("Checkpoint is not supported.")
}
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
// Start the container.
port := 8080
if err := d.Spawn(ctx, dockerutil.RunOpts{
Image: "basic/python",
}); err != nil {
t.Fatalf("docker run failed: %v", err)
}
// Find container IP address.
ip, err := d.FindIP(ctx, false)
if err != nil {
t.Fatalf("docker.FindIP failed: %v", err)
}
// Wait until it's up and running.
if err := testutil.WaitForHTTP(ip.String(), port, defaultWait); err != nil {
t.Fatalf("WaitForHTTP() timeout: %v", err)
}
// Check that container is working.
client := http.Client{Timeout: defaultWait}
if err := httpRequestSucceeds(client, ip.String(), port); err != nil {
t.Error("http request failed:", err)
}
if err := d.Pause(ctx); err != nil {
t.Fatalf("docker pause failed: %v", err)
}
// Check if container is paused.
client = http.Client{Timeout: 10 * time.Millisecond} // Don't wait a minute.
switch _, err := client.Get(fmt.Sprintf("http://%s:%d", ip.String(), port)); v := err.(type) {
case nil:
t.Errorf("http req expected to fail but it succeeded")
case net.Error:
if !v.Timeout() {
t.Errorf("http req got error %v, wanted timeout", v)
}
default:
t.Errorf("http req got unexpected error %v", v)
}
if err := d.Unpause(ctx); err != nil {
t.Fatalf("docker unpause failed: %v", err)
}
// Wait until it's up and running.
if err := testutil.WaitForHTTP(ip.String(), port, defaultWait); err != nil {
t.Fatalf("WaitForHTTP() timeout: %v", err)
}
// Check if container is working again.
client = http.Client{Timeout: defaultWait}
if err := httpRequestSucceeds(client, ip.String(), port); err != nil {
t.Error("http request failed:", err)
}
}
func TestCheckpointRestore(t *testing.T) {
if !testutil.IsCheckpointSupported() {
t.Skip("Checkpoint is not supported.")
}
dockerutil.EnsureDockerExperimentalEnabled()
if !dockerutil.IsRestoreSupported() {
t.Skip("Restore is not supported.")
}
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
// Start the container.
port := 8080
if err := d.Spawn(ctx, dockerutil.RunOpts{
Image: "basic/python",
}); err != nil {
t.Fatalf("docker run failed: %v", err)
}
// Create a snapshot.
const ckptName = "test"
if err := d.Checkpoint(ctx, ckptName); err != nil {
t.Fatalf("docker checkpoint failed: %v", err)
}
if err := d.WaitTimeout(ctx, defaultWait); err != nil {
t.Fatalf("wait failed: %v", err)
}
d.RestoreInTest(ctx, t, ckptName)
// Find container IP address.
ip, err := d.FindIP(ctx, false)
if err != nil {
t.Fatalf("docker.FindIP failed: %v", err)
}
// Wait until it's up and running.
if err := testutil.WaitForHTTP(ip.String(), port, defaultWait); err != nil {
t.Fatalf("WaitForHTTP() timeout: %v", err)
}
// Check if container is working again.
client := http.Client{Timeout: defaultWait}
if err := httpRequestSucceeds(client, ip.String(), port); err != nil {
t.Error("http request failed:", err)
}
}
// Create client and server that talk to each other using the local IP.
func TestConnectToSelf(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
// Creates server that replies "server" and exits. Sleeps at the end because
// 'docker exec' gets killed if the init process exits before it can finish.
if err := d.Spawn(ctx, dockerutil.RunOpts{
Image: "basic/ubuntu",
}, "/bin/sh", "-c", "echo server | nc -l -p 8080 && sleep 1"); err != nil {
t.Fatalf("docker run failed: %v", err)
}
// Finds IP address for host.
ip, err := d.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", "cat /etc/hosts | grep ${HOSTNAME} | awk '{print $1}'")
if err != nil {
t.Fatalf("docker exec failed: %v", err)
}
ip = strings.TrimRight(ip, "\n")
// Runs client that sends "client" to the server and exits.
reply, err := d.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", fmt.Sprintf("echo client | nc %s 8080", ip))
if err != nil {
t.Fatalf("docker exec failed: %v", err)
}
// Ensure both client and server got the message from each other.
if want := "server\n"; reply != want {
t.Errorf("Error on server, want: %q, got: %q", want, reply)
}
if _, err := d.WaitForOutput(ctx, "^client\n$", defaultWait); err != nil {
t.Fatalf("docker.WaitForOutput(client) timeout: %v", err)
}
}
func TestMemory(t *testing.T) {
// Find total amount of memory in the host.
host, err := exec.Command("sh", "-c", memInfoCmd).CombinedOutput()
if err != nil {
t.Fatal(err)
}
want, err := strconv.ParseUint(strings.TrimSpace(string(host)), 10, 64)
if err != nil {
t.Fatalf("failed to parse %q: %v", host, err)
}
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
out, err := d.Run(ctx, dockerutil.RunOpts{Image: "basic/alpine"}, "sh", "-c", memInfoCmd)
if err != nil {
t.Fatalf("docker run failed: %v", err)
}
// Get memory from inside the container and ensure it matches the host.
got, err := strconv.ParseUint(strings.TrimSpace(out), 10, 64)
if err != nil {
t.Fatalf("failed to parse %q: %v", out, err)
}
if got != want {
t.Errorf("MemTotal got: %d, want: %d", got, want)
}
}
func TestMemLimit(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
allocMemoryKb := 128 * 1024
runArgs, err := dockerutil.RuntimeArgs()
if err != nil {
t.Fatalf("dockerutil.RuntimeArgs() failed: %v", err)
}
if slices.Contains(runArgs, "--platform=kvm") {
// TODO: b/498290339 - KVM allocates more than 128 MB on startup and OOMs the sandbox.
allocMemoryKb = 150 * 1024
}
opts := dockerutil.RunOpts{
Image: "basic/alpine",
Memory: allocMemoryKb * 1024, // In bytes.
}
out, err := d.Run(ctx, opts, "sh", "-c", memInfoCmd)
if err != nil {
t.Fatalf("docker run failed: %v", err)
}
// Remove warning message that swap isn't present.
if strings.HasPrefix(out, "WARNING") {
lines := strings.Split(out, "\n")
if len(lines) != 3 {
t.Fatalf("invalid output: %s", out)
}
out = lines[1]
}
// Ensure the memory matches what we want.
got, err := strconv.ParseUint(strings.TrimSpace(out), 10, 64)
if err != nil {
t.Fatalf("failed to parse %q: %v", out, err)
}
if want := uint64(allocMemoryKb); got != want {
t.Errorf("MemTotal got: %d, want: %d", got, want)
}
}
func TestNumCPU(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
runArgs, err := dockerutil.RuntimeArgs()
if err != nil {
t.Fatalf("dockerutil.RuntimeArgs() failed: %v", err)
}
want := 1
if slices.Contains(runArgs, "--platform=kvm") && slices.Contains(runArgs, "--kvm-use-cpu-nums") {
// When kvm-use-cpu-nums is enabled, platform/KVM will advertise vCPU IDs as CPU numbers.
// platform/KVM under-the-hood adjusts vCPUs to optimize scheduling.
// This means the number of CPUs reported inside the container may not match the number
// of application cores requested by the user.
want = 3
}
defer d.CleanUp(ctx)
// Read how many cores are in the container.
out, err := d.Run(ctx, dockerutil.RunOpts{
Image: "basic/alpine",
CpusetCpus: "0",
}, "sh", "-c", "cat /proc/cpuinfo | grep 'processor.*:' | wc -l")
if err != nil {
t.Fatalf("docker run failed: %v", err)
}
// Ensure it matches what we want.
got, err := strconv.Atoi(strings.TrimSpace(out))
if err != nil {
t.Fatalf("failed to parse %q: %v", out, err)
}
if got != want {
t.Errorf("NumCPU got: %d, want: %d", got, want)
}
}
// TestJobControl tests that job control characters are handled properly.
func TestJobControl(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
// Start the container with an attached PTY.
p, err := d.SpawnProcess(ctx, dockerutil.RunOpts{
Image: "basic/alpine",
}, "sh", "-c", "sleep 100 | cat")
if err != nil {
t.Fatalf("docker run failed: %v", err)
}
// Wait until the pipeline is up and running.
if err := testutil.Poll(func() error {
out, err := d.Exec(ctx, dockerutil.ExecOpts{}, "sh", "-c", "pgrep -x sleep && pgrep -x cat")
if err != nil {
return fmt.Errorf("sleep|cat pipeline not yet running: %v (output: %s)", err, out)
}
return nil
}, defaultWait); err != nil {
t.Fatalf("waiting for the sleep|cat pipeline to start: %v", err)
}
if _, err := p.Write(defaultWait, []byte{0x03}); err != nil {
t.Fatalf("error exit: %v", err)
}
if logs, err := p.Logs(); err != nil {
t.Logf("failed to read output: %s", err)
} else {
t.Logf("output: %s", logs)
}
if err := d.WaitTimeout(ctx, defaultWait); err != nil {
t.Fatalf("WaitTimeout failed: %v", err)
}
want := 130
got, err := p.WaitExitStatus(ctx)
if err != nil {
t.Fatalf("wait for exit failed with: %v", err)
} else if got != want {
t.Fatalf("got: %d want: %d", got, want)
}
}
// TestWorkingDirCreation checks that working dir is created if it doesn't exit.
func TestWorkingDirCreation(t *testing.T) {
for _, tc := range []struct {
name string
workingDir string
}{
{name: "root", workingDir: "/foo"},
{name: "tmp", workingDir: "/tmp/foo"},
} {
for _, readonly := range []bool{true, false} {
name := tc.name
if readonly {
name += "-readonly"
}
t.Run(name, func(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
opts := dockerutil.RunOpts{
Image: "basic/alpine",
WorkDir: tc.workingDir,
ReadOnly: readonly,
}
got, err := d.Run(ctx, opts, "sh", "-c", "echo ${PWD}")
if err != nil {
t.Fatalf("docker run failed: %v", err)
}
if want := tc.workingDir + "\n"; want != got {
t.Errorf("invalid working dir, want: %q, got: %q", want, got)
}
})
}
}
}
// TestTmpFile checks that files inside '/tmp' are not overridden.
func TestTmpFile(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
opts := dockerutil.RunOpts{Image: "basic/tmpfile"}
got, err := d.Run(ctx, opts, "cat", "/tmp/foo/file.txt")
if err != nil {
t.Fatalf("docker run failed: %v", err)
}
if want := "123\n"; want != got {
t.Errorf("invalid file content, want: %q, got: %q", want, got)
}
}
// TestTmpMount checks that mounts inside '/tmp' are not overridden.
func TestTmpMount(t *testing.T) {
dir, err := os.MkdirTemp(testutil.TmpDir(), "tmp-mount")
if err != nil {
t.Fatalf("TempDir(): %v", err)
}
const want = "123"
if err := os.WriteFile(filepath.Join(dir, "file.txt"), []byte("123"), 0666); err != nil {
t.Fatalf("WriteFile(): %v", err)
}
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
opts := dockerutil.RunOpts{
Image: "basic/alpine",
Mounts: []mount.Mount{
{
Type: mount.TypeBind,
Source: dir,
Target: "/tmp/foo",
},
},
}
got, err := d.Run(ctx, opts, "cat", "/tmp/foo/file.txt")
if err != nil {
t.Fatalf("docker run failed: %v", err)
}
if want != got {
t.Errorf("invalid file content, want: %q, got: %q", want, got)
}
}
// Test that it is allowed to mount a file on top of /dev files, e.g.
// /dev/random.
func TestMountOverDev(t *testing.T) {
random, err := os.CreateTemp(testutil.TmpDir(), "random")
if err != nil {
t.Fatal("os.CreateTemp() failed:", err)
}
const want = "123"
if _, err := random.WriteString(want); err != nil {
t.Fatalf("WriteString() to %q: %v", random.Name(), err)
}
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
opts := dockerutil.RunOpts{
Image: "basic/alpine",
Mounts: []mount.Mount{
{
Type: mount.TypeBind,
Source: random.Name(),
Target: "/dev/random",
},
},
}
cmd := "dd count=1 bs=5 if=/dev/random 2> /dev/null"
got, err := d.Run(ctx, opts, "sh", "-c", cmd)
if err != nil {
t.Fatalf("docker run failed: %v", err)
}
if want != got {
t.Errorf("invalid file content, want: %q, got: %q", want, got)
}
}
// TestSyntheticDirs checks that submounts can be created inside a readonly
// mount even if the target path does not exist.
func TestSyntheticDirs(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
opts := dockerutil.RunOpts{
Image: "basic/alpine",
// Make the root read-only to force use of synthetic dirs
// inside the root gofer mount.
ReadOnly: true,
Mounts: []mount.Mount{
// Mount inside read-only gofer-backed root.
{
Type: mount.TypeTmpfs,
Target: "/foo/bar/baz",
},
// Mount inside sysfs, which always uses synthetic dirs
// for submounts.
{
Type: mount.TypeTmpfs,
Target: "/sys/foo/bar/baz",
},
},
}
// Make sure the directories exist.
if _, err := d.Run(ctx, opts, "ls", "/foo/bar/baz", "/sys/foo/bar/baz"); err != nil {
t.Fatalf("docker run failed: %v", err)
}
}
// TestHostOverlayfsCopyUp tests that the --overlayfs-stale-read option causes
// runsc to hide the incoherence of FDs opened before and after overlayfs
// copy-up on the host.
func TestHostOverlayfsCopyUp(t *testing.T) {
runIntegrationTest(t, nil, "./test_copy_up")
}
// TestHostOverlayfsRewindDir tests that rewinddir() "causes the directory
// stream to refer to the current state of the corresponding directory, as a
// call to opendir() would have done" as required by POSIX, when the directory
// in question is host overlayfs.
//
// This test specifically targets host overlayfs because, per POSIX, "if a file
// is removed from or added to the directory after the most recent call to
// opendir() or rewinddir(), whether a subsequent call to readdir() returns an
// entry for that file is unspecified"; the host filesystems used by other
// automated tests yield newly-added files from readdir() even if the fsgofer
// does not explicitly rewinddir(), but overlayfs does not.
func TestHostOverlayfsRewindDir(t *testing.T) {
runIntegrationTest(t, nil, "./test_rewinddir")
}
// Basic test for linkat(2). Syscall tests requires CAP_DAC_READ_SEARCH and it
// cannot use tricks like userns as root. For this reason, run a basic link test
// to ensure some coverage.
func TestLink(t *testing.T) {
runIntegrationTest(t, nil, "./link_test")
}
// This test ensures we can run ping without errors.
func TestPing4Loopback(t *testing.T) {
runIntegrationTest(t, nil, "./ping4.sh")
}
// This test ensures we can enable ipv6 on loopback and run ping6 without
// errors.
func TestPing6Loopback(t *testing.T) {
if testutil.IsRunningWithHostNet() {
// TODO(gvisor.dev/issue/5011): support ICMP sockets in hostnet and enable
// this test.
t.Skip("hostnet only supports TCP/UDP sockets, so ping6 is not supported.")
}
// The CAP_NET_ADMIN capability is required to use the `ip` utility, which
// we use to enable ipv6 on loopback.
//
// By default, ipv6 loopback is not enabled by runsc, because docker does
// not assign an ipv6 address to the test container.
runIntegrationTest(t, []string{"NET_ADMIN"}, "./ping6.sh")
}
// This test checks that the owner of the sticky directory can delete files
// inside it belonging to other users. It also checks that the owner of a file
// can always delete its file when the file is inside a sticky directory owned
// by another user.
func TestStickyDir(t *testing.T) {
runIntegrationTest(t, nil, "./test_sticky")
}
func TestHostFD(t *testing.T) {
t.Run("regular", func(t *testing.T) {
runIntegrationTest(t, nil, "./host_fd")
})
t.Run("tty", func(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
// Start the container with an attached PTY.
p, err := d.SpawnProcess(ctx, dockerutil.RunOpts{
Image: "basic/integrationtest",
WorkDir: "/root",
}, "./host_fd", "-t")
if err != nil {
t.Fatalf("docker run failed: %v", err)
}
if err := d.Wait(ctx); err != nil {
t.Fatalf("Wait failed: %v", err)
}
if out, err := p.Logs(); err != nil {
t.Fatal(err)
} else if len(out) > 0 {
t.Errorf("test failed:\n%s", out)
}
})
}
func runIntegrationTest(t *testing.T, capAdd []string, args ...string) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
opts := dockerutil.RunOpts{
Image: "basic/integrationtest",
WorkDir: "/root",
CapAdd: capAdd,
}
if got, err := d.Run(ctx, opts, args...); err != nil {
t.Fatalf("docker run failed: %v", err)
} else if got != "" {
t.Errorf("test failed:\n%s", got)
}
}
// Test that UDS can be created using overlay when parent directory is in lower
// layer only (b/134090485).
//
// Prerequisite: the directory where the socket file is created must not have
// been open for write before bind(2) is called.
func TestBindOverlay(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
// Run the container.
got, err := d.Run(ctx, dockerutil.RunOpts{
Image: "basic/ubuntu",
}, "bash", "-c", "nc -q -1 -l -U /var/run/sock & p=$! && sleep 1 && echo foobar-asdf | nc -q 0 -U /var/run/sock && wait $p")
if err != nil {
t.Fatalf("docker run failed: %v", err)
}
// Check the output contains what we want.
if want := "foobar-asdf"; !strings.Contains(got, want) {
t.Fatalf("docker run output is missing %q: %s", want, got)
}
}
func TestStdios(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
testStdios(t, func(user string, args ...string) (string, error) {
defer d.CleanUp(ctx)
opts := dockerutil.RunOpts{
Image: "basic/alpine",
User: user,
}
return d.Run(ctx, opts, args...)
})
}
func TestStdiosExec(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
runOpts := dockerutil.RunOpts{Image: "basic/alpine"}
if err := d.Spawn(ctx, runOpts, "sleep", "100"); err != nil {
t.Fatalf("docker run failed: %v", err)
}
testStdios(t, func(user string, args ...string) (string, error) {
opts := dockerutil.ExecOpts{User: user}
return d.Exec(ctx, opts, args...)
})
}
func testStdios(t *testing.T, run func(string, ...string) (string, error)) {
const cmd = "stat -L /proc/self/fd/0 /proc/self/fd/1 /proc/self/fd/2 | grep 'Uid:'"
got, err := run("123", "/bin/sh", "-c", cmd)
if err != nil {
t.Fatalf("docker exec failed: %v", err)
}
if len(got) == 0 {
t.Errorf("Unexpected empty output from %q", cmd)
}
re := regexp.MustCompile(`Uid: \(\s*(\w+)\/.*\)`)
for _, line := range strings.SplitN(got, "\n", 3) {
t.Logf("stat -L: %s", line)
matches := re.FindSubmatch([]byte(line))
if len(matches) != 2 {
t.Fatalf("wrong output format: %q: matches: %v", line, matches)
}
if want, got := "123", string(matches[1]); want != got {
t.Errorf("wrong user, want: %q, got: %q", want, got)
}
}
// Check that stdout and stderr can be open and written to. This checks
// that ownership and permissions are correct inside gVisor.
got, err = run("456", "/bin/sh", "-c", "echo foobar | tee /proc/self/fd/1 > /proc/self/fd/2")
if err != nil {
t.Fatalf("docker run failed: %v", err)
}
t.Logf("echo foobar: %q", got)
// Check it repeats twice, once for stdout and once for stderr.
if want := "foobar\nfoobar\n"; want != got {
t.Errorf("Wrong echo output, want: %q, got: %q", want, got)
}
// Check that timestamps can be changed. Setting timestamps require an extra
// write check _after_ the file was opened, and may fail if the underlying
// host file is not setup correctly.
if _, err := run("789", "touch", "/proc/self/fd/0", "/proc/self/fd/1", "/proc/self/fd/2"); err != nil {
t.Fatalf("docker run failed: %v", err)
}
}
func TestStdiosChown(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
opts := dockerutil.RunOpts{Image: "basic/alpine"}
if _, err := d.Run(ctx, opts, "chown", "123", "/proc/self/fd/0", "/proc/self/fd/1", "/proc/self/fd/2"); err != nil {
t.Fatalf("docker run failed: %v", err)
}
}
func TestUnmount(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
dir, err := os.MkdirTemp(testutil.TmpDir(), "sub-mount")
if err != nil {
t.Fatalf("TempDir(): %v", err)
}
opts := dockerutil.RunOpts{
Image: "basic/alpine",
Privileged: true, // Required for umount
Mounts: []mount.Mount{
{
Type: mount.TypeBind,
Source: dir,
Target: "/foo",
},
},
}
if _, err := d.Run(ctx, opts, "umount", "/foo"); err != nil {
t.Fatalf("docker run failed: %v", err)
}
}
func TestDeleteInterface(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
opts := dockerutil.RunOpts{
Image: "basic/alpine",
CapAdd: []string{"NET_ADMIN"},
}
if err := d.Spawn(ctx, opts, "sleep", "1000"); err != nil {
t.Fatalf("docker run failed: %v", err)
}
// We should be able to remove eth0.
output, err := d.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", "ip link del dev eth0")
if err != nil {
t.Fatalf("failed to remove eth0: %s, output: %s", err, output)
}
// Verify that eth0 is no longer there.
output, err = d.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", "ip link show")
if err != nil {
t.Fatalf("docker exec ip link show failed: %s, output: %s", err, output)
}
if strings.Contains(output, "eth0") {
t.Fatalf("failed to remove eth0")
}
// Loopback device can't be removed.
output, err = d.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", "ip link del dev lo")
if err == nil {
t.Fatalf("should not remove the loopback device: %v", output)
}
// Verify that lo is still there.
output, err = d.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", "ip link show")
if err != nil {
t.Fatalf("docker exec ip link show failed: %s, output: %s", err, output)
}
if !strings.Contains(output, "lo") {
t.Fatalf("loopback interface is removed")
}
}
func TestProductName(t *testing.T) {
want, err := os.ReadFile("/sys/devices/virtual/dmi/id/product_name")
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
opts := dockerutil.RunOpts{Image: "basic/alpine"}
got, err := d.Run(ctx, opts, "cat", "/sys/devices/virtual/dmi/id/product_name")
if err != nil {
t.Fatalf("docker run failed: %v", err)
}
if string(want) != got {
t.Errorf("invalid product name, want: %q, got: %q", want, got)
}
}
// TestRevalidateSymlinkChain tests that when a symlink in the middle of chain
// gets updated externally, the change is noticed and the internal cache is
// updated accordingly.
func TestRevalidateSymlinkChain(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
// Create the following structure:
// dir
// + gen1
// | + file [content: 123]
// |
// + gen2
// | + file [content: 456]
// |
// + file -> sym1/file
// + sym1 -> sym2
// + sym2 -> gen1
//
dir, err := os.MkdirTemp(testutil.TmpDir(), "sub-mount")
if err != nil {
t.Fatalf("TempDir(): %v", err)
}
if err := os.Mkdir(filepath.Join(dir, "gen1"), 0777); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(filepath.Join(dir, "gen2"), 0777); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "gen1", "file"), []byte("123"), 0666); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "gen2", "file"), []byte("456"), 0666); err != nil {
t.Fatal(err)
}
if err := os.Symlink("sym1/file", filepath.Join(dir, "file")); err != nil {
t.Fatal(err)
}
if err := os.Symlink("sym2", filepath.Join(dir, "sym1")); err != nil {
t.Fatal(err)
}
if err := os.Symlink("gen1", filepath.Join(dir, "sym2")); err != nil {
t.Fatal(err)
}
// Mount dir inside the container so that external changes are propagated to
// the container.
opts := dockerutil.RunOpts{
Image: "basic/alpine",
Privileged: true, // Required for umount
Mounts: []mount.Mount{
{
Type: mount.TypeBind,
Source: dir,
Target: "/foo",
},
},
}
if err := d.Create(ctx, opts, "sleep", "1000"); err != nil {
t.Fatalf("docker run failed: %v", err)
}
if err := d.Start(ctx); err != nil {
t.Fatalf("docker run failed: %v", err)
}
// Read and cache symlinks pointing to gen1/file.
got, err := d.Exec(ctx, dockerutil.ExecOpts{}, "cat", "/foo/file")
if err != nil {
t.Fatalf("docker run failed: %v", err)
}
if want := "123"; got != want {
t.Fatalf("Read wrong file, want: %q, got: %q", want, got)
}
// Change the symlink to point to gen2 file.
if err := os.Remove(filepath.Join(dir, "sym2")); err != nil {
t.Fatal(err)
}
if err := os.Symlink("gen2", filepath.Join(dir, "sym2")); err != nil {
t.Fatal(err)
}
// Read symlink chain again and check that it got updated to gen2/file.
got, err = d.Exec(ctx, dockerutil.ExecOpts{}, "cat", "/foo/file")
if err != nil {
t.Fatalf("docker run failed: %v", err)
}
if want := "456"; got != want {
t.Fatalf("Read wrong file, want: %q, got: %q", want, got)
}
}
// TestTmpMountWithSize checks when 'tmpfs' is mounted
// with size option the limit is not exceeded.
func TestTmpMountWithSize(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
opts := dockerutil.RunOpts{
Image: "basic/alpine",
Mounts: []mount.Mount{
{
Type: mount.TypeTmpfs,
Target: "/tmp/foo",
TmpfsOptions: &mount.TmpfsOptions{
SizeBytes: 4096,
},
},
},
}
if err := d.Create(ctx, opts, "sleep", "1000"); err != nil {
t.Fatalf("docker create failed: %v", err)
}
if err := d.Start(ctx); err != nil {
t.Fatalf("docker start failed: %v", err)
}
if _, err := d.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", "echo hello > /tmp/foo/test1.txt"); err != nil {
t.Fatalf("docker exec failed: %v", err)
}
echoOutput, err := d.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", "echo world > /tmp/foo/test2.txt")
if err == nil {
t.Fatalf("docker exec size check unexpectedly succeeded (output: %v)", echoOutput)
}
wantErr := "No space left on device"
if !strings.Contains(echoOutput, wantErr) {
t.Errorf("unexpected echo error:Expected: %v, Got: %v", wantErr, echoOutput)
}
}
func TestLibaclOnTmpMount(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
opts := dockerutil.RunOpts{
Image: "basic/libacl",
Mounts: []mount.Mount{
{
Type: mount.TypeTmpfs,
Target: "/mymnt",
},
},
}
if err := d.Create(ctx, opts, "sleep", "infinity"); err != nil {
t.Fatalf("docker create failed: %v", err)
}
if err := d.Start(ctx); err != nil {
t.Fatalf("docker start failed: %v", err)
}
if _, err := d.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", "mkdir /mymnt/acl_test"); err != nil {
t.Fatalf("docker exec failed: %v", err)
}
// Try to add named users/groups to the directory as a default ACL.
cmd := "setfacl -d -m u:alice:rwx,g:employees:rwx /mymnt/acl_test"
if _, err := d.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", cmd); err != nil {
t.Fatalf("docker exec failed: %v", err)
}
// Create a new file in the directory.
if _, err := d.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", "echo hello > /mymnt/acl_test/file"); err != nil {
t.Fatalf("docker exec failed: %v", err)
}
// The default ACL is inherited as the file's access ACL.
cmd = "getfacl --access --no-effective --omit-header /mymnt/acl_test/file"
out, err := d.Exec(ctx, dockerutil.ExecOpts{}, "/bin/sh", "-c", cmd)
if err != nil {
t.Fatalf("docker exec failed: %v", err)
}
lines := strings.Fields(out)
for _, y := range []string{"user:alice:rwx", "group:employees:rwx"} {
if !slices.Contains(lines, y) {
t.Fatalf("getfacl output: expected %v, not found in: %v", y, lines)
}
}
}
// NOTE(b/236028361): Regression test. Check we can handle a working directory
// without execute permissions. See comment in
// pkg/sentry/kernel/kernel.go:CreateProcess() for more context.
func TestNonSearchableWorkingDirectory(t *testing.T) {
dir, err := os.MkdirTemp(testutil.TmpDir(), "tmp-mount")
if err != nil {
t.Fatalf("MkdirTemp() failed: %v", err)
}
defer os.RemoveAll(dir)
// The container will run as a non-root user. Make dir not searchable by
// others by removing execute bit for others.
if err := os.Chmod(dir, 0766); err != nil {
t.Fatalf("Chmod() failed: %v", err)
}
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
targetMount := "/foo"
opts := dockerutil.RunOpts{
Image: "basic/alpine",
Mounts: []mount.Mount{
{
Type: mount.TypeBind,
Source: dir,
Target: targetMount,
},
},
WorkDir: targetMount,
User: "nobody",
}
echoPhrase := "All izz well"
got, err := d.Run(ctx, opts, "sh", "-c", "echo "+echoPhrase+" && (ls || true)")
if err != nil {
t.Fatalf("docker run failed: %v", err)
}
if !strings.Contains(got, echoPhrase) {
t.Errorf("echo output not found, want: %q, got: %q", echoPhrase, got)
}
if wantErrorMsg := "Permission denied"; !strings.Contains(got, wantErrorMsg) {
t.Errorf("ls error message not found, want: %q, got: %q", wantErrorMsg, got)
}
}
func TestCharDevice(t *testing.T) {
if testutil.IsRunningWithOverlay() {
t.Skip("files are not available outside the sandbox with overlay.")
}
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
dir, err := os.MkdirTemp(testutil.TmpDir(), "tmp-mount")
if err != nil {
t.Fatalf("MkdirTemp() failed: %v", err)
}
defer os.RemoveAll(dir)
opts := dockerutil.RunOpts{
Image: "basic/alpine",
Mounts: []mount.Mount{
{
Type: mount.TypeBind,
Source: "/dev/zero",
Target: "/test/zero",
},
{
Type: mount.TypeBind,
Source: dir,
Target: "/out",
},
},
}
const size = 1024 * 1024
// `docker logs` encodes the string, making it hard to compare. Write the
// result to a file that is available to the test.
cmd := fmt.Sprintf("head -c %d /test/zero > /out/result", size)
if _, err := d.Run(ctx, opts, "sh", "-c", cmd); err != nil {
t.Fatalf("docker run failed: %v", err)
}
got, err := os.ReadFile(filepath.Join(dir, "result"))
if err != nil {
t.Fatal(err)
}
if want := [size]byte{}; !bytes.Equal(want[:], got) {
t.Errorf("Wrong bytes, want: [all zeros], got: %v", got)
}
}
func TestBlockHostUds(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
dir, err := os.MkdirTemp(testutil.TmpDir(), "tmp-mount")
if err != nil {
t.Fatalf("MkdirTemp() failed: %v", err)
}
defer os.RemoveAll(dir)
dirFD, err := unix.Open(dir, unix.O_PATH, 0)
if err != nil {
t.Fatalf("failed to open %s: %v", dir, err)
}
defer unix.Close(dirFD)
// Use /proc/self/fd to generate path to avoid EINVAL on large path.
l, err := net.Listen("unix", filepath.Join("/proc/self/fd", strconv.Itoa(dirFD), "test.sock"))
if err != nil {
t.Fatalf("listen error: %v", err)
}
defer l.Close()
opts := dockerutil.RunOpts{
Image: "basic/integrationtest",
WorkDir: "/root",
Mounts: []mount.Mount{
{
Type: mount.TypeBind,
Source: dir,
Target: "/dir",
},
},
}
if err := d.Spawn(ctx, opts, "sleep", "infinity"); err != nil {
t.Fatalf("docker run failed: %v", err)
}
// Application should be able to walk/stat the UDS ...
if got, err := d.Exec(ctx, dockerutil.ExecOpts{}, "stat", "/dir/test.sock"); err != nil {
t.Fatalf("stat(2)-ing the UDS failed: output = %q, err = %v", got, err)
}
// ... but not connect to it.
const want = "connect: Connection refused"
if got, err := d.Exec(ctx, dockerutil.ExecOpts{}, "./host_connect", "/dir/test.sock"); err == nil || !strings.Contains(got, want) {
t.Errorf("err should be non-nil and output should contain %q, but got err = %v and output = %q", want, err, got)
}
}
func readLogs(logs string, position int) (int, error) {
if len(logs) == 0 {
return 0, fmt.Errorf("error no content was read")
}
nums := strings.Split(logs, "\n")
if position >= len(nums) {
return 0, fmt.Errorf("position %v is not within the length of content %v", position, nums)
}
if position == -1 {
// Expectation of newline at the end of last position.
position = len(nums) - 2
}
num, err := strconv.Atoi(nums[position])
if err != nil {
return 0, fmt.Errorf("error getting number from file: %v", err)
}
return num, nil
}
func checkLogs(logs string, oldPos int) error {
if len(logs) == 0 {
return fmt.Errorf("error no content was read")
}
nums := strings.Split(logs, "\n")
// Expectation of newline at the end of last position.
if oldPos >= len(nums)-2 {
return fmt.Errorf("oldPos %v is not within the length of content %v", oldPos, nums)
}
for i := oldPos + 1; i < len(nums)-1; i++ {
num, err := strconv.Atoi(nums[i])
if err != nil {
return fmt.Errorf("error getting number from file: %v", err)
}
if num != oldPos+1 {
return fmt.Errorf("error in save/resume, numbers not in order, previous: %d, next: %d", oldPos, num)
}
oldPos++
}
return nil
}
func TestUnshareUsernsWithoutSetfcap(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
if err := d.Spawn(ctx, dockerutil.RunOpts{
Image: "basic/integrationtest",
CapDrop: []string{"setfcap"},
}, "unshare", "-Ur", "ls"); err != nil {
t.Fatalf("docker run failed: %v", err)
}
if _, err := d.WaitForOutput(ctx, "Operation not permitted", 5*time.Second); err != nil {
t.Errorf("WaitForOutput failed: %v", err)
}
}
// Checkpoint the container and continue running.
func TestCheckpointResume(t *testing.T) {
if !testutil.IsCheckpointSupported() {
t.Skip("Checkpoint is not supported.")
}
dockerutil.EnsureDockerExperimentalEnabled()
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
// Start the container.
if err := d.Spawn(ctx, dockerutil.RunOpts{
Image: "basic/alpine",
}, "sh", "-c", "i=0; while true; do echo \"$i\"; i=\"$(expr \"$i\" + 1)\"; sleep .01; done"); err != nil {
t.Fatalf("docker run failed: %v", err)
}
time.Sleep(2 * time.Second)
// Get the logs before checkpointing.
logs, err := d.Logs(ctx)
if err != nil {
t.Fatalf("docker logs failed: %v", err)
}
// Get the last position of the logs printed.
pos, err := readLogs(logs, -1)
if err != nil {
t.Fatalf("readLogs failed: %v", err)
}
// Create a snapshot and continue running.
if err := d.CheckpointResume(ctx, "test"); err != nil {
t.Fatalf("docker checkpoint failed: %v", err)
}
var newLogs string
// Wait for the container to resume running and print new logs.
if err := testutil.Poll(func() error {
// Get the logs after checkpointing to check if the container resumed.
newLogs, err = d.Logs(ctx)
if err != nil {
t.Fatalf("docker logs failed: %v", err)
}
return nil
}, defaultWait); err != nil {
t.Fatalf("container read logs failed after resume: %v", err)
}
if err := checkLogs(newLogs, pos); err != nil {
t.Fatalf("checkLogs failed: %v", err)
}
if err := d.Kill(ctx); err != nil {
t.Fatalf("docker kill failed: %v", err)
}
}
func connectWithTCPServer(t *testing.T, serverIP string, startPort int, numConn int) {
const timeout = 1 * time.Minute
var conn net.Conn
var err error
for i := range numConn {
ip := serverIP + ":" + strconv.Itoa(startPort+i)
for {
conn, err = net.DialTimeout("tcp", ip, timeout)
// Retry to connect to the server if there are any errors.
// Connect can fail sometimes when the server is not ready
// and is not in listening state when the request was sent.
if err != nil {
t.Logf("Error connecting to server %s: %v, retrying...", ip, err)
continue
}
break
}
conn.Close()
}
}
func connectAndReadWrite(t *testing.T, serverIP string, port int) {
newserverIP := serverIP + ":" + strconv.Itoa(port)
newConn, err := net.DialTimeout("tcp", newserverIP, 1*time.Minute)
if err != nil {
t.Fatalf("Error connecting to server: %v", err)
}
defer newConn.Close()
readBuf := make([]byte, 32)
if _, err := newConn.Read(readBuf); err != nil {
t.Fatalf("Read failed: %v", err)
}
if _, err := newConn.Write([]byte("Hello!")); err != nil {
t.Fatalf("Write failed: %v", err)
}
}
func testCheckpointRestoreTCPConnection(t *testing.T, fName string, numConn int, restoreLoopback bool) {
if !testutil.IsCheckpointSupported() {
t.Skip("Checkpoint is not supported.")
}
dockerutil.EnsureDockerExperimentalEnabled()
if !dockerutil.IsRestoreSupported() {
t.Skip("Restore is not supported.")
}
var d *dockerutil.Container
ctx := context.Background()
d = dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
opts := dockerutil.RunOpts{
Image: "basic/integrationtest",
}
// Start the tcp server.
if err := d.Spawn(ctx, opts, fName); err != nil {
t.Fatalf("docker run failed: %v", err)
}
ip, err := d.FindIP(ctx, false)
if err != nil {
t.Fatalf("docker.FindIP failed: %v", err)
}
const port = 9000
connectWithTCPServer(t, ip.String(), port, numConn)
// Create a snapshot.
const checkpointFile = "networktest"
if err := d.Checkpoint(ctx, checkpointFile); err != nil {
t.Fatalf("docker checkpoint failed: %v", err)
}
if err := d.WaitTimeout(ctx, defaultWait); err != nil {
t.Fatalf("wait failed: %v", err)
}
d.RestoreInTest(ctx, t, checkpointFile)
var newIP net.IP
newIP, err = d.FindIP(ctx, false)
if err != nil {
t.Fatalf("docker.FindIP failed: %v", err)
}
if restoreLoopback {
connectWithTCPServer(t, newIP.String(), port, numConn)
} else {
if numConn > 1 {
connectWithTCPServer(t, newIP.String(), port, numConn)
if err := d.Kill(ctx); err != nil {
t.Fatalf("Kill failed: %v", err)
}
return
}
connectAndReadWrite(t, newIP.String(), port)
}
if err := d.Wait(ctx); err != nil {
t.Fatalf("Wait failed: %v", err)
}
}
// Test to check restore of a TCP listening connection.
func TestRestoreListenConn(t *testing.T) {
testCheckpointRestoreTCPConnection(t, "./tcp_server" /* fName */, 1 /* numConn */, false /* restoreLoopback */)
}
// Test to check restore of multiple TCP listening connections.
func TestRestoreMultipleListenConnWithNetstackSR(t *testing.T) {
testCheckpointRestoreTCPConnection(t, "./tcp_stress_server" /* fName */, 100 /* numConn */, false /* restoreLoopback */)
}
// Test to check restore of TCP established loopback connection.
func TestRestoreLoopbackConnWithNetstackSR(t *testing.T) {
testCheckpointRestoreTCPConnection(t, "./tcp_loopback" /* fName */, 1 /* numConn */, true /* restoreLoopback */)
}
// Test to check if sudo works
func TestSudo(t *testing.T) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
if err := d.Create(ctx, dockerutil.RunOpts{
Image: "basic/sudo",
}, "sleep", "infinity"); err != nil {
t.Fatalf("docker create failed: %v", err)
}
if err := d.Start(ctx); err != nil {
t.Fatalf("docker start failed: %v", err)
}
// sudo works through kernel.go/CreateProcess
if who, err := d.Exec(ctx, dockerutil.ExecOpts{User: "alice"},
"sudo", "whoami"); err != nil || strings.TrimSpace(who) != "root" {
t.Errorf("sudo without execve failed; err: %v, who: %s", err, who)
}
// sudo works through execve(2)
if who, err := d.Exec(ctx, dockerutil.ExecOpts{User: "alice"},
"/bin/sh", "-c", "sudo whoami"); err != nil || strings.TrimSpace(who) != "root" {
t.Errorf("sudo through shell failed; err: %v, who: %s", err, who)
}
// Fails when prctl(PR_SET_NO_NEW_PRIVS) is set
if output, err := d.Exec(ctx, dockerutil.ExecOpts{User: "alice"},
"setpriv", "--no-new-privs", "sudo", "pwd"); err == nil {
t.Errorf("setpriv --no-new-privs sudo worked")
} else if !strings.Contains(output, "no new privileges") {
t.Errorf("setpriv --no-new-privs sudo failed with unexpected error: %v", output)
}
// Fails when there is an unprivileged ptracer
if output, err := d.Exec(ctx, dockerutil.ExecOpts{User: "alice"},
"/bin/sh", "-c", "strace -o /dev/null sudo whoami"); err == nil ||
!strings.Contains(output, "sudo: effective uid is not 0") {
t.Errorf("sudo worked despite inadequate ptracer, err: %v, output: %s", err, output)
}
// A new process created in the sandbox via "docker run" (containerManager.StartRoot)
// should honor the no-new-privileges security option.
privlessD := dockerutil.MakeContainer(ctx, t)
defer privlessD.CleanUp(ctx)
if who, err := privlessD.Run(ctx, dockerutil.RunOpts{
User: "alice",
Image: "basic/sudo",
SecurityOpts: []string{"no-new-privileges"},
}, "sudo", "whoami"); err == nil || strings.TrimSpace(who) == "root" {
t.Errorf("\"docker run\" sudo successful despite a no-new-privileges container; err: %v, who: %s", err, who)
}
// A new process created in the sandbox via "docker exec" (containerManager.ExecuteAsync)
// should honor the no-new-privileges security option the container was created with.
privlessExecD := dockerutil.MakeContainer(ctx, t)
defer privlessExecD.CleanUp(ctx)
if err := privlessExecD.Create(ctx, dockerutil.RunOpts{
Image: "basic/sudo",
SecurityOpts: []string{"no-new-privileges"},
}, "sleep", "infinity"); err != nil {
t.Fatalf("docker create failed: %v", err)
}
if err := privlessExecD.Start(ctx); err != nil {
t.Fatalf("docker start failed: %v", err)
}
if who, err := privlessExecD.Exec(ctx, dockerutil.ExecOpts{User: "alice"},
"sudo", "whoami"); err == nil || strings.TrimSpace(who) == "root" {
t.Errorf("\"docker exec\" sudo successful despite a no-new-privileges container; err: %v, who: %s", err, who)
}
}
// Adds some basic iptables rules to verify iptables installation is working.
func TestIPTables(t *testing.T) {
if testutil.IsRunningWithHostNet() {
t.Skip("Skip iptables for hostinet.")
}
cmd := `apt-get update && apt-get -y install iptables
iptables --version
iptables -t nat -L -v -n
iptables -t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to-port 8080
iptables -t nat -A OUTPUT -p tcp --dport 443 -m owner --uid-owner 1337 -j ACCEPT
iptables -t nat -L -v -n`
ctx := context.Background()
d := dockerutil.MakeContainerWithRuntime(ctx, t, "-docker")
defer d.CleanUp(ctx)
opts := dockerutil.RunOpts{
Image: "basic/ubuntu",
CapAdd: []string{"NET_ADMIN", "NET_RAW"},
}
got, err := d.Run(ctx, opts, "bash", "-c", cmd)
if err != nil {
t.Fatalf("docker run failed: %v", err)
}
t.Logf("Output:\n%s", got)
if !strings.Contains(got, "REDIRECT") || !strings.Contains(got, "tcp dpt:80 redir ports 8080") {
t.Errorf("Output missing REDIRECT rule for port 80 to 8080: %s", got)
}
if !strings.Contains(got, "ACCEPT") || !strings.Contains(got, "tcp dpt:443 owner UID match 1337") {
t.Errorf("Output missing ACCEPT rule for port 443 and UID 1337: %s", got)
}
}