Files
visor/test/e2e/integration_test.go
T
Ayush RanjanandgVisor bot 5c781c9ca4 Allow walking on FIFO and UDS in lisafs.
The flags --host-uds and --host-fifo only control whether the application can
open/connect or create/bind these special files. Stat-ing a host FIFO or UDS
should not be blocked.

PiperOrigin-RevId: 549199286
2023-07-18 21:53:21 -07:00

1049 lines
30 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"
"io/ioutil"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"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"
)
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())
}
// 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",
Ports: []int{port},
}); 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 := testutil.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 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",
Ports: []int{port}, // See Dockerfile.
}); 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 := testutil.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 := testutil.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 exists. Sleeps at the end because
// 'docker exec' gets killed if the init process exists 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
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)
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 want := 1; got != want {
t.Errorf("MemTotal 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)
}
// Give shell a few seconds to start executing the sleep.
time.Sleep(2 * time.Second)
if _, err := p.Write(time.Second, []byte{0x03}); err != nil {
t.Fatalf("error exit: %v", err)
}
if err := d.WaitTimeout(ctx, 3*time.Second); 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 := ioutil.TempDir(testutil.TmpDir(), "tmp-mount")
if err != nil {
t.Fatalf("TempDir(): %v", err)
}
const want = "123"
if err := ioutil.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 := ioutil.TempFile(testutil.TmpDir(), "random")
if err != nil {
t.Fatal("ioutil.TempFile() 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 := ioutil.TempDir(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 := ioutil.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 := ioutil.TempDir(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)
}
}
// 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)
}
}