diff options
Diffstat (limited to 'test')
133 files changed, 6488 insertions, 1201 deletions
diff --git a/test/README.md b/test/README.md new file mode 100644 index 000000000..09c36b461 --- /dev/null +++ b/test/README.md @@ -0,0 +1,18 @@ +# Tests + +The tests defined under this path are verifying functionality beyond what unit +tests can cover, e.g. integration and end to end tests. Due to their nature, +they may need extra setup in the test machine and extra configuration to run. + +- **syscalls**: system call tests use a local runner, and do not require + additional configuration in the machine. +- **integration:** defines integration tests that uses `docker run` to test + functionality. +- **image:** basic end to end test for popular images. These require the same + setup as integration tests. +- **root:** tests that require to be run as root. +- **util:** utilities library to support the tests. + +For the above noted cases, the relevant runtime must be installed via `runsc +install` before running. This is handled automatically by the test scripts in +the `kokoro` directory. diff --git a/test/e2e/BUILD b/test/e2e/BUILD new file mode 100644 index 000000000..4fe03a220 --- /dev/null +++ b/test/e2e/BUILD @@ -0,0 +1,33 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +package(licenses = ["notice"]) + +go_test( + name = "integration_test", + size = "large", + srcs = [ + "exec_test.go", + "integration_test.go", + "regression_test.go", + ], + embed = [":integration"], + tags = [ + # Requires docker and runsc to be configured before the test runs. + "manual", + "local", + ], + visibility = ["//:sandbox"], + deps = [ + "//pkg/abi/linux", + "//pkg/bits", + "//runsc/dockerutil", + "//runsc/specutils", + "//runsc/testutil", + ], +) + +go_library( + name = "integration", + srcs = ["integration.go"], + importpath = "gvisor.dev/gvisor/test/integration", +) diff --git a/test/e2e/exec_test.go b/test/e2e/exec_test.go new file mode 100644 index 000000000..88d26e865 --- /dev/null +++ b/test/e2e/exec_test.go @@ -0,0 +1,256 @@ +// 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. These +// tests require docker and runsc to be installed on the machine. +// +// 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. + +package integration + +import ( + "fmt" + "strconv" + "strings" + "syscall" + "testing" + "time" + + "gvisor.dev/gvisor/pkg/abi/linux" + "gvisor.dev/gvisor/pkg/bits" + "gvisor.dev/gvisor/runsc/dockerutil" + "gvisor.dev/gvisor/runsc/specutils" +) + +// Test that exec uses the exact same capability set as the container. +func TestExecCapabilities(t *testing.T) { + if err := dockerutil.Pull("alpine"); err != nil { + t.Fatalf("docker pull failed: %v", err) + } + d := dockerutil.MakeDocker("exec-capabilities-test") + + // Start the container. + if err := d.Run("alpine", "sh", "-c", "cat /proc/self/status; sleep 100"); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + matches, err := d.WaitForOutputSubmatch("CapEff:\t([0-9a-f]+)\n", 5*time.Second) + if err != nil { + t.Fatalf("WaitForOutputSubmatch() timeout: %v", err) + } + if len(matches) != 2 { + t.Fatalf("There should be a match for the whole line and the capability bitmask") + } + want := fmt.Sprintf("CapEff:\t%s\n", matches[1]) + t.Log("Root capabilities:", want) + + // Now check that exec'd process capabilities match the root. + got, err := d.Exec("grep", "CapEff:", "/proc/self/status") + if err != nil { + t.Fatalf("docker exec failed: %v", err) + } + t.Logf("CapEff: %v", got) + if got != want { + t.Errorf("wrong capabilities, got: %q, want: %q", got, want) + } +} + +// Test that 'exec --privileged' adds all capabilities, except for CAP_NET_RAW +// which is removed from the container when --net-raw=false. +func TestExecPrivileged(t *testing.T) { + if err := dockerutil.Pull("alpine"); err != nil { + t.Fatalf("docker pull failed: %v", err) + } + d := dockerutil.MakeDocker("exec-privileged-test") + + // Start the container with all capabilities dropped. + if err := d.Run("--cap-drop=all", "alpine", "sh", "-c", "cat /proc/self/status; sleep 100"); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + // Check that all capabilities where dropped from container. + matches, err := d.WaitForOutputSubmatch("CapEff:\t([0-9a-f]+)\n", 5*time.Second) + if err != nil { + t.Fatalf("WaitForOutputSubmatch() timeout: %v", err) + } + if len(matches) != 2 { + t.Fatalf("There should be a match for the whole line and the capability bitmask") + } + containerCaps, err := strconv.ParseUint(matches[1], 16, 64) + if err != nil { + t.Fatalf("failed to convert capabilities %q: %v", matches[1], err) + } + t.Logf("Container capabilities: %#x", containerCaps) + if containerCaps != 0 { + t.Fatalf("Container should have no capabilities: %x", containerCaps) + } + + // Check that 'exec --privileged' adds all capabilities, except + // for CAP_NET_RAW. + got, err := d.ExecWithFlags([]string{"--privileged"}, "grep", "CapEff:", "/proc/self/status") + if err != nil { + t.Fatalf("docker exec failed: %v", err) + } + t.Logf("Exec CapEff: %v", got) + want := fmt.Sprintf("CapEff:\t%016x\n", specutils.AllCapabilitiesUint64()&^bits.MaskOf64(int(linux.CAP_NET_RAW))) + if got != want { + t.Errorf("wrong capabilities, got: %q, want: %q", got, want) + } +} + +func TestExecJobControl(t *testing.T) { + if err := dockerutil.Pull("alpine"); err != nil { + t.Fatalf("docker pull failed: %v", err) + } + d := dockerutil.MakeDocker("exec-job-control-test") + + // Start the container. + if err := d.Run("alpine", "sleep", "1000"); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + // Exec 'sh' with an attached pty. + cmd, ptmx, err := d.ExecWithTerminal("sh") + if err != nil { + t.Fatalf("docker exec failed: %v", err) + } + defer ptmx.Close() + + // Call "sleep 100 | cat" in the shell. We pipe to cat so that there + // will be two processes in the foreground process group. + if _, err := ptmx.Write([]byte("sleep 100 | cat\n")); err != nil { + t.Fatalf("error writing to pty: %v", err) + } + + // Give shell a few seconds to start executing the sleep. + time.Sleep(2 * time.Second) + + // Send a ^C to the pty, which should kill sleep and cat, but not the + // shell. \x03 is ASCII "end of text", which is the same as ^C. + if _, err := ptmx.Write([]byte{'\x03'}); err != nil { + t.Fatalf("error writing to pty: %v", err) + } + + // The shell should still be alive at this point. Sleep should have + // exited with code 2+128=130. We'll exit with 10 plus that number, so + // that we can be sure that the shell did not get signalled. + if _, err := ptmx.Write([]byte("exit $(expr $? + 10)\n")); err != nil { + t.Fatalf("error writing to pty: %v", err) + } + + // Exec process should exit with code 10+130=140. + ps, err := cmd.Process.Wait() + if err != nil { + t.Fatalf("error waiting for exec process: %v", err) + } + ws := ps.Sys().(syscall.WaitStatus) + if !ws.Exited() { + t.Errorf("ws.Exited got false, want true") + } + if got, want := ws.ExitStatus(), 140; got != want { + t.Errorf("ws.ExitedStatus got %d, want %d", got, want) + } +} + +// Test that failure to exec returns proper error message. +func TestExecError(t *testing.T) { + if err := dockerutil.Pull("alpine"); err != nil { + t.Fatalf("docker pull failed: %v", err) + } + d := dockerutil.MakeDocker("exec-error-test") + + // Start the container. + if err := d.Run("alpine", "sleep", "1000"); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + _, err := d.Exec("no_can_find") + if err == nil { + t.Fatalf("docker exec didn't fail") + } + if want := `error finding executable "no_can_find" in PATH`; !strings.Contains(err.Error(), want) { + t.Fatalf("docker exec wrong error, got: %s, want: .*%s.*", err.Error(), want) + } +} + +// Test that exec inherits environment from run. +func TestExecEnv(t *testing.T) { + if err := dockerutil.Pull("alpine"); err != nil { + t.Fatalf("docker pull failed: %v", err) + } + d := dockerutil.MakeDocker("exec-env-test") + + // Start the container with env FOO=BAR. + if err := d.Run("-e", "FOO=BAR", "alpine", "sleep", "1000"); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + // Exec "echo $FOO". + got, err := d.Exec("/bin/sh", "-c", "echo $FOO") + if err != nil { + t.Fatalf("docker exec failed: %v", err) + } + if want := "BAR"; !strings.Contains(got, want) { + t.Errorf("wanted exec output to contain %q, got %q", want, got) + } +} + +// Test that exec always has HOME environment set, even when not set in run. +func TestExecEnvHasHome(t *testing.T) { + // Base alpine image does not have any environment variables set. + if err := dockerutil.Pull("alpine"); err != nil { + t.Fatalf("docker pull failed: %v", err) + } + d := dockerutil.MakeDocker("exec-env-home-test") + + // We will check that HOME is set for root user, and also for a new + // non-root user we will create. + newUID := 1234 + newHome := "/foo/bar" + + // Create a new user with a home directory, and then sleep. + script := fmt.Sprintf(` + mkdir -p -m 777 %s && \ + adduser foo -D -u %d -h %s && \ + sleep 1000`, newHome, newUID, newHome) + if err := d.Run("alpine", "/bin/sh", "-c", script); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + // Exec "echo $HOME", and expect to see "/root". + got, err := d.Exec("/bin/sh", "-c", "echo $HOME") + if err != nil { + t.Fatalf("docker exec failed: %v", err) + } + if want := "/root"; !strings.Contains(got, want) { + t.Errorf("wanted exec output to contain %q, got %q", want, got) + } + + // Execute the same as uid 123 and expect newHome. + got, err = d.ExecAsUser(strconv.Itoa(newUID), "/bin/sh", "-c", "echo $HOME") + if err != nil { + t.Fatalf("docker exec failed: %v", err) + } + if want := newHome; !strings.Contains(got, want) { + t.Errorf("wanted exec output to contain %q, got %q", want, got) + } +} diff --git a/test/e2e/integration.go b/test/e2e/integration.go new file mode 100644 index 000000000..4cd5f6c24 --- /dev/null +++ b/test/e2e/integration.go @@ -0,0 +1,16 @@ +// 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 is empty. See integration_test.go for description. +package integration diff --git a/test/e2e/integration_test.go b/test/e2e/integration_test.go new file mode 100644 index 000000000..7cc0de129 --- /dev/null +++ b/test/e2e/integration_test.go @@ -0,0 +1,348 @@ +// 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 ( + "flag" + "fmt" + "net" + "net/http" + "os" + "strconv" + "strings" + "syscall" + "testing" + "time" + + "gvisor.dev/gvisor/runsc/dockerutil" + "gvisor.dev/gvisor/runsc/testutil" +) + +// 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) { + if err := dockerutil.Pull("nginx"); err != nil { + t.Fatal("docker pull failed:", err) + } + d := dockerutil.MakeDocker("lifecycle-test") + if err := d.Create("-p", "80", "nginx"); err != nil { + t.Fatal("docker create failed:", err) + } + if err := d.Start(); err != nil { + d.CleanUp() + t.Fatal("docker start failed:", err) + } + + // Test that container is working + port, err := d.FindPort(80) + if err != nil { + t.Fatal("docker.FindPort(80) failed: ", err) + } + if err := testutil.WaitForHTTP(port, 30*time.Second); err != nil { + t.Fatal("WaitForHTTP() timeout:", err) + } + client := http.Client{Timeout: time.Duration(2 * time.Second)} + if err := httpRequestSucceeds(client, "localhost", port); err != nil { + t.Error("http request failed:", err) + } + + if err := d.Stop(); err != nil { + d.CleanUp() + t.Fatal("docker stop failed:", err) + } + if err := d.Remove(); err != nil { + t.Fatal("docker rm failed:", err) + } +} + +func TestPauseResume(t *testing.T) { + const img = "gcr.io/gvisor-presubmit/python-hello" + if !testutil.IsCheckpointSupported() { + t.Log("Checkpoint is not supported, skipping test.") + return + } + + if err := dockerutil.Pull(img); err != nil { + t.Fatal("docker pull failed:", err) + } + d := dockerutil.MakeDocker("pause-resume-test") + if err := d.Run("-p", "8080", img); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + // Find where port 8080 is mapped to. + port, err := d.FindPort(8080) + if err != nil { + t.Fatal("docker.FindPort(8080) failed:", err) + } + + // Wait until it's up and running. + if err := testutil.WaitForHTTP(port, 30*time.Second); err != nil { + t.Fatal("WaitForHTTP() timeout:", err) + } + + // Check that container is working. + client := http.Client{Timeout: time.Duration(2 * time.Second)} + if err := httpRequestSucceeds(client, "localhost", port); err != nil { + t.Error("http request failed:", err) + } + + if err := d.Pause(); err != nil { + t.Fatal("docker pause failed:", err) + } + + // Check if container is paused. + switch _, err := client.Get(fmt.Sprintf("http://localhost:%d", 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(); err != nil { + t.Fatal("docker unpause failed:", err) + } + + // Wait until it's up and running. + if err := testutil.WaitForHTTP(port, 30*time.Second); err != nil { + t.Fatal("WaitForHTTP() timeout:", err) + } + + // Check if container is working again. + if err := httpRequestSucceeds(client, "localhost", port); err != nil { + t.Error("http request failed:", err) + } +} + +func TestCheckpointRestore(t *testing.T) { + const img = "gcr.io/gvisor-presubmit/python-hello" + if !testutil.IsCheckpointSupported() { + t.Log("Pause/resume is not supported, skipping test.") + return + } + + if err := dockerutil.Pull(img); err != nil { + t.Fatal("docker pull failed:", err) + } + d := dockerutil.MakeDocker("save-restore-test") + if err := d.Run("-p", "8080", img); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + if err := d.Checkpoint("test"); err != nil { + t.Fatal("docker checkpoint failed:", err) + } + + if _, err := d.Wait(30 * time.Second); err != nil { + t.Fatal(err) + } + + if err := d.Restore("test"); err != nil { + t.Fatal("docker restore failed:", err) + } + + // Find where port 8080 is mapped to. + port, err := d.FindPort(8080) + if err != nil { + t.Fatal("docker.FindPort(8080) failed:", err) + } + + // Wait until it's up and running. + if err := testutil.WaitForHTTP(port, 30*time.Second); err != nil { + t.Fatal("WaitForHTTP() timeout:", err) + } + + // Check if container is working again. + client := http.Client{Timeout: time.Duration(2 * time.Second)} + if err := httpRequestSucceeds(client, "localhost", 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) { + d := dockerutil.MakeDocker("connect-to-self-test") + + // 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.Run("ubuntu:trusty", "/bin/sh", "-c", "echo server | nc -l -p 8080 && sleep 1"); err != nil { + t.Fatal("docker run failed:", err) + } + defer d.CleanUp() + + // Finds IP address for host. + ip, err := d.Exec("/bin/sh", "-c", "cat /etc/hosts | grep ${HOSTNAME} | awk '{print $1}'") + if err != nil { + t.Fatal("docker exec failed:", err) + } + ip = strings.TrimRight(ip, "\n") + + // Runs client that sends "client" to the server and exits. + reply, err := d.Exec("/bin/sh", "-c", fmt.Sprintf("echo client | nc %s 8080", ip)) + if err != nil { + t.Fatal("docker exec failed:", 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("^client\n$", 1*time.Second); err != nil { + t.Fatal("docker.WaitForOutput(client) timeout:", err) + } +} + +func TestMemLimit(t *testing.T) { + if err := dockerutil.Pull("alpine"); err != nil { + t.Fatal("docker pull failed:", err) + } + d := dockerutil.MakeDocker("cgroup-test") + cmd := "cat /proc/meminfo | grep MemTotal: | awk '{print $2}'" + out, err := d.RunFg("--memory=500MB", "alpine", "sh", "-c", cmd) + if err != nil { + t.Fatal("docker run failed:", err) + } + defer d.CleanUp() + + // 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] + } + + got, err := strconv.ParseUint(strings.TrimSpace(out), 10, 64) + if err != nil { + t.Fatalf("failed to parse %q: %v", out, err) + } + if want := uint64(500 * 1024); got != want { + t.Errorf("MemTotal got: %d, want: %d", got, want) + } +} + +func TestNumCPU(t *testing.T) { + if err := dockerutil.Pull("alpine"); err != nil { + t.Fatal("docker pull failed:", err) + } + d := dockerutil.MakeDocker("cgroup-test") + cmd := "cat /proc/cpuinfo | grep 'processor.*:' | wc -l" + out, err := d.RunFg("--cpuset-cpus=0", "alpine", "sh", "-c", cmd) + if err != nil { + t.Fatal("docker run failed:", err) + } + defer d.CleanUp() + + 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) { + if err := dockerutil.Pull("alpine"); err != nil { + t.Fatalf("docker pull failed: %v", err) + } + d := dockerutil.MakeDocker("job-control-test") + + // Start the container with an attached PTY. + _, ptmx, err := d.RunWithPty("alpine", "sh") + if err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer ptmx.Close() + defer d.CleanUp() + + // Call "sleep 100" in the shell. + if _, err := ptmx.Write([]byte("sleep 100\n")); err != nil { + t.Fatalf("error writing to pty: %v", err) + } + + // Give shell a few seconds to start executing the sleep. + time.Sleep(2 * time.Second) + + // Send a ^C to the pty, which should kill sleep, but not the shell. + // \x03 is ASCII "end of text", which is the same as ^C. + if _, err := ptmx.Write([]byte{'\x03'}); err != nil { + t.Fatalf("error writing to pty: %v", err) + } + + // The shell should still be alive at this point. Sleep should have + // exited with code 2+128=130. We'll exit with 10 plus that number, so + // that we can be sure that the shell did not get signalled. + if _, err := ptmx.Write([]byte("exit $(expr $? + 10)\n")); err != nil { + t.Fatalf("error writing to pty: %v", err) + } + + // Wait for the container to exit. + got, err := d.Wait(5 * time.Second) + if err != nil { + t.Fatalf("error getting exit code: %v", err) + } + // Container should exit with code 10+130=140. + if want := syscall.WaitStatus(140); got != want { + t.Errorf("container exited with code %d want %d", got, want) + } +} + +// TestTmpFile checks that files inside '/tmp' are not overridden. In addition, +// it checks that working dir is created if it doesn't exit. +func TestTmpFile(t *testing.T) { + if err := dockerutil.Pull("alpine"); err != nil { + t.Fatal("docker pull failed:", err) + } + d := dockerutil.MakeDocker("tmp-file-test") + if err := d.Run("-w=/tmp/foo/bar", "--read-only", "alpine", "touch", "/tmp/foo/bar/file"); err != nil { + t.Fatal("docker run failed:", err) + } + defer d.CleanUp() +} + +func TestMain(m *testing.M) { + dockerutil.EnsureSupportedDockerVersion() + flag.Parse() + os.Exit(m.Run()) +} diff --git a/test/e2e/regression_test.go b/test/e2e/regression_test.go new file mode 100644 index 000000000..2488be383 --- /dev/null +++ b/test/e2e/regression_test.go @@ -0,0 +1,45 @@ +// Copyright 2019 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 + +import ( + "strings" + "testing" + + "gvisor.dev/gvisor/runsc/dockerutil" +) + +// 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) { + if err := dockerutil.Pull("ubuntu:trusty"); err != nil { + t.Fatal("docker pull failed:", err) + } + d := dockerutil.MakeDocker("bind-overlay-test") + + cmd := "nc -l -U /var/run/sock & p=$! && sleep 1 && echo foobar-asdf | nc -U /var/run/sock && wait $p" + got, err := d.RunFg("ubuntu:trusty", "bash", "-c", cmd) + if err != nil { + t.Fatal("docker run failed:", err) + } + + if want := "foobar-asdf"; !strings.Contains(got, want) { + t.Fatalf("docker run output is missing %q: %s", want, got) + } + defer d.CleanUp() +} diff --git a/test/image/BUILD b/test/image/BUILD new file mode 100644 index 000000000..09b0a0ad5 --- /dev/null +++ b/test/image/BUILD @@ -0,0 +1,34 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +package(licenses = ["notice"]) + +go_test( + name = "image_test", + size = "large", + srcs = [ + "image_test.go", + ], + data = [ + "latin10k.txt", + "mysql.sql", + "ruby.rb", + "ruby.sh", + ], + embed = [":image"], + tags = [ + # Requires docker and runsc to be configured before the test runs. + "manual", + "local", + ], + visibility = ["//:sandbox"], + deps = [ + "//runsc/dockerutil", + "//runsc/testutil", + ], +) + +go_library( + name = "image", + srcs = ["image.go"], + importpath = "gvisor.dev/gvisor/test/image", +) diff --git a/test/image/image.go b/test/image/image.go new file mode 100644 index 000000000..297f1ab92 --- /dev/null +++ b/test/image/image.go @@ -0,0 +1,16 @@ +// 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 image is empty. See image_test.go for description. +package image diff --git a/test/image/image_test.go b/test/image/image_test.go new file mode 100644 index 000000000..d0dcb1861 --- /dev/null +++ b/test/image/image_test.go @@ -0,0 +1,353 @@ +// 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 image provides end-to-end image tests for runsc. + +// Each test calls docker commands to start up a container, and tests that it +// is behaving properly, like connecting to a port or looking at the output. +// The container is killed and deleted at the end. +// +// Setup instruction in test/README.md. +package image + +import ( + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "gvisor.dev/gvisor/runsc/dockerutil" + "gvisor.dev/gvisor/runsc/testutil" +) + +func TestHelloWorld(t *testing.T) { + d := dockerutil.MakeDocker("hello-test") + if err := d.Run("hello-world"); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + if _, err := d.WaitForOutput("Hello from Docker!", 5*time.Second); err != nil { + t.Fatalf("docker didn't say hello: %v", err) + } +} + +func runHTTPRequest(port int) error { + url := fmt.Sprintf("http://localhost:%d/not-found", port) + resp, err := http.Get(url) + if err != nil { + return fmt.Errorf("error reaching http server: %v", err) + } + if want := http.StatusNotFound; resp.StatusCode != want { + return fmt.Errorf("Wrong response code, got: %d, want: %d", resp.StatusCode, want) + } + + url = fmt.Sprintf("http://localhost:%d/latin10k.txt", port) + resp, err = http.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) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("Error reading http response: %v", err) + } + defer resp.Body.Close() + + // READALL is the last word in the file. Ensures everything was read. + if want := "READALL"; strings.HasSuffix(string(body), want) { + return fmt.Errorf("response doesn't contain %q, resp: %q", want, body) + } + return nil +} + +func testHTTPServer(t *testing.T, port int) { + const requests = 10 + ch := make(chan error, requests) + for i := 0; i < requests; i++ { + go func() { + start := time.Now() + err := runHTTPRequest(port) + log.Printf("Response time %v: %v", time.Since(start).String(), err) + ch <- err + }() + } + + for i := 0; i < requests; i++ { + err := <-ch + if err != nil { + t.Errorf("testHTTPServer(%d) failed: %v", port, err) + } + } +} + +func TestHttpd(t *testing.T) { + if err := dockerutil.Pull("httpd"); err != nil { + t.Fatalf("docker pull failed: %v", err) + } + d := dockerutil.MakeDocker("http-test") + + dir, err := dockerutil.PrepareFiles("latin10k.txt") + if err != nil { + t.Fatalf("PrepareFiles() failed: %v", err) + } + + // Start the container. + mountArg := dockerutil.MountArg(dir, "/usr/local/apache2/htdocs", dockerutil.ReadOnly) + if err := d.Run("-p", "80", mountArg, "httpd"); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + // Find where port 80 is mapped to. + port, err := d.FindPort(80) + if err != nil { + t.Fatalf("docker.FindPort(80) failed: %v", err) + } + + // Wait until it's up and running. + if err := testutil.WaitForHTTP(port, 30*time.Second); err != nil { + t.Errorf("WaitForHTTP() timeout: %v", err) + } + + testHTTPServer(t, port) +} + +func TestNginx(t *testing.T) { + if err := dockerutil.Pull("nginx"); err != nil { + t.Fatalf("docker pull failed: %v", err) + } + d := dockerutil.MakeDocker("net-test") + + dir, err := dockerutil.PrepareFiles("latin10k.txt") + if err != nil { + t.Fatalf("PrepareFiles() failed: %v", err) + } + + // Start the container. + mountArg := dockerutil.MountArg(dir, "/usr/share/nginx/html", dockerutil.ReadOnly) + if err := d.Run("-p", "80", mountArg, "nginx"); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + // Find where port 80 is mapped to. + port, err := d.FindPort(80) + if err != nil { + t.Fatalf("docker.FindPort(80) failed: %v", err) + } + + // Wait until it's up and running. + if err := testutil.WaitForHTTP(port, 30*time.Second); err != nil { + t.Errorf("WaitForHTTP() timeout: %v", err) + } + + testHTTPServer(t, port) +} + +func TestMysql(t *testing.T) { + if err := dockerutil.Pull("mysql"); err != nil { + t.Fatalf("docker pull failed: %v", err) + } + d := dockerutil.MakeDocker("mysql-test") + + // Start the container. + if err := d.Run("-e", "MYSQL_ROOT_PASSWORD=foobar123", "mysql"); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + // Wait until it's up and running. + if _, err := d.WaitForOutput("port: 3306 MySQL Community Server", 3*time.Minute); err != nil { + t.Fatalf("docker.WaitForOutput() timeout: %v", err) + } + + client := dockerutil.MakeDocker("mysql-client-test") + dir, err := dockerutil.PrepareFiles("mysql.sql") + if err != nil { + t.Fatalf("PrepareFiles() failed: %v", err) + } + + // Tell mysql client to connect to the server and execute the file in verbose + // mode to verify the output. + args := []string{ + dockerutil.LinkArg(&d, "mysql"), + dockerutil.MountArg(dir, "/sql", dockerutil.ReadWrite), + "mysql", + "mysql", "-hmysql", "-uroot", "-pfoobar123", "-v", "-e", "source /sql/mysql.sql", + } + if err := client.Run(args...); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer client.CleanUp() + + // Ensure file executed to the end and shutdown mysql. + if _, err := client.WaitForOutput("--------------\nshutdown\n--------------", 15*time.Second); err != nil { + t.Fatalf("docker.WaitForOutput() timeout: %v", err) + } + if _, err := d.WaitForOutput("mysqld: Shutdown complete", 30*time.Second); err != nil { + t.Fatalf("docker.WaitForOutput() timeout: %v", err) + } +} + +func TestPythonHello(t *testing.T) { + // TODO(b/136503277): Once we have more complete python runtime tests, + // we can drop this one. + const img = "gcr.io/gvisor-presubmit/python-hello" + if err := dockerutil.Pull(img); err != nil { + t.Fatalf("docker pull failed: %v", err) + } + d := dockerutil.MakeDocker("python-hello-test") + if err := d.Run("-p", "8080", img); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + // Find where port 8080 is mapped to. + port, err := d.FindPort(8080) + if err != nil { + t.Fatalf("docker.FindPort(8080) failed: %v", err) + } + + // Wait until it's up and running. + if err := testutil.WaitForHTTP(port, 30*time.Second); err != nil { + t.Fatalf("WaitForHTTP() timeout: %v", err) + } + + // Ensure that content is being served. + url := fmt.Sprintf("http://localhost:%d", port) + resp, err := http.Get(url) + if err != nil { + t.Errorf("Error reaching http server: %v", err) + } + if want := http.StatusOK; resp.StatusCode != want { + t.Errorf("Wrong response code, got: %d, want: %d", resp.StatusCode, want) + } +} + +func TestTomcat(t *testing.T) { + if err := dockerutil.Pull("tomcat:8.0"); err != nil { + t.Fatalf("docker pull failed: %v", err) + } + d := dockerutil.MakeDocker("tomcat-test") + if err := d.Run("-p", "8080", "tomcat:8.0"); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + // Find where port 8080 is mapped to. + port, err := d.FindPort(8080) + if err != nil { + t.Fatalf("docker.FindPort(8080) failed: %v", err) + } + + // Wait until it's up and running. + if err := testutil.WaitForHTTP(port, 30*time.Second); err != nil { + t.Fatalf("WaitForHTTP() timeout: %v", err) + } + + // Ensure that content is being served. + url := fmt.Sprintf("http://localhost:%d", port) + resp, err := http.Get(url) + if err != nil { + t.Errorf("Error reaching http server: %v", err) + } + if want := http.StatusOK; resp.StatusCode != want { + t.Errorf("Wrong response code, got: %d, want: %d", resp.StatusCode, want) + } +} + +func TestRuby(t *testing.T) { + if err := dockerutil.Pull("ruby"); err != nil { + t.Fatalf("docker pull failed: %v", err) + } + d := dockerutil.MakeDocker("ruby-test") + + dir, err := dockerutil.PrepareFiles("ruby.rb", "ruby.sh") + if err != nil { + t.Fatalf("PrepareFiles() failed: %v", err) + } + if err := os.Chmod(filepath.Join(dir, "ruby.sh"), 0333); err != nil { + t.Fatalf("os.Chmod(%q, 0333) failed: %v", dir, err) + } + + if err := d.Run("-p", "8080", dockerutil.MountArg(dir, "/src", dockerutil.ReadOnly), "ruby", "/src/ruby.sh"); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + // Find where port 8080 is mapped to. + port, err := d.FindPort(8080) + if err != nil { + t.Fatalf("docker.FindPort(8080) failed: %v", err) + } + + // Wait until it's up and running, 'gem install' can take some time. + if err := testutil.WaitForHTTP(port, 1*time.Minute); err != nil { + t.Fatalf("WaitForHTTP() timeout: %v", err) + } + + // Ensure that content is being served. + url := fmt.Sprintf("http://localhost:%d", port) + resp, err := http.Get(url) + if err != nil { + t.Errorf("error reaching http server: %v", err) + } + if want := http.StatusOK; resp.StatusCode != want { + t.Errorf("wrong response code, got: %d, want: %d", resp.StatusCode, want) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("error reading body: %v", err) + } + if got, want := string(body), "Hello World"; !strings.Contains(got, want) { + t.Errorf("invalid body content, got: %q, want: %q", got, want) + } +} + +func TestStdio(t *testing.T) { + if err := dockerutil.Pull("alpine"); err != nil { + t.Fatalf("docker pull failed: %v", err) + } + d := dockerutil.MakeDocker("stdio-test") + + wantStdout := "hello stdout" + wantStderr := "bonjour stderr" + cmd := fmt.Sprintf("echo %q; echo %q 1>&2;", wantStdout, wantStderr) + if err := d.Run("alpine", "/bin/sh", "-c", cmd); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + for _, want := range []string{wantStdout, wantStderr} { + if _, err := d.WaitForOutput(want, 5*time.Second); err != nil { + t.Fatalf("docker didn't get output %q : %v", want, err) + } + } +} + +func TestMain(m *testing.M) { + dockerutil.EnsureSupportedDockerVersion() + flag.Parse() + os.Exit(m.Run()) +} diff --git a/test/image/latin10k.txt b/test/image/latin10k.txt new file mode 100644 index 000000000..61341e00b --- /dev/null +++ b/test/image/latin10k.txt @@ -0,0 +1,33 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras ut placerat felis. Maecenas urna est, auctor a efficitur sit amet, egestas et augue. Curabitur dignissim scelerisque nunc vel cursus. Ut vehicula est pretium, consectetur nunc non, pharetra ligula. Curabitur ut ultricies metus. Suspendisse pulvinar, orci sed fermentum vestibulum, eros turpis molestie lectus, nec elementum risus dolor mattis felis. Donec ultrices ipsum sem, at pretium lacus convallis at. Mauris nulla enim, tincidunt non bibendum at, vehicula pulvinar mauris. + +Duis in dapibus turpis. Pellentesque maximus magna odio, ac congue libero laoreet quis. Maecenas euismod risus in justo aliquam accumsan. Nunc quis ornare arcu, sit amet sodales elit. Phasellus nec scelerisque nisl, a tincidunt arcu. Proin ornare est nunc, sed suscipit orci interdum et. Suspendisse condimentum venenatis diam in tempor. Aliquam egestas lectus in rutrum tempus. Donec id egestas eros. Donec molestie consequat purus, sed posuere odio venenatis vitae. Nunc placerat augue id vehicula varius. In hac habitasse platea dictumst. Proin at est accumsan, venenatis quam a, fermentum risus. Phasellus posuere pellentesque enim, id suscipit magna consequat ut. Quisque ut tortor ante. + +Cras ut vulputate metus, a laoreet lectus. Vivamus ultrices molestie odio in tristique. Morbi faucibus mi eget sollicitudin fringilla. Fusce vitae lacinia ligula. Sed egestas sed diam eu posuere. Maecenas justo nisl, venenatis vel nibh vel, cursus aliquam velit. Praesent lacinia dui id erat venenatis rhoncus. Morbi gravida felis ante, sit amet vehicula orci rhoncus vitae. + +Sed finibus sagittis dictum. Proin auctor suscipit sem et mattis. Phasellus libero ligula, pellentesque ut felis porttitor, fermentum sollicitudin orci. Nulla eu nulla nibh. Fusce a eros risus. Proin vel magna risus. Donec nec elit eleifend, scelerisque sapien vitae, pharetra quam. Donec porttitor mauris scelerisque, tempus orci hendrerit, dapibus felis. Nullam libero elit, sollicitudin a aliquam at, ultrices in erat. Mauris eget ligula sodales, porta turpis et, scelerisque odio. Mauris mollis leo vitae purus gravida, in tempor nunc efficitur. Nulla facilisis posuere augue, nec pellentesque lectus eleifend ac. Vestibulum convallis est a feugiat tincidunt. Donec vitae enim volutpat, tincidunt eros eu, malesuada nibh. + +Quisque molestie, magna ornare elementum convallis, erat enim sagittis ipsum, eget porttitor sapien arcu id purus. Donec ut cursus diam. Nulla rutrum nulla et mi fermentum, vel tempus tellus posuere. Proin vitae pharetra nulla, nec ornare ex. Nulla consequat, augue a accumsan euismod, turpis leo ornare ligula, a pulvinar enim dolor ut augue. Quisque volutpat, lectus a varius mollis, nisl eros feugiat sem, at egestas lacus justo eu elit. Vestibulum scelerisque mauris est, sagittis interdum nunc accumsan sit amet. Maecenas aliquet ex ut lacus ornare, eu sagittis nibh imperdiet. Duis ultrices nisi velit, sed sodales risus sollicitudin et. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Etiam a accumsan augue, vitae pulvinar nulla. Pellentesque euismod sodales magna, nec luctus eros mattis eget. Sed lacinia suscipit lectus, eget consectetur dui pellentesque sed. Nullam nec mattis tellus. + +Aliquam erat volutpat. Praesent lobortis massa porttitor eros tincidunt, nec consequat diam pharetra. Duis efficitur non lorem sed mattis. Suspendisse justo nunc, pulvinar eu porttitor at, facilisis id eros. Suspendisse potenti. Cras molestie aliquet orci ut fermentum. In tempus aliquet eros nec suscipit. Suspendisse in mauris ut lectus ultrices blandit sit amet vitae est. Nam magna massa, porttitor ut semper id, feugiat vel quam. Suspendisse dignissim posuere scelerisque. Donec scelerisque lorem efficitur suscipit suscipit. Nunc luctus ligula et scelerisque lacinia. + +Suspendisse potenti. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed ultrices, sem in venenatis scelerisque, tellus ipsum porttitor urna, et iaculis lectus odio ac nisi. Integer luctus dui urna, at sollicitudin elit dapibus eu. Praesent nibh ante, porttitor a ante in, ullamcorper pretium felis. Aliquam vel tortor imperdiet, imperdiet lorem et, cursus mi. Proin tempus velit est, ut hendrerit metus gravida sed. Sed nibh sapien, faucibus quis ipsum in, scelerisque lacinia elit. In nec magna eu magna laoreet rhoncus. Donec vitae rutrum mauris. Integer urna felis, consequat at rhoncus vitae, auctor quis elit. Duis a pulvinar sem, nec gravida nisl. Nam non dapibus purus. Praesent vestibulum turpis nec erat porttitor, a scelerisque purus tincidunt. + +Nam fringilla leo nisi, nec placerat nisl luctus eget. Aenean malesuada nunc porta sapien sodales convallis. Suspendisse ut massa tempor, ullamcorper mi ut, faucibus turpis. Vivamus at sagittis metus. Donec varius ac mi eget sodales. Nulla feugiat, nulla eu fringilla fringilla, nunc lorem sollicitudin quam, vitae lacinia velit lorem eu orci. Mauris leo urna, pellentesque ac posuere non, pellentesque sit amet quam. + +Vestibulum porta diam urna, a aliquet nibh vestibulum et. Proin interdum bibendum nisl sed rhoncus. Sed vel diam hendrerit, faucibus ante et, hendrerit diam. Nunc dolor augue, mattis non dolor vel, luctus sodales neque. Cras malesuada fermentum dolor eu lobortis. Integer dapibus volutpat consequat. Maecenas posuere feugiat nunc. Donec vel mollis elit, volutpat consequat enim. Nulla id nisi finibus orci imperdiet elementum. Phasellus ultrices, elit vitae consequat rutrum, nisl est congue massa, quis condimentum justo nisi vitae turpis. Maecenas aliquet risus sit amet accumsan elementum. Proin non finibus elit, sit amet lobortis augue. + +Morbi pretium pulvinar sem vel sollicitudin. Proin imperdiet fringilla leo, non pellentesque lacus gravida nec. Vivamus ullamcorper consectetur ligula eu consectetur. Curabitur sit amet tempus purus. Curabitur quam quam, tincidunt eu tempus vel, volutpat at ipsum. Maecenas lobortis elit ac justo interdum, sit amet mattis ligula mollis. Sed posuere ligula et felis convallis tempor. Aliquam nec mollis velit. Donec varius sit amet erat at imperdiet. Nulla ipsum justo, tempor non sollicitudin gravida, dignissim vel orci. In hac habitasse platea dictumst. Cras cursus tellus id arcu aliquet accumsan. Phasellus ac erat dui. + +Duis mollis metus at mi luctus aliquam. Duis varius eget erat ac porttitor. Phasellus lobortis sagittis lacinia. Etiam sagittis eget erat in pulvinar. Phasellus sodales risus nec vulputate accumsan. Cras sit amet pellentesque dui. Praesent consequat felis mi, at vulputate diam convallis a. Donec hendrerit nibh vel justo consequat dictum. In euismod, dui sit amet malesuada suscipit, mauris ex rhoncus eros, sed ornare arcu nunc eu urna. Pellentesque eget erat augue. Integer rutrum mauris sem, nec sodales nulla cursus vel. Vivamus porta, urna vel varius vulputate, nulla arcu malesuada dui, a ultrices magna ante sed nibh. + +Morbi ultricies aliquam lorem id bibendum. Donec sit amet nunc vitae massa gravida eleifend hendrerit vel libero. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nulla vestibulum tempus condimentum. Aliquam dolor ipsum, condimentum in sapien et, tempor iaculis nulla. Aenean non pharetra augue. Maecenas mattis dignissim maximus. Fusce elementum tincidunt massa sit amet lobortis. Phasellus nec pharetra dui, et malesuada ante. Nullam commodo pretium tellus. Praesent sollicitudin, enim eget imperdiet scelerisque, odio felis vulputate dolor, eget auctor neque tellus ac lorem. + +In consectetur augue et sapien feugiat varius. Nam tortor mi, consectetur ac felis non, elementum venenatis augue. Suspendisse ut tellus in est sagittis cursus. Quisque faucibus, neque sit amet semper congue, nibh augue finibus odio, vitae interdum dolor arcu eget arcu. Curabitur dictum risus massa, non tincidunt urna molestie non. Maecenas eu quam purus. Donec vulputate, dui eu accumsan blandit, mauris tortor tristique mi, sed blandit leo quam id quam. Ut venenatis sagittis malesuada. Integer non auctor orci. Duis consectetur massa felis. Fusce euismod est sit amet bibendum finibus. Vestibulum dolor ex, tempor at elit in, iaculis cursus dui. Nunc sed neque ac risus rutrum tempus sit amet at ante. In hac habitasse platea dictumst. + +Donec rutrum, velit nec viverra tincidunt, est velit viverra neque, quis auctor leo ex at lectus. Morbi eget purus nisi. Aliquam lacus dui, interdum vitae elit at, venenatis dignissim est. Duis ac mollis lorem. Vivamus a vestibulum quam. Maecenas non metus dolor. Praesent tortor nunc, tristique at nisl molestie, vulputate eleifend diam. Integer ultrices lacus odio, vel imperdiet enim accumsan id. Sed ligula tortor, interdum eu velit eget, pharetra pulvinar magna. Sed non lacus in eros tincidunt sagittis ac vel justo. Donec vitae leo sagittis, accumsan ante sit amet, accumsan odio. Ut volutpat ultricies tortor. Vestibulum tempus purus et est tristique sagittis quis vitae turpis. + +Nam iaculis neque lacus, eget euismod turpis blandit eget. In hac habitasse platea dictumst. Phasellus justo neque, scelerisque sit amet risus ut, pretium commodo nisl. Phasellus auctor sapien sed ex bibendum fermentum. Proin maximus odio a ante ornare, a feugiat lorem egestas. Etiam efficitur tortor a ante tincidunt interdum. Nullam non est ac massa congue efficitur sit amet nec eros. Nullam at ipsum vel mauris tincidunt efficitur. Duis pulvinar nisl elit, id auctor risus laoreet ac. Sed nunc mauris, tristique id leo ut, condimentum congue nunc. Sed ultricies, mauris et convallis faucibus, justo ex faucibus est, at lobortis purus justo non arcu. Integer vel facilisis elit, dapibus imperdiet mauris. + +Pellentesque non mattis turpis, eget bibendum velit. Fusce sollicitudin ante ac tincidunt rhoncus. Praesent porta scelerisque consequat. Donec eleifend faucibus sollicitudin. Quisque vitae purus eget tortor tempor ultrices. Maecenas mauris diam, semper vitae est non, imperdiet tempor magna. Duis elit lacus, auctor vestibulum enim eget, rhoncus porttitor tortor. + +Donec non rhoncus nibh. Cras dapibus justo vitae nunc accumsan, id congue erat egestas. Aenean at ante ante. Duis eleifend imperdiet dREADALL diff --git a/test/image/mysql.sql b/test/image/mysql.sql new file mode 100644 index 000000000..51554b98d --- /dev/null +++ b/test/image/mysql.sql @@ -0,0 +1,23 @@ +# 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. + +SHOW databases; +USE mysql; + +CREATE TABLE foo (id int); +INSERT INTO foo VALUES(1); +SELECT * FROM foo; +DROP TABLE foo; + +shutdown; diff --git a/test/image/ruby.rb b/test/image/ruby.rb new file mode 100644 index 000000000..aced49c6d --- /dev/null +++ b/test/image/ruby.rb @@ -0,0 +1,23 @@ +# 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. + +require 'sinatra' + +set :bind, "0.0.0.0" +set :port, 8080 + +get '/' do + 'Hello World' +end + diff --git a/test/image/ruby.sh b/test/image/ruby.sh new file mode 100644 index 000000000..ebe8d5b0e --- /dev/null +++ b/test/image/ruby.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# 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. + +set -e + +gem install sinatra +ruby /src/ruby.rb diff --git a/test/root/BUILD b/test/root/BUILD new file mode 100644 index 000000000..d5dd9bca2 --- /dev/null +++ b/test/root/BUILD @@ -0,0 +1,44 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +package(licenses = ["notice"]) + +go_library( + name = "root", + srcs = ["root.go"], + importpath = "gvisor.dev/gvisor/test/root", +) + +go_test( + name = "root_test", + size = "small", + srcs = [ + "cgroup_test.go", + "chroot_test.go", + "crictl_test.go", + "main_test.go", + "oom_score_adj_test.go", + ], + data = [ + "//runsc", + ], + embed = [":root"], + tags = [ + # Requires docker and runsc to be configured before the test runs. + # Also test only runs as root. + "manual", + "local", + ], + visibility = ["//:sandbox"], + deps = [ + "//runsc/boot", + "//runsc/cgroup", + "//runsc/container", + "//runsc/criutil", + "//runsc/dockerutil", + "//runsc/specutils", + "//runsc/testutil", + "//test/root/testdata", + "@com_github_opencontainers_runtime-spec//specs-go:go_default_library", + "@com_github_syndtr_gocapability//capability:go_default_library", + ], +) diff --git a/test/root/cgroup_test.go b/test/root/cgroup_test.go new file mode 100644 index 000000000..76f1e4f2a --- /dev/null +++ b/test/root/cgroup_test.go @@ -0,0 +1,232 @@ +// 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 root + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" + + "gvisor.dev/gvisor/runsc/cgroup" + "gvisor.dev/gvisor/runsc/dockerutil" + "gvisor.dev/gvisor/runsc/testutil" +) + +func verifyPid(pid int, path string) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + var gots []int + scanner := bufio.NewScanner(f) + for scanner.Scan() { + got, err := strconv.Atoi(scanner.Text()) + if err != nil { + return err + } + if got == pid { + return nil + } + gots = append(gots, got) + } + if scanner.Err() != nil { + return scanner.Err() + } + return fmt.Errorf("got: %s, want: %d", gots, pid) +} + +// TestCgroup sets cgroup options and checks that cgroup was properly configured. +func TestCgroup(t *testing.T) { + if err := dockerutil.Pull("alpine"); err != nil { + t.Fatal("docker pull failed:", err) + } + d := dockerutil.MakeDocker("cgroup-test") + + // This is not a comprehensive list of attributes. + // + // Note that we are specifically missing cpusets, which fail if specified. + // In any case, it's unclear if cpusets can be reliably tested here: these + // are often run on a single core virtual machine, and there is only a single + // CPU available in our current set, and every container's set. + attrs := []struct { + arg string + ctrl string + file string + want string + skipIfNotFound bool + }{ + { + arg: "--cpu-shares=1000", + ctrl: "cpu", + file: "cpu.shares", + want: "1000", + }, + { + arg: "--cpu-period=2000", + ctrl: "cpu", + file: "cpu.cfs_period_us", + want: "2000", + }, + { + arg: "--cpu-quota=3000", + ctrl: "cpu", + file: "cpu.cfs_quota_us", + want: "3000", + }, + { + arg: "--kernel-memory=100MB", + ctrl: "memory", + file: "memory.kmem.limit_in_bytes", + want: "104857600", + }, + { + arg: "--memory=1GB", + ctrl: "memory", + file: "memory.limit_in_bytes", + want: "1073741824", + }, + { + arg: "--memory-reservation=500MB", + ctrl: "memory", + file: "memory.soft_limit_in_bytes", + want: "524288000", + }, + { + arg: "--memory-swap=2GB", + ctrl: "memory", + file: "memory.memsw.limit_in_bytes", + want: "2147483648", + skipIfNotFound: true, // swap may be disabled on the machine. + }, + { + arg: "--memory-swappiness=5", + ctrl: "memory", + file: "memory.swappiness", + want: "5", + }, + { + arg: "--blkio-weight=750", + ctrl: "blkio", + file: "blkio.weight", + want: "750", + }, + } + + args := make([]string, 0, len(attrs)) + for _, attr := range attrs { + args = append(args, attr.arg) + } + + args = append(args, "alpine", "sleep", "10000") + if err := d.Run(args...); err != nil { + t.Fatal("docker create failed:", err) + } + defer d.CleanUp() + + gid, err := d.ID() + if err != nil { + t.Fatalf("Docker.ID() failed: %v", err) + } + t.Logf("cgroup ID: %s", gid) + + // Check list of attributes defined above. + for _, attr := range attrs { + path := filepath.Join("/sys/fs/cgroup", attr.ctrl, "docker", gid, attr.file) + out, err := ioutil.ReadFile(path) + if err != nil { + if os.IsNotExist(err) && attr.skipIfNotFound { + t.Logf("skipped %s/%s", attr.ctrl, attr.file) + continue + } + t.Fatalf("failed to read %q: %v", path, err) + } + if got := strings.TrimSpace(string(out)); got != attr.want { + t.Errorf("arg: %q, cgroup attribute %s/%s, got: %q, want: %q", attr.arg, attr.ctrl, attr.file, got, attr.want) + } + } + + // Check that sandbox is inside cgroup. + controllers := []string{ + "blkio", + "cpu", + "cpuset", + "memory", + "net_cls", + "net_prio", + "devices", + "freezer", + "perf_event", + "pids", + "systemd", + } + pid, err := d.SandboxPid() + if err != nil { + t.Fatalf("SandboxPid: %v", err) + } + for _, ctrl := range controllers { + path := filepath.Join("/sys/fs/cgroup", ctrl, "docker", gid, "cgroup.procs") + if err := verifyPid(pid, path); err != nil { + t.Errorf("cgroup control %q processes: %v", ctrl, err) + } + } +} + +func TestCgroupParent(t *testing.T) { + if err := dockerutil.Pull("alpine"); err != nil { + t.Fatal("docker pull failed:", err) + } + d := dockerutil.MakeDocker("cgroup-test") + + parent := testutil.RandomName("runsc") + if err := d.Run("--cgroup-parent", parent, "alpine", "sleep", "10000"); err != nil { + t.Fatal("docker create failed:", err) + } + defer d.CleanUp() + gid, err := d.ID() + if err != nil { + t.Fatalf("Docker.ID() failed: %v", err) + } + t.Logf("cgroup ID: %s", gid) + + // Check that sandbox is inside cgroup. + pid, err := d.SandboxPid() + if err != nil { + t.Fatalf("SandboxPid: %v", err) + } + + // Finds cgroup for the sandbox's parent process to check that cgroup is + // created in the right location relative to the parent. + cmd := fmt.Sprintf("grep PPid: /proc/%d/status | sed 's/PPid:\\s//'", pid) + ppid, err := exec.Command("bash", "-c", cmd).CombinedOutput() + if err != nil { + t.Fatalf("Executing %q: %v", cmd, err) + } + cgroups, err := cgroup.LoadPaths(strings.TrimSpace(string(ppid))) + if err != nil { + t.Fatalf("cgroup.LoadPath(%s): %v", ppid, err) + } + path := filepath.Join("/sys/fs/cgroup/memory", cgroups["memory"], parent, gid, "cgroup.procs") + if err := verifyPid(pid, path); err != nil { + t.Errorf("cgroup control %q processes: %v", "memory", err) + } +} diff --git a/test/root/chroot_test.go b/test/root/chroot_test.go new file mode 100644 index 000000000..be0f63d18 --- /dev/null +++ b/test/root/chroot_test.go @@ -0,0 +1,142 @@ +// 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 root is used for tests that requires sysadmin privileges run. +package root + +import ( + "fmt" + "io/ioutil" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" + + "gvisor.dev/gvisor/runsc/dockerutil" +) + +// TestChroot verifies that the sandbox is chroot'd and that mounts are cleaned +// up after the sandbox is destroyed. +func TestChroot(t *testing.T) { + d := dockerutil.MakeDocker("chroot-test") + if err := d.Run("alpine", "sleep", "10000"); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + pid, err := d.SandboxPid() + if err != nil { + t.Fatalf("Docker.SandboxPid(): %v", err) + } + + // Check that sandbox is chroot'ed. + procRoot := filepath.Join("/proc", strconv.Itoa(pid), "root") + chroot, err := filepath.EvalSymlinks(procRoot) + if err != nil { + t.Fatalf("error resolving /proc/<pid>/root symlink: %v", err) + } + if chroot != "/" { + t.Errorf("sandbox is not chroot'd, it should be inside: /, got: %q", chroot) + } + + path, err := filepath.EvalSymlinks(filepath.Join("/proc", strconv.Itoa(pid), "cwd")) + if err != nil { + t.Fatalf("error resolving /proc/<pid>/cwd symlink: %v", err) + } + if chroot != path { + t.Errorf("sandbox current dir is wrong, want: %q, got: %q", chroot, path) + } + + fi, err := ioutil.ReadDir(procRoot) + if err != nil { + t.Fatalf("error listing %q: %v", chroot, err) + } + if want, got := 1, len(fi); want != got { + t.Fatalf("chroot dir got %d entries, want %d", got, want) + } + + // chroot dir is prepared by runsc and should contains only /proc. + if fi[0].Name() != "proc" { + t.Errorf("chroot got children %v, want %v", fi[0].Name(), "proc") + } + + d.CleanUp() +} + +func TestChrootGofer(t *testing.T) { + d := dockerutil.MakeDocker("chroot-test") + if err := d.Run("alpine", "sleep", "10000"); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + // It's tricky to find gofers. Get sandbox PID first, then find parent. From + // parent get all immediate children, remove the sandbox, and everything else + // are gofers. + sandPID, err := d.SandboxPid() + if err != nil { + t.Fatalf("Docker.SandboxPid(): %v", err) + } + + // Find sandbox's parent PID. + cmd := fmt.Sprintf("grep PPid /proc/%d/status | awk '{print $2}'", sandPID) + parent, err := exec.Command("sh", "-c", cmd).CombinedOutput() + if err != nil { + t.Fatalf("failed to fetch runsc (%d) parent PID: %v, out:\n%s", sandPID, err, string(parent)) + } + parentPID, err := strconv.Atoi(strings.TrimSpace(string(parent))) + if err != nil { + t.Fatalf("failed to parse PPID %q: %v", string(parent), err) + } + + // Get all children from parent. + childrenOut, err := exec.Command("/usr/bin/pgrep", "-P", strconv.Itoa(parentPID)).CombinedOutput() + if err != nil { + t.Fatalf("failed to fetch containerd-shim children: %v", err) + } + children := strings.Split(strings.TrimSpace(string(childrenOut)), "\n") + + // This where the root directory is mapped on the host and that's where the + // gofer must have chroot'd to. + root := "/root" + + for _, child := range children { + childPID, err := strconv.Atoi(child) + if err != nil { + t.Fatalf("failed to parse child PID %q: %v", child, err) + } + if childPID == sandPID { + // Skip the sandbox, all other immediate children are gofers. + continue + } + + // Check that gofer is chroot'ed. + chroot, err := filepath.EvalSymlinks(filepath.Join("/proc", child, "root")) + if err != nil { + t.Fatalf("error resolving /proc/<pid>/root symlink: %v", err) + } + if root != chroot { + t.Errorf("gofer chroot is wrong, want: %q, got: %q", root, chroot) + } + + path, err := filepath.EvalSymlinks(filepath.Join("/proc", child, "cwd")) + if err != nil { + t.Fatalf("error resolving /proc/<pid>/cwd symlink: %v", err) + } + if root != path { + t.Errorf("gofer current dir is wrong, want: %q, got: %q", root, path) + } + } +} diff --git a/test/root/crictl_test.go b/test/root/crictl_test.go new file mode 100644 index 000000000..d597664f5 --- /dev/null +++ b/test/root/crictl_test.go @@ -0,0 +1,242 @@ +// 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 root + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "testing" + "time" + + "gvisor.dev/gvisor/runsc/criutil" + "gvisor.dev/gvisor/runsc/dockerutil" + "gvisor.dev/gvisor/runsc/specutils" + "gvisor.dev/gvisor/runsc/testutil" + "gvisor.dev/gvisor/test/root/testdata" +) + +// Tests for crictl have to be run as root (rather than in a user namespace) +// because crictl creates named network namespaces in /var/run/netns/. + +// TestCrictlSanity refers to b/112433158. +func TestCrictlSanity(t *testing.T) { + // Setup containerd and crictl. + crictl, cleanup, err := setup(t) + if err != nil { + t.Fatalf("failed to setup crictl: %v", err) + } + defer cleanup() + podID, contID, err := crictl.StartPodAndContainer("httpd", testdata.Sandbox, testdata.Httpd) + if err != nil { + t.Fatal(err) + } + + // Look for the httpd page. + if err = httpGet(crictl, podID, "index.html"); err != nil { + t.Fatalf("failed to get page: %v", err) + } + + // Stop everything. + if err := crictl.StopPodAndContainer(podID, contID); err != nil { + t.Fatal(err) + } +} + +// TestMountPaths refers to b/117635704. +func TestMountPaths(t *testing.T) { + // Setup containerd and crictl. + crictl, cleanup, err := setup(t) + if err != nil { + t.Fatalf("failed to setup crictl: %v", err) + } + defer cleanup() + podID, contID, err := crictl.StartPodAndContainer("httpd", testdata.Sandbox, testdata.HttpdMountPaths) + if err != nil { + t.Fatal(err) + } + + // Look for the directory available at /test. + if err = httpGet(crictl, podID, "test"); err != nil { + t.Fatalf("failed to get page: %v", err) + } + + // Stop everything. + if err := crictl.StopPodAndContainer(podID, contID); err != nil { + t.Fatal(err) + } +} + +// TestMountPaths refers to b/118728671. +func TestMountOverSymlinks(t *testing.T) { + // Setup containerd and crictl. + crictl, cleanup, err := setup(t) + if err != nil { + t.Fatalf("failed to setup crictl: %v", err) + } + defer cleanup() + podID, contID, err := crictl.StartPodAndContainer("k8s.gcr.io/busybox", testdata.Sandbox, testdata.MountOverSymlink) + if err != nil { + t.Fatal(err) + } + + out, err := crictl.Exec(contID, "readlink", "/etc/resolv.conf") + if err != nil { + t.Fatal(err) + } + if want := "/tmp/resolv.conf"; !strings.Contains(string(out), want) { + t.Fatalf("/etc/resolv.conf is not pointing to %q: %q", want, string(out)) + } + + etc, err := crictl.Exec(contID, "cat", "/etc/resolv.conf") + if err != nil { + t.Fatal(err) + } + tmp, err := crictl.Exec(contID, "cat", "/tmp/resolv.conf") + if err != nil { + t.Fatal(err) + } + if tmp != etc { + t.Fatalf("file content doesn't match:\n\t/etc/resolv.conf: %s\n\t/tmp/resolv.conf: %s", string(etc), string(tmp)) + } + + // Stop everything. + if err := crictl.StopPodAndContainer(podID, contID); err != nil { + t.Fatal(err) + } +} + +// setup sets up before a test. Specifically it: +// * Creates directories and a socket for containerd to utilize. +// * Runs containerd and waits for it to reach a "ready" state for testing. +// * Returns a cleanup function that should be called at the end of the test. +func setup(t *testing.T) (*criutil.Crictl, func(), error) { + var cleanups []func() + cleanupFunc := func() { + for i := len(cleanups) - 1; i >= 0; i-- { + cleanups[i]() + } + } + cleanup := specutils.MakeCleanup(cleanupFunc) + defer cleanup.Clean() + + // Create temporary containerd root and state directories, and a socket + // via which crictl and containerd communicate. + containerdRoot, err := ioutil.TempDir(testutil.TmpDir(), "containerd-root") + if err != nil { + t.Fatalf("failed to create containerd root: %v", err) + } + cleanups = append(cleanups, func() { os.RemoveAll(containerdRoot) }) + containerdState, err := ioutil.TempDir(testutil.TmpDir(), "containerd-state") + if err != nil { + t.Fatalf("failed to create containerd state: %v", err) + } + cleanups = append(cleanups, func() { os.RemoveAll(containerdState) }) + sockAddr := filepath.Join(testutil.TmpDir(), "containerd-test.sock") + + // We rewrite a configuration. This is based on the current docker + // configuration for the runtime under test. + runtime, err := dockerutil.RuntimePath() + if err != nil { + t.Fatalf("error discovering runtime path: %v", err) + } + config, err := testutil.WriteTmpFile("containerd-config", testdata.ContainerdConfig(runtime)) + if err != nil { + t.Fatalf("failed to write containerd config") + } + cleanups = append(cleanups, func() { os.RemoveAll(config) }) + + // Start containerd. + containerd := exec.Command(getContainerd(), + "--config", config, + "--log-level", "debug", + "--root", containerdRoot, + "--state", containerdState, + "--address", sockAddr) + cleanups = append(cleanups, func() { + if err := testutil.KillCommand(containerd); err != nil { + log.Printf("error killing containerd: %v", err) + } + }) + containerdStderr, err := containerd.StderrPipe() + if err != nil { + t.Fatalf("failed to get containerd stderr: %v", err) + } + containerdStdout, err := containerd.StdoutPipe() + if err != nil { + t.Fatalf("failed to get containerd stdout: %v", err) + } + if err := containerd.Start(); err != nil { + t.Fatalf("failed running containerd: %v", err) + } + + // Wait for containerd to boot. Then put all containerd output into a + // buffer to be logged at the end of the test. + testutil.WaitUntilRead(containerdStderr, "Start streaming server", nil, 10*time.Second) + stdoutBuf := &bytes.Buffer{} + stderrBuf := &bytes.Buffer{} + go func() { io.Copy(stdoutBuf, containerdStdout) }() + go func() { io.Copy(stderrBuf, containerdStderr) }() + cleanups = append(cleanups, func() { + t.Logf("containerd stdout: %s", string(stdoutBuf.Bytes())) + t.Logf("containerd stderr: %s", string(stderrBuf.Bytes())) + }) + + cleanup.Release() + return criutil.NewCrictl(20*time.Second, sockAddr), cleanupFunc, nil +} + +// httpGet GETs the contents of a file served from a pod on port 80. +func httpGet(crictl *criutil.Crictl, podID, filePath string) error { + // Get the IP of the httpd server. + ip, err := crictl.PodIP(podID) + if err != nil { + return fmt.Errorf("failed to get IP from pod %q: %v", podID, err) + } + + // GET the page. We may be waiting for the server to start, so retry + // with a timeout. + var resp *http.Response + cb := func() error { + r, err := http.Get(fmt.Sprintf("http://%s", path.Join(ip, filePath))) + resp = r + return err + } + if err := testutil.Poll(cb, 20*time.Second); err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("bad status returned: %d", resp.StatusCode) + } + return nil +} + +func getContainerd() string { + // Use the local path if it exists, otherwise, use the system one. + if _, err := os.Stat("/usr/local/bin/containerd"); err == nil { + return "/usr/local/bin/containerd" + } + return "/usr/bin/containerd" +} diff --git a/test/root/main_test.go b/test/root/main_test.go new file mode 100644 index 000000000..d74dec85f --- /dev/null +++ b/test/root/main_test.go @@ -0,0 +1,49 @@ +// 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 root + +import ( + "flag" + "fmt" + "os" + "testing" + + "github.com/syndtr/gocapability/capability" + "gvisor.dev/gvisor/runsc/dockerutil" + "gvisor.dev/gvisor/runsc/specutils" +) + +// TestMain is the main function for root tests. This function checks the +// supported docker version, required capabilities, and configures the executable +// path for runsc. +func TestMain(m *testing.M) { + flag.Parse() + + if !specutils.HasCapabilities(capability.CAP_SYS_ADMIN, capability.CAP_DAC_OVERRIDE) { + fmt.Println("Test requires sysadmin privileges to run. Try again with sudo.") + os.Exit(1) + } + + dockerutil.EnsureSupportedDockerVersion() + + // Configure exe for tests. + path, err := dockerutil.RuntimePath() + if err != nil { + panic(err.Error()) + } + specutils.ExePath = path + + os.Exit(m.Run()) +} diff --git a/test/root/oom_score_adj_test.go b/test/root/oom_score_adj_test.go new file mode 100644 index 000000000..6cd378a1b --- /dev/null +++ b/test/root/oom_score_adj_test.go @@ -0,0 +1,376 @@ +// 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 root + +import ( + "fmt" + "os" + "testing" + + specs "github.com/opencontainers/runtime-spec/specs-go" + "gvisor.dev/gvisor/runsc/boot" + "gvisor.dev/gvisor/runsc/container" + "gvisor.dev/gvisor/runsc/specutils" + "gvisor.dev/gvisor/runsc/testutil" +) + +var ( + maxOOMScoreAdj = 1000 + highOOMScoreAdj = 500 + lowOOMScoreAdj = -500 + minOOMScoreAdj = -1000 +) + +// Tests for oom_score_adj have to be run as root (rather than in a user +// namespace) because we need to adjust oom_score_adj for PIDs other than our +// own and test values below 0. + +// TestOOMScoreAdjSingle tests that oom_score_adj is set properly in a +// single container sandbox. +func TestOOMScoreAdjSingle(t *testing.T) { + ppid, err := specutils.GetParentPid(os.Getpid()) + if err != nil { + t.Fatalf("getting parent pid: %v", err) + } + parentOOMScoreAdj, err := specutils.GetOOMScoreAdj(ppid) + if err != nil { + t.Fatalf("getting parent oom_score_adj: %v", err) + } + + testCases := []struct { + Name string + + // OOMScoreAdj is the oom_score_adj set to the OCI spec. If nil then + // no value is set. + OOMScoreAdj *int + }{ + { + Name: "max", + OOMScoreAdj: &maxOOMScoreAdj, + }, + { + Name: "high", + OOMScoreAdj: &highOOMScoreAdj, + }, + { + Name: "low", + OOMScoreAdj: &lowOOMScoreAdj, + }, + { + Name: "min", + OOMScoreAdj: &minOOMScoreAdj, + }, + { + Name: "nil", + OOMScoreAdj: &parentOOMScoreAdj, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + id := testutil.UniqueContainerID() + s := testutil.NewSpecWithArgs("sleep", "1000") + s.Process.OOMScoreAdj = testCase.OOMScoreAdj + + conf := testutil.TestConfig() + containers, cleanup, err := startContainers(conf, []*specs.Spec{s}, []string{id}) + if err != nil { + t.Fatalf("error starting containers: %v", err) + } + defer cleanup() + + c := containers[0] + + // Verify the gofer's oom_score_adj + if testCase.OOMScoreAdj != nil { + goferScore, err := specutils.GetOOMScoreAdj(c.GoferPid) + if err != nil { + t.Fatalf("error reading gofer oom_score_adj: %v", err) + } + if goferScore != *testCase.OOMScoreAdj { + t.Errorf("gofer oom_score_adj got: %d, want: %d", goferScore, *testCase.OOMScoreAdj) + } + + // Verify the sandbox's oom_score_adj. + // + // The sandbox should be the same for all containers so just use + // the first one. + sandboxPid := c.Sandbox.Pid + sandboxScore, err := specutils.GetOOMScoreAdj(sandboxPid) + if err != nil { + t.Fatalf("error reading sandbox oom_score_adj: %v", err) + } + if sandboxScore != *testCase.OOMScoreAdj { + t.Errorf("sandbox oom_score_adj got: %d, want: %d", sandboxScore, *testCase.OOMScoreAdj) + } + } + }) + } +} + +// TestOOMScoreAdjMulti tests that oom_score_adj is set properly in a +// multi-container sandbox. +func TestOOMScoreAdjMulti(t *testing.T) { + ppid, err := specutils.GetParentPid(os.Getpid()) + if err != nil { + t.Fatalf("getting parent pid: %v", err) + } + parentOOMScoreAdj, err := specutils.GetOOMScoreAdj(ppid) + if err != nil { + t.Fatalf("getting parent oom_score_adj: %v", err) + } + + testCases := []struct { + Name string + + // OOMScoreAdj is the oom_score_adj set to the OCI spec. If nil then + // no value is set. One value for each container. The first value is the + // root container. + OOMScoreAdj []*int + + // Expected is the expected oom_score_adj of the sandbox. If nil, then + // this value is ignored. + Expected *int + + // Remove is a set of container indexes to remove from the sandbox. + Remove []int + + // ExpectedAfterRemove is the expected oom_score_adj of the sandbox + // after containers are removed. Ignored if nil. + ExpectedAfterRemove *int + }{ + // A single container CRI test case. This should not happen in + // practice as there should be at least one container besides the pause + // container. However, we include a test case to ensure sane behavior. + { + Name: "single", + OOMScoreAdj: []*int{&highOOMScoreAdj}, + Expected: &parentOOMScoreAdj, + }, + { + Name: "multi_no_value", + OOMScoreAdj: []*int{nil, nil, nil}, + Expected: &parentOOMScoreAdj, + }, + { + Name: "multi_non_nil_root", + OOMScoreAdj: []*int{&minOOMScoreAdj, nil, nil}, + Expected: &parentOOMScoreAdj, + }, + { + Name: "multi_value", + OOMScoreAdj: []*int{&minOOMScoreAdj, &highOOMScoreAdj, &lowOOMScoreAdj}, + // The lowest value excluding the root container is expected. + Expected: &lowOOMScoreAdj, + }, + { + Name: "multi_min_value", + OOMScoreAdj: []*int{&minOOMScoreAdj, &lowOOMScoreAdj}, + // The lowest value excluding the root container is expected. + Expected: &lowOOMScoreAdj, + }, + { + Name: "multi_max_value", + OOMScoreAdj: []*int{&minOOMScoreAdj, &maxOOMScoreAdj, &highOOMScoreAdj}, + // The lowest value excluding the root container is expected. + Expected: &highOOMScoreAdj, + }, + { + Name: "remove_adjusted", + OOMScoreAdj: []*int{&minOOMScoreAdj, &maxOOMScoreAdj, &highOOMScoreAdj}, + // The lowest value excluding the root container is expected. + Expected: &highOOMScoreAdj, + // Remove highOOMScoreAdj container. + Remove: []int{2}, + ExpectedAfterRemove: &maxOOMScoreAdj, + }, + { + // This test removes all non-root sandboxes with a specified oomScoreAdj. + Name: "remove_to_nil", + OOMScoreAdj: []*int{&minOOMScoreAdj, nil, &lowOOMScoreAdj}, + Expected: &lowOOMScoreAdj, + // Remove lowOOMScoreAdj container. + Remove: []int{2}, + // The oom_score_adj expected after remove is that of the parent process. + ExpectedAfterRemove: &parentOOMScoreAdj, + }, + { + Name: "remove_no_effect", + OOMScoreAdj: []*int{&minOOMScoreAdj, &maxOOMScoreAdj, &highOOMScoreAdj}, + // The lowest value excluding the root container is expected. + Expected: &highOOMScoreAdj, + // Remove the maxOOMScoreAdj container. + Remove: []int{1}, + ExpectedAfterRemove: &highOOMScoreAdj, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.Name, func(t *testing.T) { + var cmds [][]string + var oomScoreAdj []*int + var toRemove []string + + for _, oomScore := range testCase.OOMScoreAdj { + oomScoreAdj = append(oomScoreAdj, oomScore) + cmds = append(cmds, []string{"sleep", "100"}) + } + + specs, ids := createSpecs(cmds...) + for i, spec := range specs { + // Ensure the correct value is set, including no value. + spec.Process.OOMScoreAdj = oomScoreAdj[i] + + for _, j := range testCase.Remove { + if i == j { + toRemove = append(toRemove, ids[i]) + } + } + } + + conf := testutil.TestConfig() + containers, cleanup, err := startContainers(conf, specs, ids) + if err != nil { + t.Fatalf("error starting containers: %v", err) + } + defer cleanup() + + for i, c := range containers { + if oomScoreAdj[i] != nil { + // Verify the gofer's oom_score_adj + score, err := specutils.GetOOMScoreAdj(c.GoferPid) + if err != nil { + t.Fatalf("error reading gofer oom_score_adj: %v", err) + } + if score != *oomScoreAdj[i] { + t.Errorf("gofer oom_score_adj got: %d, want: %d", score, *oomScoreAdj[i]) + } + } + } + + // Verify the sandbox's oom_score_adj. + // + // The sandbox should be the same for all containers so just use + // the first one. + sandboxPid := containers[0].Sandbox.Pid + if testCase.Expected != nil { + score, err := specutils.GetOOMScoreAdj(sandboxPid) + if err != nil { + t.Fatalf("error reading sandbox oom_score_adj: %v", err) + } + if score != *testCase.Expected { + t.Errorf("sandbox oom_score_adj got: %d, want: %d", score, *testCase.Expected) + } + } + + if len(toRemove) == 0 { + return + } + + // Remove containers. + for _, removeID := range toRemove { + for _, c := range containers { + if c.ID == removeID { + c.Destroy() + } + } + } + + // Check the new adjusted oom_score_adj. + if testCase.ExpectedAfterRemove != nil { + scoreAfterRemove, err := specutils.GetOOMScoreAdj(sandboxPid) + if err != nil { + t.Fatalf("error reading sandbox oom_score_adj: %v", err) + } + if scoreAfterRemove != *testCase.ExpectedAfterRemove { + t.Errorf("sandbox oom_score_adj got: %d, want: %d", scoreAfterRemove, *testCase.ExpectedAfterRemove) + } + } + }) + } +} + +func createSpecs(cmds ...[]string) ([]*specs.Spec, []string) { + var specs []*specs.Spec + var ids []string + rootID := testutil.UniqueContainerID() + + for i, cmd := range cmds { + spec := testutil.NewSpecWithArgs(cmd...) + if i == 0 { + spec.Annotations = map[string]string{ + specutils.ContainerdContainerTypeAnnotation: specutils.ContainerdContainerTypeSandbox, + } + ids = append(ids, rootID) + } else { + spec.Annotations = map[string]string{ + specutils.ContainerdContainerTypeAnnotation: specutils.ContainerdContainerTypeContainer, + specutils.ContainerdSandboxIDAnnotation: rootID, + } + ids = append(ids, testutil.UniqueContainerID()) + } + specs = append(specs, spec) + } + return specs, ids +} + +func startContainers(conf *boot.Config, specs []*specs.Spec, ids []string) ([]*container.Container, func(), error) { + // Setup root dir if one hasn't been provided. + if len(conf.RootDir) == 0 { + rootDir, err := testutil.SetupRootDir() + if err != nil { + return nil, nil, fmt.Errorf("error creating root dir: %v", err) + } + conf.RootDir = rootDir + } + + var containers []*container.Container + var bundles []string + cleanup := func() { + for _, c := range containers { + c.Destroy() + } + for _, b := range bundles { + os.RemoveAll(b) + } + os.RemoveAll(conf.RootDir) + } + for i, spec := range specs { + bundleDir, err := testutil.SetupBundleDir(spec) + if err != nil { + cleanup() + return nil, nil, fmt.Errorf("error setting up container: %v", err) + } + bundles = append(bundles, bundleDir) + + args := container.Args{ + ID: ids[i], + Spec: spec, + BundleDir: bundleDir, + } + cont, err := container.New(conf, args) + if err != nil { + cleanup() + return nil, nil, fmt.Errorf("error creating container: %v", err) + } + containers = append(containers, cont) + + if err := cont.Start(conf); err != nil { + cleanup() + return nil, nil, fmt.Errorf("error starting container: %v", err) + } + } + return containers, cleanup, nil +} diff --git a/test/root/root.go b/test/root/root.go new file mode 100644 index 000000000..0f1d29faf --- /dev/null +++ b/test/root/root.go @@ -0,0 +1,21 @@ +// 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 root is used for tests that requires sysadmin privileges run. First, +// follow the setup instruction in runsc/test/README.md. You should also have +// docker, containerd, and crictl installed. To run these tests from the +// project root directory: +// +// ./scripts/root_tests.sh +package root diff --git a/test/root/testdata/BUILD b/test/root/testdata/BUILD new file mode 100644 index 000000000..14c19ef1e --- /dev/null +++ b/test/root/testdata/BUILD @@ -0,0 +1,18 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +package(licenses = ["notice"]) + +go_library( + name = "testdata", + srcs = [ + "busybox.go", + "containerd_config.go", + "httpd.go", + "httpd_mount_paths.go", + "sandbox.go", + ], + importpath = "gvisor.dev/gvisor/test/root/testdata", + visibility = [ + "//visibility:public", + ], +) diff --git a/test/root/testdata/busybox.go b/test/root/testdata/busybox.go new file mode 100644 index 000000000..e4dbd2843 --- /dev/null +++ b/test/root/testdata/busybox.go @@ -0,0 +1,32 @@ +// 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 testdata + +// MountOverSymlink is a JSON config for a container that /etc/resolv.conf is a +// symlink to /tmp/resolv.conf. +var MountOverSymlink = ` +{ + "metadata": { + "name": "busybox" + }, + "image": { + "image": "k8s.gcr.io/busybox" + }, + "command": [ + "sleep", + "1000" + ] +} +` diff --git a/test/root/testdata/containerd_config.go b/test/root/testdata/containerd_config.go new file mode 100644 index 000000000..e12f1ec88 --- /dev/null +++ b/test/root/testdata/containerd_config.go @@ -0,0 +1,39 @@ +// 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 testdata contains data required for root tests. +package testdata + +import "fmt" + +// containerdConfigTemplate is a .toml config for containerd. It contains a +// formatting verb so the runtime field can be set via fmt.Sprintf. +const containerdConfigTemplate = ` +disabled_plugins = ["restart"] +[plugins.linux] + runtime = "%s" + runtime_root = "/tmp/test-containerd/runsc" + shim = "/usr/local/bin/gvisor-containerd-shim" + shim_debug = true + +[plugins.cri.containerd.runtimes.runsc] + runtime_type = "io.containerd.runtime.v1.linux" + runtime_engine = "%s" +` + +// ContainerdConfig returns a containerd config file with the specified +// runtime. +func ContainerdConfig(runtime string) string { + return fmt.Sprintf(containerdConfigTemplate, runtime, runtime) +} diff --git a/test/root/testdata/httpd.go b/test/root/testdata/httpd.go new file mode 100644 index 000000000..45d5e33d4 --- /dev/null +++ b/test/root/testdata/httpd.go @@ -0,0 +1,32 @@ +// 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 testdata + +// Httpd is a JSON config for an httpd container. +const Httpd = ` +{ + "metadata": { + "name": "httpd" + }, + "image":{ + "image": "httpd" + }, + "mounts": [ + ], + "linux": { + }, + "log_path": "httpd.log" +} +` diff --git a/test/root/testdata/httpd_mount_paths.go b/test/root/testdata/httpd_mount_paths.go new file mode 100644 index 000000000..ac3f4446a --- /dev/null +++ b/test/root/testdata/httpd_mount_paths.go @@ -0,0 +1,53 @@ +// 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 testdata + +// HttpdMountPaths is a JSON config for an httpd container with additional +// mounts. +const HttpdMountPaths = ` +{ + "metadata": { + "name": "httpd" + }, + "image":{ + "image": "httpd" + }, + "mounts": [ + { + "container_path": "/var/run/secrets/kubernetes.io/serviceaccount", + "host_path": "/var/lib/kubelet/pods/82bae206-cdf5-11e8-b245-8cdcd43ac064/volumes/kubernetes.io~secret/default-token-2rpfx", + "readonly": true + }, + { + "container_path": "/etc/hosts", + "host_path": "/var/lib/kubelet/pods/82bae206-cdf5-11e8-b245-8cdcd43ac064/etc-hosts", + "readonly": false + }, + { + "container_path": "/dev/termination-log", + "host_path": "/var/lib/kubelet/pods/82bae206-cdf5-11e8-b245-8cdcd43ac064/containers/httpd/d1709580", + "readonly": false + }, + { + "container_path": "/usr/local/apache2/htdocs/test", + "host_path": "/var/lib/kubelet/pods/82bae206-cdf5-11e8-b245-8cdcd43ac064", + "readonly": true + } + ], + "linux": { + }, + "log_path": "httpd.log" +} +` diff --git a/test/root/testdata/sandbox.go b/test/root/testdata/sandbox.go new file mode 100644 index 000000000..0db210370 --- /dev/null +++ b/test/root/testdata/sandbox.go @@ -0,0 +1,30 @@ +// 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 testdata + +// Sandbox is a default JSON config for a sandbox. +const Sandbox = ` +{ + "metadata": { + "name": "default-sandbox", + "namespace": "default", + "attempt": 1, + "uid": "hdishd83djaidwnduwk28bcsb" + }, + "linux": { + }, + "log_directory": "/tmp" +} +` diff --git a/test/runtimes/BUILD b/test/runtimes/BUILD index e85804a83..1cde74cfc 100644 --- a/test/runtimes/BUILD +++ b/test/runtimes/BUILD @@ -1,25 +1,42 @@ # These packages are used to run language runtime tests inside gVisor sandboxes. -load("@io_bazel_rules_go//go:def.bzl", "go_library") -load("//runsc/test:build_defs.bzl", "runtime_test") +load("@io_bazel_rules_go//go:def.bzl", "go_binary") +load("//test/runtimes:build_defs.bzl", "runtime_test") package(licenses = ["notice"]) -go_library( - name = "runtimes", - srcs = ["runtimes.go"], - importpath = "gvisor.dev/gvisor/test/runtimes", +go_binary( + name = "runner", + testonly = 1, + srcs = ["runner.go"], + deps = [ + "//runsc/dockerutil", + "//runsc/testutil", + ], ) runtime_test( - name = "runtimes_test", - size = "small", - srcs = ["runtimes_test.go"], - embed = [":runtimes"], - tags = [ - # Requires docker and runsc to be configured before the test runs. - "manual", - "local", - ], - deps = ["//runsc/test/testutil"], + image = "gcr.io/gvisor-presubmit/go1.12", + lang = "go", +) + +runtime_test( + image = "gcr.io/gvisor-presubmit/java11", + lang = "java", +) + +runtime_test( + blacklist_file = "blacklist_nodejs12.4.0.csv", + image = "gcr.io/gvisor-presubmit/nodejs12.4.0", + lang = "nodejs", +) + +runtime_test( + image = "gcr.io/gvisor-presubmit/php7.3.6", + lang = "php", +) + +runtime_test( + image = "gcr.io/gvisor-presubmit/python3.7.3", + lang = "python", ) diff --git a/test/runtimes/README.md b/test/runtimes/README.md index 34d3507be..e41e78f77 100644 --- a/test/runtimes/README.md +++ b/test/runtimes/README.md @@ -16,10 +16,11 @@ The following runtimes are currently supported: 1) [Install and configure Docker](https://docs.docker.com/install/) -2) Build each Docker container from the runtimes directory: +2) Build each Docker container from the runtimes/images directory: ```bash -$ docker build -f $LANG/Dockerfile [-t $NAME] . +$ cd images +$ docker build -f Dockerfile_$LANG [-t $NAME] . ``` ### Testing: diff --git a/test/runtimes/blacklist_nodejs12.4.0.csv b/test/runtimes/blacklist_nodejs12.4.0.csv new file mode 100644 index 000000000..9135d763c --- /dev/null +++ b/test/runtimes/blacklist_nodejs12.4.0.csv @@ -0,0 +1,47 @@ +test name,bug id,comment +benchmark/test-benchmark-fs.js,, +benchmark/test-benchmark-module.js,, +benchmark/test-benchmark-napi.js,, +doctool/test-make-doc.js,b/68848110,Expected to fail. +fixtures/test-error-first-line-offset.js,, +fixtures/test-fs-readfile-error.js,, +fixtures/test-fs-stat-sync-overflow.js,, +internet/test-dgram-broadcast-multi-process.js,, +internet/test-dgram-multicast-multi-process.js,, +internet/test-dgram-multicast-set-interface-lo.js,, +parallel/test-cluster-dgram-reuse.js,b/64024294, +parallel/test-dgram-bind-fd.js,b/132447356, +parallel/test-dgram-create-socket-handle-fd.js,b/132447238, +parallel/test-dgram-createSocket-type.js,b/68847739, +parallel/test-dgram-socket-buffer-size.js,b/68847921, +parallel/test-fs-access.js,, +parallel/test-fs-write-stream-double-close.js +parallel/test-fs-write-stream-throw-type-error.js,b/110226209, +parallel/test-fs-write-stream.js,, +parallel/test-http2-respond-file-error-pipe-offset.js,, +parallel/test-os.js,, +parallel/test-process-uid-gid.js,, +pseudo-tty/test-assert-colors.js,, +pseudo-tty/test-assert-no-color.js,, +pseudo-tty/test-assert-position-indicator.js,, +pseudo-tty/test-async-wrap-getasyncid-tty.js,, +pseudo-tty/test-fatal-error.js,, +pseudo-tty/test-handle-wrap-isrefed-tty.js,, +pseudo-tty/test-readable-tty-keepalive.js,, +pseudo-tty/test-set-raw-mode-reset-process-exit.js,, +pseudo-tty/test-set-raw-mode-reset-signal.js,, +pseudo-tty/test-set-raw-mode-reset.js,, +pseudo-tty/test-stderr-stdout-handle-sigwinch.js,, +pseudo-tty/test-stdout-read.js,, +pseudo-tty/test-tty-color-support.js,, +pseudo-tty/test-tty-isatty.js,, +pseudo-tty/test-tty-stdin-call-end.js,, +pseudo-tty/test-tty-stdin-end.js,, +pseudo-tty/test-stdin-write.js,, +pseudo-tty/test-tty-stdout-end.js,, +pseudo-tty/test-tty-stdout-resize.js,, +pseudo-tty/test-tty-stream-constructors.js,, +pseudo-tty/test-tty-window-size.js,, +pseudo-tty/test-tty-wrap.js,, +pummel/test-net-pingpong.js,, +pummel/test-vm-memleak.js,, diff --git a/test/runtimes/build_defs.bzl b/test/runtimes/build_defs.bzl new file mode 100644 index 000000000..7edd12c17 --- /dev/null +++ b/test/runtimes/build_defs.bzl @@ -0,0 +1,42 @@ +"""Defines a rule for runsc test targets.""" + +# runtime_test is a macro that will create targets to run the given test target +# with different runtime options. +def runtime_test( + lang, + image, + shard_count = 50, + size = "enormous", + blacklist_file = ""): + args = [ + "--lang", + lang, + "--image", + image, + ] + data = [ + ":runner", + ] + if blacklist_file != "": + args += ["--blacklist_file", "test/runtimes/" + blacklist_file] + data += [blacklist_file] + + sh_test( + name = lang + "_test", + srcs = ["runner.sh"], + args = args, + data = data, + size = size, + shard_count = shard_count, + tags = [ + # Requires docker and runsc to be configured before the test runs. + "manual", + "local", + ], + ) + +def sh_test(**kwargs): + """Wraps the standard sh_test.""" + native.sh_test( + **kwargs + ) diff --git a/test/runtimes/common/BUILD b/test/runtimes/common/BUILD deleted file mode 100644 index 1b39606b8..000000000 --- a/test/runtimes/common/BUILD +++ /dev/null @@ -1,20 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") - -package(licenses = ["notice"]) - -go_library( - name = "common", - srcs = ["common.go"], - importpath = "gvisor.dev/gvisor/test/runtimes/common", - visibility = ["//:sandbox"], -) - -go_test( - name = "common_test", - size = "small", - srcs = ["common_test.go"], - deps = [ - ":common", - "//runsc/test/testutil", - ], -) diff --git a/test/runtimes/common/common.go b/test/runtimes/common/common.go deleted file mode 100644 index 0ff87fa8b..000000000 --- a/test/runtimes/common/common.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2019 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 common executes functions for proctor binaries. -package common - -import ( - "flag" - "fmt" - "os" - "path/filepath" - "regexp" -) - -var ( - list = flag.Bool("list", false, "list all available tests") - test = flag.String("test", "", "run a single test from the list of available tests") - version = flag.Bool("v", false, "print out the version of node that is installed") -) - -// TestRunner is an interface to be implemented in each proctor binary. -type TestRunner interface { - // ListTests returns a string slice of tests available to run. - ListTests() ([]string, error) - - // RunTest runs a single test. - RunTest(test string) error -} - -// LaunchFunc parses flags passed by a proctor binary and calls the requested behavior. -func LaunchFunc(tr TestRunner) error { - flag.Parse() - - if *list && *test != "" { - flag.PrintDefaults() - return fmt.Errorf("cannot specify 'list' and 'test' flags simultaneously") - } - if *list { - tests, err := tr.ListTests() - if err != nil { - return fmt.Errorf("failed to list tests: %v", err) - } - for _, test := range tests { - fmt.Println(test) - } - return nil - } - if *version { - fmt.Println(os.Getenv("LANG_NAME"), "version:", os.Getenv("LANG_VER"), "is installed.") - return nil - } - if *test != "" { - if err := tr.RunTest(*test); err != nil { - return fmt.Errorf("test %q failed to run: %v", *test, err) - } - return nil - } - - if err := runAllTests(tr); err != nil { - return fmt.Errorf("error running all tests: %v", err) - } - return nil -} - -// Search uses filepath.Walk to perform a search of the disk for test files -// and returns a string slice of tests. -func Search(root string, testFilter *regexp.Regexp) ([]string, error) { - var testSlice []string - - err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - name := filepath.Base(path) - - if info.IsDir() || !testFilter.MatchString(name) { - return nil - } - - relPath, err := filepath.Rel(root, path) - if err != nil { - return err - } - testSlice = append(testSlice, relPath) - return nil - }) - - if err != nil { - return nil, fmt.Errorf("walking %q: %v", root, err) - } - - return testSlice, nil -} - -func runAllTests(tr TestRunner) error { - tests, err := tr.ListTests() - if err != nil { - return fmt.Errorf("failed to list tests: %v", err) - } - for _, test := range tests { - if err := tr.RunTest(test); err != nil { - return fmt.Errorf("test %q failed to run: %v", test, err) - } - } - return nil -} diff --git a/test/runtimes/go/BUILD b/test/runtimes/go/BUILD deleted file mode 100644 index ce971ee9d..000000000 --- a/test/runtimes/go/BUILD +++ /dev/null @@ -1,9 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_binary") - -package(licenses = ["notice"]) - -go_binary( - name = "proctor-go", - srcs = ["proctor-go.go"], - deps = ["//test/runtimes/common"], -) diff --git a/test/runtimes/go/Dockerfile b/test/runtimes/go/Dockerfile deleted file mode 100644 index 1d5202b70..000000000 --- a/test/runtimes/go/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -FROM ubuntu:bionic -ENV LANG_VER=1.12.5 -ENV LANG_NAME=Go - -RUN apt-get update && apt-get install -y \ - curl \ - gcc \ - git - -WORKDIR /root - -# Download Go 1.4 to use as a bootstrap for building Go from the source. -RUN curl -o go1.4.linux-amd64.tar.gz https://dl.google.com/go/go1.4.linux-amd64.tar.gz -RUN curl -LJO https://github.com/golang/go/archive/go${LANG_VER}.tar.gz -RUN mkdir bootstr -RUN tar -C bootstr -xzf go1.4.linux-amd64.tar.gz -RUN tar -xzf go-go${LANG_VER}.tar.gz -RUN mv go-go${LANG_VER} go - -ENV GOROOT=/root/go -ENV GOROOT_BOOTSTRAP=/root/bootstr/go -ENV LANG_DIR=${GOROOT} - -WORKDIR ${LANG_DIR}/src -RUN ./make.bash -# Pre-compile the tests for faster execution -RUN ["/root/go/bin/go", "tool", "dist", "test", "-compile-only"] - -WORKDIR ${LANG_DIR} - -COPY common /root/go/src/gvisor.dev/gvisor/test/runtimes/common/common -COPY go/proctor-go.go ${LANG_DIR} - -ENTRYPOINT ["/root/go/bin/go", "run", "proctor-go.go"] diff --git a/test/runtimes/images/Dockerfile_go1.12 b/test/runtimes/images/Dockerfile_go1.12 new file mode 100644 index 000000000..ab9d6abf3 --- /dev/null +++ b/test/runtimes/images/Dockerfile_go1.12 @@ -0,0 +1,10 @@ +# Go is easy, since we already have everything we need to compile the proctor +# binary and run the tests in the golang Docker image. +FROM golang:1.12 +ADD ["proctor/", "/go/src/proctor/"] +RUN ["go", "build", "-o", "/proctor", "/go/src/proctor"] + +# Pre-compile the tests so we don't need to do so in each test run. +RUN ["go", "tool", "dist", "test", "-compile-only"] + +ENTRYPOINT ["/proctor", "--runtime=go"] diff --git a/test/runtimes/images/Dockerfile_java11 b/test/runtimes/images/Dockerfile_java11 new file mode 100644 index 000000000..9b7c3d5a3 --- /dev/null +++ b/test/runtimes/images/Dockerfile_java11 @@ -0,0 +1,30 @@ +# Compile the proctor binary. +FROM golang:1.12 AS golang +ADD ["proctor/", "/go/src/proctor/"] +RUN ["go", "build", "-o", "/proctor", "/go/src/proctor"] + +FROM ubuntu:bionic +RUN apt-get update && apt-get install -y \ + autoconf \ + build-essential \ + curl \ + make \ + openjdk-11-jdk \ + unzip \ + zip + +# Download the JDK test library. +WORKDIR /root +RUN set -ex \ + && curl -fsSL --retry 10 -o /tmp/jdktests.tar.gz http://hg.openjdk.java.net/jdk/jdk11/archive/76072a077ee1.tar.gz/test \ + && tar -xzf /tmp/jdktests.tar.gz \ + && mv jdk11-76072a077ee1/test test \ + && rm -f /tmp/jdktests.tar.gz + +# Install jtreg and add to PATH. +RUN curl -o jtreg.tar.gz https://ci.adoptopenjdk.net/view/Dependencies/job/jtreg/lastSuccessfulBuild/artifact/jtreg-4.2.0-tip.tar.gz +RUN tar -xzf jtreg.tar.gz +ENV PATH="/root/jtreg/bin:$PATH" + +COPY --from=golang /proctor /proctor +ENTRYPOINT ["/proctor", "--runtime=java"] diff --git a/test/runtimes/images/Dockerfile_nodejs12.4.0 b/test/runtimes/images/Dockerfile_nodejs12.4.0 new file mode 100644 index 000000000..26f68b487 --- /dev/null +++ b/test/runtimes/images/Dockerfile_nodejs12.4.0 @@ -0,0 +1,28 @@ +# Compile the proctor binary. +FROM golang:1.12 AS golang +ADD ["proctor/", "/go/src/proctor/"] +RUN ["go", "build", "-o", "/proctor", "/go/src/proctor"] + +FROM ubuntu:bionic +RUN apt-get update && apt-get install -y \ + curl \ + dumb-init \ + g++ \ + make \ + python + +WORKDIR /root +ARG VERSION=v12.4.0 +RUN curl -o node-${VERSION}.tar.gz https://nodejs.org/dist/${VERSION}/node-${VERSION}.tar.gz +RUN tar -zxf node-${VERSION}.tar.gz + +WORKDIR /root/node-${VERSION} +RUN ./configure +RUN make +RUN make test-build + +COPY --from=golang /proctor /proctor + +# Including dumb-init emulates the Linux "init" process, preventing the failure +# of tests involving worker processes. +ENTRYPOINT ["/usr/bin/dumb-init", "/proctor", "--runtime=nodejs"] diff --git a/test/runtimes/images/Dockerfile_php7.3.6 b/test/runtimes/images/Dockerfile_php7.3.6 new file mode 100644 index 000000000..e6b4c6329 --- /dev/null +++ b/test/runtimes/images/Dockerfile_php7.3.6 @@ -0,0 +1,27 @@ +# Compile the proctor binary. +FROM golang:1.12 AS golang +ADD ["proctor/", "/go/src/proctor/"] +RUN ["go", "build", "-o", "/proctor", "/go/src/proctor"] + +FROM ubuntu:bionic +RUN apt-get update && apt-get install -y \ + autoconf \ + automake \ + bison \ + build-essential \ + curl \ + libtool \ + libxml2-dev \ + re2c + +WORKDIR /root +ARG VERSION=7.3.6 +RUN curl -o php-${VERSION}.tar.gz https://www.php.net/distributions/php-${VERSION}.tar.gz +RUN tar -zxf php-${VERSION}.tar.gz + +WORKDIR /root/php-${VERSION} +RUN ./configure +RUN make + +COPY --from=golang /proctor /proctor +ENTRYPOINT ["/proctor", "--runtime=php"] diff --git a/test/runtimes/images/Dockerfile_python3.7.3 b/test/runtimes/images/Dockerfile_python3.7.3 new file mode 100644 index 000000000..905cd22d7 --- /dev/null +++ b/test/runtimes/images/Dockerfile_python3.7.3 @@ -0,0 +1,30 @@ +# Compile the proctor binary. +FROM golang:1.12 AS golang +ADD ["proctor/", "/go/src/proctor/"] +RUN ["go", "build", "-o", "/proctor", "/go/src/proctor"] + +FROM ubuntu:bionic + +RUN apt-get update && apt-get install -y \ + curl \ + gcc \ + libbz2-dev \ + libffi-dev \ + liblzma-dev \ + libreadline-dev \ + libssl-dev \ + make \ + zlib1g-dev + +# Use flags -LJO to follow the html redirect and download .tar.gz. +WORKDIR /root +ARG VERSION=3.7.3 +RUN curl -LJO https://github.com/python/cpython/archive/v${VERSION}.tar.gz +RUN tar -zxf cpython-${VERSION}.tar.gz + +WORKDIR /root/cpython-${VERSION} +RUN ./configure --with-pydebug +RUN make -s -j2 + +COPY --from=golang /proctor /proctor +ENTRYPOINT ["/proctor", "--runtime=python"] diff --git a/test/runtimes/images/proctor/BUILD b/test/runtimes/images/proctor/BUILD new file mode 100644 index 000000000..09dc6c42f --- /dev/null +++ b/test/runtimes/images/proctor/BUILD @@ -0,0 +1,26 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_test") + +package(licenses = ["notice"]) + +go_binary( + name = "proctor", + srcs = [ + "go.go", + "java.go", + "nodejs.go", + "php.go", + "proctor.go", + "python.go", + ], + visibility = ["//test/runtimes/images:__subpackages__"], +) + +go_test( + name = "proctor_test", + size = "small", + srcs = ["proctor_test.go"], + embed = [":proctor"], + deps = [ + "//runsc/testutil", + ], +) diff --git a/test/runtimes/go/proctor-go.go b/test/runtimes/images/proctor/go.go index 3eb24576e..3e2d5d8db 100644 --- a/test/runtimes/go/proctor-go.go +++ b/test/runtimes/images/proctor/go.go @@ -12,50 +12,42 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Binary proctor-go is a utility that facilitates language testing for Go. - -// There are two types of Go tests: "Go tool tests" and "Go tests on disk". -// "Go tool tests" are found and executed using `go tool dist test`. -// "Go tests on disk" are found in the /test directory and are -// executed using `go run run.go`. package main import ( "fmt" - "log" "os" "os/exec" - "path/filepath" "regexp" "strings" - - "gvisor.dev/gvisor/test/runtimes/common" ) var ( - dir = os.Getenv("LANG_DIR") - goBin = filepath.Join(dir, "bin/go") - testDir = filepath.Join(dir, "test") - testRegEx = regexp.MustCompile(`^.+\.go$`) + goTestRegEx = regexp.MustCompile(`^.+\.go$`) // Directories with .dir contain helper files for tests. // Exclude benchmarks and stress tests. - dirFilter = regexp.MustCompile(`^(bench|stress)\/.+$|^.+\.dir.+$`) + goDirFilter = regexp.MustCompile(`^(bench|stress)\/.+$|^.+\.dir.+$`) ) -type goRunner struct { -} +// Location of Go tests on disk. +const goTestDir = "/usr/local/go/test" -func main() { - if err := common.LaunchFunc(goRunner{}); err != nil { - log.Fatalf("Failed to start: %v", err) - } -} +// goRunner implements TestRunner for Go. +// +// There are two types of Go tests: "Go tool tests" and "Go tests on disk". +// "Go tool tests" are found and executed using `go tool dist test`. "Go tests +// on disk" are found in the /usr/local/go/test directory and are executed +// using `go run run.go`. +type goRunner struct{} + +var _ TestRunner = goRunner{} -func (g goRunner) ListTests() ([]string, error) { +// ListTests implements TestRunner.ListTests. +func (goRunner) ListTests() ([]string, error) { // Go tool dist test tests. args := []string{"tool", "dist", "test", "-list"} - cmd := exec.Command(filepath.Join(dir, "bin/go"), args...) + cmd := exec.Command("go", args...) cmd.Stderr = os.Stderr out, err := cmd.Output() if err != nil { @@ -67,14 +59,14 @@ func (g goRunner) ListTests() ([]string, error) { } // Go tests on disk. - diskSlice, err := common.Search(testDir, testRegEx) + diskSlice, err := search(goTestDir, goTestRegEx) if err != nil { return nil, err } // Remove items from /bench/, /stress/ and .dir files diskFiltered := diskSlice[:0] for _, file := range diskSlice { - if !dirFilter.MatchString(file) { + if !goDirFilter.MatchString(file) { diskFiltered = append(diskFiltered, file) } } @@ -82,24 +74,17 @@ func (g goRunner) ListTests() ([]string, error) { return append(toolSlice, diskFiltered...), nil } -func (g goRunner) RunTest(test string) error { +// TestCmd implements TestRunner.TestCmd. +func (goRunner) TestCmd(test string) *exec.Cmd { // Check if test exists on disk by searching for file of the same name. // This will determine whether or not it is a Go test on disk. if strings.HasSuffix(test, ".go") { // Test has suffix ".go" which indicates a disk test, run it as such. - cmd := exec.Command(goBin, "run", "run.go", "-v", "--", test) - cmd.Dir = testDir - cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to run test: %v", err) - } - } else { - // No ".go" suffix, run as a tool test. - cmd := exec.Command(goBin, "tool", "dist", "test", "-run", test) - cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to run test: %v", err) - } + cmd := exec.Command("go", "run", "run.go", "-v", "--", test) + cmd.Dir = goTestDir + return cmd } - return nil + + // No ".go" suffix, run as a tool test. + return exec.Command("go", "tool", "dist", "test", "-run", test) } diff --git a/test/runtimes/java/proctor-java.go b/test/runtimes/images/proctor/java.go index 7f6a66f4f..8b362029d 100644 --- a/test/runtimes/java/proctor-java.go +++ b/test/runtimes/images/proctor/java.go @@ -12,40 +12,31 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Binary proctor-java is a utility that facilitates language testing for Java. package main import ( "fmt" - "log" "os" "os/exec" - "path/filepath" "regexp" "strings" - - "gvisor.dev/gvisor/test/runtimes/common" ) -var ( - dir = os.Getenv("LANG_DIR") - hash = os.Getenv("LANG_HASH") - jtreg = filepath.Join(dir, "jtreg/bin/jtreg") - exclDirs = regexp.MustCompile(`(^(sun\/security)|(java\/util\/stream)|(java\/time)| )`) -) +// Directories to exclude from tests. +var javaExclDirs = regexp.MustCompile(`(^(sun\/security)|(java\/util\/stream)|(java\/time)| )`) -type javaRunner struct { -} +// Location of java tests. +const javaTestDir = "/root/test/jdk" -func main() { - if err := common.LaunchFunc(javaRunner{}); err != nil { - log.Fatalf("Failed to start: %v", err) - } -} +// javaRunner implements TestRunner for Java. +type javaRunner struct{} + +var _ TestRunner = javaRunner{} -func (j javaRunner) ListTests() ([]string, error) { +// ListTests implements TestRunner.ListTests. +func (javaRunner) ListTests() ([]string, error) { args := []string{ - "-dir:/root/jdk11-" + hash + "/test/jdk", + "-dir:" + javaTestDir, "-ignore:quiet", "-a", "-listtests", @@ -54,7 +45,7 @@ func (j javaRunner) ListTests() ([]string, error) { ":jdk_sound", ":jdk_imageio", } - cmd := exec.Command(jtreg, args...) + cmd := exec.Command("jtreg", args...) cmd.Stderr = os.Stderr out, err := cmd.Output() if err != nil { @@ -62,19 +53,19 @@ func (j javaRunner) ListTests() ([]string, error) { } var testSlice []string for _, test := range strings.Split(string(out), "\n") { - if !exclDirs.MatchString(test) { + if !javaExclDirs.MatchString(test) { testSlice = append(testSlice, test) } } return testSlice, nil } -func (j javaRunner) RunTest(test string) error { - args := []string{"-noreport", "-dir:/root/jdk11-" + hash + "/test/jdk", test} - cmd := exec.Command(jtreg, args...) - cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to run: %v", err) +// TestCmd implements TestRunner.TestCmd. +func (javaRunner) TestCmd(test string) *exec.Cmd { + args := []string{ + "-noreport", + "-dir:" + javaTestDir, + test, } - return nil + return exec.Command("jtreg", args...) } diff --git a/test/runtimes/images/proctor/nodejs.go b/test/runtimes/images/proctor/nodejs.go new file mode 100644 index 000000000..bd57db444 --- /dev/null +++ b/test/runtimes/images/proctor/nodejs.go @@ -0,0 +1,46 @@ +// Copyright 2019 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 main + +import ( + "os/exec" + "path/filepath" + "regexp" +) + +var nodejsTestRegEx = regexp.MustCompile(`^test-[^-].+\.js$`) + +// Location of nodejs tests relative to working dir. +const nodejsTestDir = "test" + +// nodejsRunner implements TestRunner for NodeJS. +type nodejsRunner struct{} + +var _ TestRunner = nodejsRunner{} + +// ListTests implements TestRunner.ListTests. +func (nodejsRunner) ListTests() ([]string, error) { + testSlice, err := search(nodejsTestDir, nodejsTestRegEx) + if err != nil { + return nil, err + } + return testSlice, nil +} + +// TestCmd implements TestRunner.TestCmd. +func (nodejsRunner) TestCmd(test string) *exec.Cmd { + args := []string{filepath.Join("tools", "test.py"), test} + return exec.Command("/usr/bin/python", args...) +} diff --git a/test/runtimes/php/proctor-php.go b/test/runtimes/images/proctor/php.go index e6c5fabdf..9115040e1 100644 --- a/test/runtimes/php/proctor-php.go +++ b/test/runtimes/images/proctor/php.go @@ -12,47 +12,31 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Binary proctor-php is a utility that facilitates language testing for PHP. package main import ( - "fmt" - "log" - "os" "os/exec" "regexp" - - "gvisor.dev/gvisor/test/runtimes/common" ) -var ( - dir = os.Getenv("LANG_DIR") - testRegEx = regexp.MustCompile(`^.+\.phpt$`) -) +var phpTestRegEx = regexp.MustCompile(`^.+\.phpt$`) -type phpRunner struct { -} +// phpRunner implements TestRunner for PHP. +type phpRunner struct{} -func main() { - if err := common.LaunchFunc(phpRunner{}); err != nil { - log.Fatalf("Failed to start: %v", err) - } -} +var _ TestRunner = phpRunner{} -func (p phpRunner) ListTests() ([]string, error) { - testSlice, err := common.Search(dir, testRegEx) +// ListTests implements TestRunner.ListTests. +func (phpRunner) ListTests() ([]string, error) { + testSlice, err := search(".", phpTestRegEx) if err != nil { return nil, err } return testSlice, nil } -func (p phpRunner) RunTest(test string) error { +// TestCmd implements TestRunner.TestCmd. +func (phpRunner) TestCmd(test string) *exec.Cmd { args := []string{"test", "TESTS=" + test} - cmd := exec.Command("make", args...) - cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to run: %v", err) - } - return nil + return exec.Command("make", args...) } diff --git a/test/runtimes/images/proctor/proctor.go b/test/runtimes/images/proctor/proctor.go new file mode 100644 index 000000000..e6178e82b --- /dev/null +++ b/test/runtimes/images/proctor/proctor.go @@ -0,0 +1,154 @@ +// Copyright 2019 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. + +// Binary proctor runs the test for a particular runtime. It is meant to be +// included in Docker images for all runtime tests. +package main + +import ( + "flag" + "fmt" + "log" + "os" + "os/exec" + "os/signal" + "path/filepath" + "regexp" + "syscall" +) + +// TestRunner is an interface that must be implemented for each runtime +// integrated with proctor. +type TestRunner interface { + // ListTests returns a string slice of tests available to run. + ListTests() ([]string, error) + + // TestCmd returns an *exec.Cmd that will run the given test. + TestCmd(test string) *exec.Cmd +} + +var ( + runtime = flag.String("runtime", "", "name of runtime") + list = flag.Bool("list", false, "list all available tests") + test = flag.String("test", "", "run a single test from the list of available tests") + pause = flag.Bool("pause", false, "cause container to pause indefinitely, reaping any zombie children") +) + +func main() { + flag.Parse() + + if *pause { + pauseAndReap() + panic("pauseAndReap should never return") + } + + if *runtime == "" { + log.Fatalf("runtime flag must be provided") + } + + tr, err := testRunnerForRuntime(*runtime) + if err != nil { + log.Fatalf("%v", err) + } + + // List tests. + if *list { + tests, err := tr.ListTests() + if err != nil { + log.Fatalf("failed to list tests: %v", err) + } + for _, test := range tests { + fmt.Println(test) + } + return + } + + // Run a single test. + if *test == "" { + log.Fatalf("test flag must be provided") + } + cmd := tr.TestCmd(*test) + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + if err := cmd.Run(); err != nil { + log.Fatalf("FAIL: %v", err) + } +} + +// testRunnerForRuntime returns a new TestRunner for the given runtime. +func testRunnerForRuntime(runtime string) (TestRunner, error) { + switch runtime { + case "go": + return goRunner{}, nil + case "java": + return javaRunner{}, nil + case "nodejs": + return nodejsRunner{}, nil + case "php": + return phpRunner{}, nil + case "python": + return pythonRunner{}, nil + } + return nil, fmt.Errorf("invalid runtime %q", runtime) +} + +// pauseAndReap is like init. It runs forever and reaps any children. +func pauseAndReap() { + // Get notified of any new children. + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGCHLD) + + for { + if _, ok := <-ch; !ok { + // Channel closed. This should not happen. + panic("signal channel closed") + } + + // Reap the child. + for { + if cpid, _ := syscall.Wait4(-1, nil, syscall.WNOHANG, nil); cpid < 1 { + break + } + } + } +} + +// search is a helper function to find tests in the given directory that match +// the regex. +func search(root string, testFilter *regexp.Regexp) ([]string, error) { + var testSlice []string + + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + name := filepath.Base(path) + + if info.IsDir() || !testFilter.MatchString(name) { + return nil + } + + relPath, err := filepath.Rel(root, path) + if err != nil { + return err + } + testSlice = append(testSlice, relPath) + return nil + }) + if err != nil { + return nil, fmt.Errorf("walking %q: %v", root, err) + } + + return testSlice, nil +} diff --git a/test/runtimes/common/common_test.go b/test/runtimes/images/proctor/proctor_test.go index 4fb1e482a..6bb61d142 100644 --- a/test/runtimes/common/common_test.go +++ b/test/runtimes/images/proctor/proctor_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package common_test +package main import ( "io/ioutil" @@ -23,8 +23,7 @@ import ( "strings" "testing" - "gvisor.dev/gvisor/runsc/test/testutil" - "gvisor.dev/gvisor/test/runtimes/common" + "gvisor.dev/gvisor/runsc/testutil" ) func touch(t *testing.T, name string) { @@ -48,9 +47,9 @@ func TestSearchEmptyDir(t *testing.T) { var want []string testFilter := regexp.MustCompile(`^test-[^-].+\.tc$`) - got, err := common.Search(td, testFilter) + got, err := search(td, testFilter) if err != nil { - t.Errorf("Search error: %v", err) + t.Errorf("search error: %v", err) } if !reflect.DeepEqual(got, want) { @@ -117,9 +116,9 @@ func TestSearch(t *testing.T) { } testFilter := regexp.MustCompile(`^test-[^-].+\.tc$`) - got, err := common.Search(td, testFilter) + got, err := search(td, testFilter) if err != nil { - t.Errorf("Search error: %v", err) + t.Errorf("search error: %v", err) } if !reflect.DeepEqual(got, want) { diff --git a/test/runtimes/python/proctor-python.go b/test/runtimes/images/proctor/python.go index 35e28a7df..b9e0fbe6f 100644 --- a/test/runtimes/python/proctor-python.go +++ b/test/runtimes/images/proctor/python.go @@ -12,36 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Binary proctor-python is a utility that facilitates language testing for Pyhton. package main import ( "fmt" - "log" "os" "os/exec" - "path/filepath" "strings" - - "gvisor.dev/gvisor/test/runtimes/common" ) -var ( - dir = os.Getenv("LANG_DIR") -) +// pythonRunner implements TestRunner for Python. +type pythonRunner struct{} -type pythonRunner struct { -} +var _ TestRunner = pythonRunner{} -func main() { - if err := common.LaunchFunc(pythonRunner{}); err != nil { - log.Fatalf("Failed to start: %v", err) - } -} - -func (p pythonRunner) ListTests() ([]string, error) { +// ListTests implements TestRunner.ListTests. +func (pythonRunner) ListTests() ([]string, error) { args := []string{"-m", "test", "--list-tests"} - cmd := exec.Command(filepath.Join(dir, "python"), args...) + cmd := exec.Command("./python", args...) cmd.Stderr = os.Stderr out, err := cmd.Output() if err != nil { @@ -54,12 +42,8 @@ func (p pythonRunner) ListTests() ([]string, error) { return toolSlice, nil } -func (p pythonRunner) RunTest(test string) error { +// TestCmd implements TestRunner.TestCmd. +func (pythonRunner) TestCmd(test string) *exec.Cmd { args := []string{"-m", "test", test} - cmd := exec.Command(filepath.Join(dir, "python"), args...) - cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to run: %v", err) - } - return nil + return exec.Command("./python", args...) } diff --git a/test/runtimes/java/BUILD b/test/runtimes/java/BUILD deleted file mode 100644 index 8c39d39ec..000000000 --- a/test/runtimes/java/BUILD +++ /dev/null @@ -1,9 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_binary") - -package(licenses = ["notice"]) - -go_binary( - name = "proctor-java", - srcs = ["proctor-java.go"], - deps = ["//test/runtimes/common"], -) diff --git a/test/runtimes/java/Dockerfile b/test/runtimes/java/Dockerfile deleted file mode 100644 index b9132b575..000000000 --- a/test/runtimes/java/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -FROM ubuntu:bionic -# This hash is associated with a specific JDK release and needed for ensuring -# the same version is downloaded every time. -ENV LANG_HASH=76072a077ee1 -ENV LANG_VER=11 -ENV LANG_NAME=Java - -RUN apt-get update && apt-get install -y \ - autoconf \ - build-essential \ - curl \ - make \ - openjdk-${LANG_VER}-jdk \ - unzip \ - zip - -WORKDIR /root -RUN curl -o go.tar.gz https://dl.google.com/go/go1.12.6.linux-amd64.tar.gz -RUN tar -zxf go.tar.gz - -# Download the JDK test library. -RUN set -ex \ - && curl -fsSL --retry 10 -o /tmp/jdktests.tar.gz http://hg.openjdk.java.net/jdk/jdk${LANG_VER}/archive/${LANG_HASH}.tar.gz/test \ - && tar -xzf /tmp/jdktests.tar.gz -C /root \ - && rm -f /tmp/jdktests.tar.gz - -RUN curl -o jtreg.tar.gz https://ci.adoptopenjdk.net/view/Dependencies/job/jtreg/lastSuccessfulBuild/artifact/jtreg-4.2.0-tip.tar.gz -RUN tar -xzf jtreg.tar.gz - -ENV LANG_DIR=/root - -COPY common /root/go/src/gvisor.dev/gvisor/test/runtimes/common/common -COPY java/proctor-java.go ${LANG_DIR} - -ENTRYPOINT ["/root/go/bin/go", "run", "proctor-java.go"] diff --git a/test/runtimes/nodejs/BUILD b/test/runtimes/nodejs/BUILD deleted file mode 100644 index 0594c250b..000000000 --- a/test/runtimes/nodejs/BUILD +++ /dev/null @@ -1,9 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_binary") - -package(licenses = ["notice"]) - -go_binary( - name = "proctor-nodejs", - srcs = ["proctor-nodejs.go"], - deps = ["//test/runtimes/common"], -) diff --git a/test/runtimes/nodejs/Dockerfile b/test/runtimes/nodejs/Dockerfile deleted file mode 100644 index aba30d2ee..000000000 --- a/test/runtimes/nodejs/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -FROM ubuntu:bionic -ENV LANG_VER=12.4.0 -ENV LANG_NAME=Node - -RUN apt-get update && apt-get install -y \ - curl \ - dumb-init \ - g++ \ - make \ - python - -WORKDIR /root -RUN curl -o go.tar.gz https://dl.google.com/go/go1.12.6.linux-amd64.tar.gz -RUN tar -zxf go.tar.gz - -RUN curl -o node-v${LANG_VER}.tar.gz https://nodejs.org/dist/v${LANG_VER}/node-v${LANG_VER}.tar.gz -RUN tar -zxf node-v${LANG_VER}.tar.gz -ENV LANG_DIR=/root/node-v${LANG_VER} - -WORKDIR ${LANG_DIR} -RUN ./configure -RUN make -RUN make test-build - -COPY common /root/go/src/gvisor.dev/gvisor/test/runtimes/common/common -COPY nodejs/proctor-nodejs.go ${LANG_DIR} - -# Including dumb-init emulates the Linux "init" process, preventing the failure -# of tests involving worker processes. -ENTRYPOINT ["/usr/bin/dumb-init", "/root/go/bin/go", "run", "proctor-nodejs.go"] diff --git a/test/runtimes/nodejs/proctor-nodejs.go b/test/runtimes/nodejs/proctor-nodejs.go deleted file mode 100644 index 0624f6a0d..000000000 --- a/test/runtimes/nodejs/proctor-nodejs.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2019 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. - -// Binary proctor-nodejs is a utility that facilitates language testing for NodeJS. -package main - -import ( - "fmt" - "log" - "os" - "os/exec" - "path/filepath" - "regexp" - - "gvisor.dev/gvisor/test/runtimes/common" -) - -var ( - dir = os.Getenv("LANG_DIR") - testDir = filepath.Join(dir, "test") - testRegEx = regexp.MustCompile(`^test-[^-].+\.js$`) -) - -type nodejsRunner struct { -} - -func main() { - if err := common.LaunchFunc(nodejsRunner{}); err != nil { - log.Fatalf("Failed to start: %v", err) - } -} - -func (n nodejsRunner) ListTests() ([]string, error) { - testSlice, err := common.Search(testDir, testRegEx) - if err != nil { - return nil, err - } - return testSlice, nil -} - -func (n nodejsRunner) RunTest(test string) error { - args := []string{filepath.Join(dir, "tools", "test.py"), test} - cmd := exec.Command("/usr/bin/python", args...) - cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to run: %v", err) - } - return nil -} diff --git a/test/runtimes/php/BUILD b/test/runtimes/php/BUILD deleted file mode 100644 index 31799b77a..000000000 --- a/test/runtimes/php/BUILD +++ /dev/null @@ -1,9 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_binary") - -package(licenses = ["notice"]) - -go_binary( - name = "proctor-php", - srcs = ["proctor-php.go"], - deps = ["//test/runtimes/common"], -) diff --git a/test/runtimes/php/Dockerfile b/test/runtimes/php/Dockerfile deleted file mode 100644 index 491ab902d..000000000 --- a/test/runtimes/php/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -FROM ubuntu:bionic -ENV LANG_VER=7.3.6 -ENV LANG_NAME=PHP - -RUN apt-get update && apt-get install -y \ - autoconf \ - automake \ - bison \ - build-essential \ - curl \ - libtool \ - libxml2-dev \ - re2c - -WORKDIR /root -RUN curl -o go.tar.gz https://dl.google.com/go/go1.12.6.linux-amd64.tar.gz -RUN tar -zxf go.tar.gz - -RUN curl -o php-${LANG_VER}.tar.gz https://www.php.net/distributions/php-${LANG_VER}.tar.gz -RUN tar -zxf php-${LANG_VER}.tar.gz -ENV LANG_DIR=/root/php-${LANG_VER} - -WORKDIR ${LANG_DIR} -RUN ./configure -RUN make - -COPY common /root/go/src/gvisor.dev/gvisor/test/runtimes/common/common -COPY php/proctor-php.go ${LANG_DIR} - -ENTRYPOINT ["/root/go/bin/go", "run", "proctor-php.go"] diff --git a/test/runtimes/python/BUILD b/test/runtimes/python/BUILD deleted file mode 100644 index 37fd6a0f2..000000000 --- a/test/runtimes/python/BUILD +++ /dev/null @@ -1,9 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_binary") - -package(licenses = ["notice"]) - -go_binary( - name = "proctor-python", - srcs = ["proctor-python.go"], - deps = ["//test/runtimes/common"], -) diff --git a/test/runtimes/python/Dockerfile b/test/runtimes/python/Dockerfile deleted file mode 100644 index 710daee43..000000000 --- a/test/runtimes/python/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -FROM ubuntu:bionic -ENV LANG_VER=3.7.3 -ENV LANG_NAME=Python - -RUN apt-get update && apt-get install -y \ - curl \ - gcc \ - libbz2-dev \ - libffi-dev \ - liblzma-dev \ - libreadline-dev \ - libssl-dev \ - make \ - zlib1g-dev - -WORKDIR /root -RUN curl -o go.tar.gz https://dl.google.com/go/go1.12.6.linux-amd64.tar.gz -RUN tar -zxf go.tar.gz - -# Use flags -LJO to follow the html redirect and download .tar.gz. -RUN curl -LJO https://github.com/python/cpython/archive/v${LANG_VER}.tar.gz -RUN tar -zxf cpython-${LANG_VER}.tar.gz -ENV LANG_DIR=/root/cpython-${LANG_VER} - -WORKDIR ${LANG_DIR} -RUN ./configure --with-pydebug -RUN make -s -j2 - -COPY common /root/go/src/gvisor.dev/gvisor/test/runtimes/common/common -COPY python/proctor-python.go ${LANG_DIR} - -ENTRYPOINT ["/root/go/bin/go", "run", "proctor-python.go"] diff --git a/test/runtimes/runner.go b/test/runtimes/runner.go new file mode 100644 index 000000000..bec37c69d --- /dev/null +++ b/test/runtimes/runner.go @@ -0,0 +1,199 @@ +// Copyright 2019 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. + +// Binary runner runs the runtime tests in a Docker container. +package main + +import ( + "encoding/csv" + "flag" + "fmt" + "io" + "log" + "os" + "sort" + "strings" + "testing" + "time" + + "gvisor.dev/gvisor/runsc/dockerutil" + "gvisor.dev/gvisor/runsc/testutil" +) + +var ( + lang = flag.String("lang", "", "language runtime to test") + image = flag.String("image", "", "docker image with runtime tests") + blacklistFile = flag.String("blacklist_file", "", "file containing blacklist of tests to exclude, in CSV format with fields: test name, bug id, comment") +) + +// Wait time for each test to run. +const timeout = 5 * time.Minute + +func main() { + flag.Parse() + if *lang == "" || *image == "" { + fmt.Fprintf(os.Stderr, "lang and image flags must not be empty\n") + os.Exit(1) + } + + os.Exit(runTests()) +} + +// runTests is a helper that is called by main. It exists so that we can run +// defered functions before exiting. It returns an exit code that should be +// passed to os.Exit. +func runTests() int { + // Get tests to blacklist. + blacklist, err := getBlacklist() + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting blacklist: %s\n", err.Error()) + return 1 + } + + // Create a single docker container that will be used for all tests. + d := dockerutil.MakeDocker("gvisor-" + *lang) + defer d.CleanUp() + + // Get a slice of tests to run. This will also start a single Docker + // container that will be used to run each test. The final test will + // stop the Docker container. + tests, err := getTests(d, blacklist) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + return 1 + } + + m := testing.MainStart(testDeps{}, tests, nil, nil) + return m.Run() +} + +// getTests returns a slice of tests to run, subject to the shard size and +// index. +func getTests(d dockerutil.Docker, blacklist map[string]struct{}) ([]testing.InternalTest, error) { + // Pull the image. + if err := dockerutil.Pull(*image); err != nil { + return nil, fmt.Errorf("docker pull %q failed: %v", *image, err) + } + + // Run proctor with --pause flag to keep container alive forever. + if err := d.Run(*image, "--pause"); err != nil { + return nil, fmt.Errorf("docker run failed: %v", err) + } + + // Get a list of all tests in the image. + list, err := d.Exec("/proctor", "--runtime", *lang, "--list") + if err != nil { + return nil, fmt.Errorf("docker exec failed: %v", err) + } + + // Calculate a subset of tests to run corresponding to the current + // shard. + tests := strings.Fields(list) + sort.Strings(tests) + begin, end, err := testutil.TestBoundsForShard(len(tests)) + if err != nil { + return nil, fmt.Errorf("TestsForShard() failed: %v", err) + } + log.Printf("Got bounds [%d:%d) for shard out of %d total tests", begin, end, len(tests)) + tests = tests[begin:end] + + var itests []testing.InternalTest + for _, tc := range tests { + // Capture tc in this scope. + tc := tc + itests = append(itests, testing.InternalTest{ + Name: tc, + F: func(t *testing.T) { + // Is the test blacklisted? + if _, ok := blacklist[tc]; ok { + t.Skip("SKIP: blacklisted test %q", tc) + } + + var ( + now = time.Now() + done = make(chan struct{}) + output string + err error + ) + + go func() { + fmt.Printf("RUNNING %s...\n", tc) + output, err = d.Exec("/proctor", "--runtime", *lang, "--test", tc) + close(done) + }() + + select { + case <-done: + if err == nil { + fmt.Printf("PASS: %s (%v)\n\n", tc, time.Since(now)) + return + } + t.Errorf("FAIL: %s (%v):\n%s\n", tc, time.Since(now), output) + case <-time.After(timeout): + t.Errorf("TIMEOUT: %s (%v):\n%s\n", tc, time.Since(now), output) + } + }, + }) + } + return itests, nil +} + +// getBlacklist reads the blacklist file and returns a set of test names to +// exclude. +func getBlacklist() (map[string]struct{}, error) { + blacklist := make(map[string]struct{}) + if *blacklistFile == "" { + return blacklist, nil + } + file, err := testutil.FindFile(*blacklistFile) + if err != nil { + return nil, err + } + f, err := os.Open(file) + if err != nil { + return nil, err + } + defer f.Close() + + r := csv.NewReader(f) + + // First line is header. Skip it. + if _, err := r.Read(); err != nil { + return nil, err + } + + for { + record, err := r.Read() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + blacklist[record[0]] = struct{}{} + } + return blacklist, nil +} + +// testDeps implements testing.testDeps (an unexported interface), and is +// required to use testing.MainStart. +type testDeps struct{} + +func (f testDeps) MatchString(a, b string) (bool, error) { return a == b, nil } +func (f testDeps) StartCPUProfile(io.Writer) error { return nil } +func (f testDeps) StopCPUProfile() {} +func (f testDeps) WriteProfileTo(string, io.Writer, int) error { return nil } +func (f testDeps) ImportPath() string { return "" } +func (f testDeps) StartTestLog(io.Writer) {} +func (f testDeps) StopTestLog() error { return nil } diff --git a/test/runtimes/runner.sh b/test/runtimes/runner.sh new file mode 100755 index 000000000..a8d9a3460 --- /dev/null +++ b/test/runtimes/runner.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# 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. + +set -euf -x -o pipefail + +echo -- "$@" + +# Create outputs dir if it does not exist. +if [[ -n "${TEST_UNDECLARED_OUTPUTS_DIR}" ]]; then + mkdir -p "${TEST_UNDECLARED_OUTPUTS_DIR}" + chmod a+rwx "${TEST_UNDECLARED_OUTPUTS_DIR}" +fi + +# Update the timestamp on the shard status file. Bazel looks for this. +touch "${TEST_SHARD_STATUS_FILE}" + +# Get location of runner binary. +readonly runner=$(find "${TEST_SRCDIR}" -name runner) + +# Pass the arguments of this script directly to the runner. +exec "${runner}" "$@" + diff --git a/test/runtimes/runtimes_test.go b/test/runtimes/runtimes_test.go deleted file mode 100644 index 9421021a1..000000000 --- a/test/runtimes/runtimes_test.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2019 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 runtimes - -import ( - "strings" - "testing" - "time" - - "gvisor.dev/gvisor/runsc/test/testutil" -) - -// Wait time for each test to run. -const timeout = 180 * time.Second - -// Helper function to execute the docker container associated with the -// language passed. Captures the output of the list function and executes -// each test individually, supplying any errors recieved. -func testLang(t *testing.T, lang string) { - t.Helper() - - img := "gcr.io/gvisor-presubmit/" + lang - if err := testutil.Pull(img); err != nil { - t.Fatalf("docker pull failed: %v", err) - } - - c := testutil.MakeDocker("gvisor-list") - - list, err := c.RunFg(img, "--list") - if err != nil { - t.Fatalf("docker run failed: %v", err) - } - c.CleanUp() - - tests := strings.Fields(list) - - for _, tc := range tests { - tc := tc - t.Run(tc, func(t *testing.T) { - d := testutil.MakeDocker("gvisor-test") - if err := d.Run(img, "--test", tc); err != nil { - t.Fatalf("docker test %q failed to run: %v", tc, err) - } - defer d.CleanUp() - - status, err := d.Wait(timeout) - if err != nil { - t.Fatalf("docker test %q failed to wait: %v", tc, err) - } - if status == 0 { - t.Logf("test %q passed", tc) - return - } - logs, err := d.Logs() - if err != nil { - t.Fatalf("docker test %q failed to supply logs: %v", tc, err) - } - t.Errorf("test %q failed: %v", tc, logs) - }) - } -} - -func TestGo(t *testing.T) { - testLang(t, "go") -} - -func TestJava(t *testing.T) { - testLang(t, "java") -} - -func TestNodejs(t *testing.T) { - testLang(t, "nodejs") -} - -func TestPhp(t *testing.T) { - testLang(t, "php") -} - -func TestPython(t *testing.T) { - testLang(t, "python") -} diff --git a/test/syscalls/BUILD b/test/syscalls/BUILD index ccae4925f..341e6b252 100644 --- a/test/syscalls/BUILD +++ b/test/syscalls/BUILD @@ -305,6 +305,8 @@ syscall_test( syscall_test(test = "//test/syscalls/linux:proc_pid_uid_gid_map_test") +syscall_test(test = "//test/syscalls/linux:proc_net_test") + syscall_test( size = "medium", test = "//test/syscalls/linux:pselect_test", @@ -344,6 +346,11 @@ syscall_test( ) syscall_test( + add_overlay = True, + test = "//test/syscalls/linux:readahead_test", +) + +syscall_test( size = "medium", shard_count = 5, test = "//test/syscalls/linux:readv_socket_test", @@ -691,8 +698,13 @@ syscall_test( syscall_test(test = "//test/syscalls/linux:proc_net_unix_test") +syscall_test(test = "//test/syscalls/linux:proc_net_tcp_test") + +syscall_test(test = "//test/syscalls/linux:proc_net_udp_test") + go_binary( name = "syscall_test_runner", + testonly = 1, srcs = ["syscall_test_runner.go"], data = [ "//runsc", @@ -700,7 +712,7 @@ go_binary( deps = [ "//pkg/log", "//runsc/specutils", - "//runsc/test/testutil", + "//runsc/testutil", "//test/syscalls/gtest", "@com_github_opencontainers_runtime-spec//specs-go:go_default_library", "@org_golang_x_sys//unix:go_default_library", diff --git a/test/syscalls/build_defs.bzl b/test/syscalls/build_defs.bzl index 771d171f8..e94ef5602 100644 --- a/test/syscalls/build_defs.bzl +++ b/test/syscalls/build_defs.bzl @@ -94,6 +94,7 @@ def _syscall_test( # more stable. if platform == "kvm": tags += ["manual"] + tags += ["requires-kvm"] args = [ # Arguments are passed directly to syscall_test_runner binary. @@ -122,3 +123,6 @@ def sh_test(**kwargs): native.sh_test( **kwargs ) + +def select_for_linux(for_linux, for_others = []): + return for_linux diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index ca4344139..d5a2b7725 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -1,3 +1,6 @@ +load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library") +load("//test/syscalls:build_defs.bzl", "select_for_linux") + package( default_visibility = ["//:sandbox"], licenses = ["notice"], @@ -108,20 +111,27 @@ cc_library( cc_library( name = "socket_test_util", testonly = 1, - srcs = ["socket_test_util.cc"], + srcs = [ + "socket_test_util.cc", + ] + select_for_linux( + [ + "socket_test_util_impl.cc", + ], + ), hdrs = ["socket_test_util.h"], deps = [ + "@com_google_googletest//:gtest", + "@com_google_absl//absl/memory", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_absl//absl/time", "//test/util:file_descriptor", "//test/util:posix_error", "//test/util:temp_path", "//test/util:test_util", "//test/util:thread_util", - "@com_google_absl//absl/memory", - "@com_google_absl//absl/strings", - "@com_google_absl//absl/strings:str_format", - "@com_google_absl//absl/time", - "@com_google_googletest//:gtest", - ], + ] + select_for_linux([ + ]), ) cc_library( @@ -256,12 +266,15 @@ cc_binary( ], linkstatic = 1, deps = [ - # The heap check doesn't handle mremap properly. + # The heapchecker doesn't recognize that io_destroy munmaps. "@com_google_googletest//:gtest", "@com_google_absl//absl/strings", "//test/util:cleanup", "//test/util:file_descriptor", + "//test/util:fs_util", + "//test/util:memory_util", "//test/util:posix_error", + "//test/util:proc_util", "//test/util:temp_path", "//test/util:test_main", "//test/util:test_util", @@ -381,6 +394,7 @@ cc_binary( "//test/util:test_main", "//test/util:test_util", "//test/util:thread_util", + "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/synchronization", "@com_google_googletest//:gtest", ], @@ -399,6 +413,7 @@ cc_binary( "//test/util:test_main", "//test/util:test_util", "//test/util:thread_util", + "@com_google_absl//absl/flags:flag", "@com_google_googletest//:gtest", ], ) @@ -715,6 +730,7 @@ cc_binary( "//test/util:test_util", "//test/util:timer_util", "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/memory", "@com_google_absl//absl/strings", "@com_google_absl//absl/time", @@ -963,6 +979,7 @@ cc_binary( "//test/util:test_main", "//test/util:test_util", "//test/util:thread_util", + "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/synchronization", "@com_google_absl//absl/time", "@com_google_googletest//:gtest", @@ -983,6 +1000,7 @@ cc_binary( "//test/util:test_main", "//test/util:test_util", "//test/util:thread_util", + "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/strings", "@com_google_googletest//:gtest", ], @@ -1204,6 +1222,7 @@ cc_binary( "//test/util:test_main", "//test/util:test_util", "//test/util:thread_util", + "@com_google_absl//absl/memory", "@com_google_absl//absl/strings", "@com_google_googletest//:gtest", ], @@ -1411,6 +1430,7 @@ cc_binary( "//test/util:posix_error", "//test/util:test_util", "//test/util:thread_util", + "@com_google_absl//absl/flags:flag", "@com_google_googletest//:gtest", ], ) @@ -1427,6 +1447,7 @@ cc_binary( "//test/util:posix_error", "//test/util:test_util", "//test/util:thread_util", + "@com_google_absl//absl/flags:flag", "@com_google_googletest//:gtest", ], ) @@ -1530,6 +1551,7 @@ cc_binary( srcs = ["proc_net.cc"], linkstatic = 1, deps = [ + "//test/util:capability_util", "//test/util:file_descriptor", "//test/util:fs_util", "//test/util:test_main", @@ -1609,6 +1631,7 @@ cc_binary( "//test/util:test_util", "//test/util:thread_util", "//test/util:time_util", + "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/time", "@com_google_googletest//:gtest", ], @@ -1713,6 +1736,20 @@ cc_binary( ) cc_binary( + name = "readahead_test", + testonly = 1, + srcs = ["readahead.cc"], + linkstatic = 1, + deps = [ + "//test/util:file_descriptor", + "//test/util:temp_path", + "//test/util:test_main", + "//test/util:test_util", + "@com_google_googletest//:gtest", + ], +) + +cc_binary( name = "readv_test", testonly = 1, srcs = [ @@ -1871,7 +1908,9 @@ cc_binary( "//test/util:temp_path", "//test/util:test_main", "//test/util:test_util", + "//test/util:thread_util", "@com_google_absl//absl/strings", + "@com_google_absl//absl/time", "@com_google_googletest//:gtest", ], ) @@ -1905,6 +1944,7 @@ cc_binary( "//test/util:test_util", "//test/util:thread_util", "@com_google_absl//absl/strings", + "@com_google_absl//absl/time", "@com_google_googletest//:gtest", ], ) @@ -1957,6 +1997,24 @@ cc_binary( ) cc_binary( + name = "signalfd_test", + testonly = 1, + srcs = ["signalfd.cc"], + linkstatic = 1, + deps = [ + "//test/util:file_descriptor", + "//test/util:logging", + "//test/util:posix_error", + "//test/util:signal_util", + "//test/util:test_main", + "//test/util:test_util", + "//test/util:thread_util", + "@com_google_absl//absl/synchronization", + "@com_google_googletest//:gtest", + ], +) + +cc_binary( name = "sigprocmask_test", testonly = 1, srcs = ["sigprocmask.cc"], @@ -1979,6 +2037,7 @@ cc_binary( "//test/util:posix_error", "//test/util:test_util", "//test/util:thread_util", + "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/time", "@com_google_googletest//:gtest", ], @@ -2405,6 +2464,63 @@ cc_binary( ) cc_binary( + name = "socket_bind_to_device_test", + testonly = 1, + srcs = [ + "socket_bind_to_device.cc", + ], + linkstatic = 1, + deps = [ + ":ip_socket_test_util", + ":socket_bind_to_device_util", + ":socket_test_util", + "//test/util:capability_util", + "//test/util:test_main", + "//test/util:test_util", + "//test/util:thread_util", + "@com_google_googletest//:gtest", + ], +) + +cc_binary( + name = "socket_bind_to_device_sequence_test", + testonly = 1, + srcs = [ + "socket_bind_to_device_sequence.cc", + ], + linkstatic = 1, + deps = [ + ":ip_socket_test_util", + ":socket_bind_to_device_util", + ":socket_test_util", + "//test/util:capability_util", + "//test/util:test_main", + "//test/util:test_util", + "//test/util:thread_util", + "@com_google_googletest//:gtest", + ], +) + +cc_binary( + name = "socket_bind_to_device_distribution_test", + testonly = 1, + srcs = [ + "socket_bind_to_device_distribution.cc", + ], + linkstatic = 1, + deps = [ + ":ip_socket_test_util", + ":socket_bind_to_device_util", + ":socket_test_util", + "//test/util:capability_util", + "//test/util:test_main", + "//test/util:test_util", + "//test/util:thread_util", + "@com_google_googletest//:gtest", + ], +) + +cc_binary( name = "socket_ip_udp_loopback_non_blocking_test", testonly = 1, srcs = [ @@ -2681,6 +2797,23 @@ cc_library( alwayslink = 1, ) +cc_library( + name = "socket_bind_to_device_util", + testonly = 1, + srcs = [ + "socket_bind_to_device_util.cc", + ], + hdrs = [ + "socket_bind_to_device_util.h", + ], + deps = [ + "//test/util:test_util", + "@com_google_absl//absl/memory", + "@com_google_absl//absl/strings", + ], + alwayslink = 1, +) + cc_binary( name = "socket_stream_local_test", testonly = 1, @@ -3104,8 +3237,6 @@ cc_binary( testonly = 1, srcs = ["timers.cc"], linkstatic = 1, - # FIXME(b/136599201) - tags = ["flaky"], deps = [ "//test/util:cleanup", "//test/util:logging", @@ -3114,6 +3245,7 @@ cc_binary( "//test/util:signal_util", "//test/util:test_util", "//test/util:thread_util", + "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/time", "@com_google_googletest//:gtest", ], @@ -3193,6 +3325,8 @@ cc_binary( "//test/util:test_main", "//test/util:test_util", "//test/util:thread_util", + "//test/util:uid_util", + "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/strings", "@com_google_googletest//:gtest", ], @@ -3284,6 +3418,7 @@ cc_binary( "//test/util:multiprocess_util", "//test/util:test_util", "//test/util:time_util", + "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/time", "@com_google_googletest//:gtest", ], @@ -3353,6 +3488,7 @@ cc_binary( "//test/util:test_util", "//test/util:thread_util", "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/memory", "@com_google_absl//absl/synchronization", "@com_google_absl//absl/time", "@com_google_googletest//:gtest", @@ -3463,3 +3599,18 @@ cc_binary( "@com_google_googletest//:gtest", ], ) + +cc_binary( + name = "proc_net_udp_test", + testonly = 1, + srcs = ["proc_net_udp.cc"], + linkstatic = 1, + deps = [ + ":ip_socket_test_util", + "//test/util:file_descriptor", + "//test/util:test_main", + "//test/util:test_util", + "@com_google_absl//absl/strings", + "@com_google_googletest//:gtest", + ], +) diff --git a/test/syscalls/linux/affinity.cc b/test/syscalls/linux/affinity.cc index f2d8375b6..128364c34 100644 --- a/test/syscalls/linux/affinity.cc +++ b/test/syscalls/linux/affinity.cc @@ -13,6 +13,7 @@ // limitations under the License. #include <sched.h> +#include <sys/syscall.h> #include <sys/types.h> #include <unistd.h> diff --git a/test/syscalls/linux/aio.cc b/test/syscalls/linux/aio.cc index 68dc05417..b27d4e10a 100644 --- a/test/syscalls/linux/aio.cc +++ b/test/syscalls/linux/aio.cc @@ -14,31 +14,57 @@ #include <fcntl.h> #include <linux/aio_abi.h> -#include <string.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/types.h> #include <unistd.h> +#include <algorithm> +#include <string> + #include "gtest/gtest.h" #include "test/syscalls/linux/file_base.h" #include "test/util/cleanup.h" #include "test/util/file_descriptor.h" +#include "test/util/fs_util.h" +#include "test/util/memory_util.h" +#include "test/util/posix_error.h" +#include "test/util/proc_util.h" #include "test/util/temp_path.h" #include "test/util/test_util.h" +using ::testing::_; + namespace gvisor { namespace testing { namespace { +// Returns the size of the VMA containing the given address. +PosixErrorOr<size_t> VmaSizeAt(uintptr_t addr) { + ASSIGN_OR_RETURN_ERRNO(std::string proc_self_maps, + GetContents("/proc/self/maps")); + ASSIGN_OR_RETURN_ERRNO(auto entries, ParseProcMaps(proc_self_maps)); + // Use binary search to find the first VMA that might contain addr. + ProcMapsEntry target = {}; + target.end = addr; + auto it = + std::upper_bound(entries.begin(), entries.end(), target, + [](const ProcMapsEntry& x, const ProcMapsEntry& y) { + return x.end < y.end; + }); + // Check that it actually contains addr. + if (it == entries.end() || addr < it->start) { + return PosixError(ENOENT, absl::StrCat("no VMA contains address ", addr)); + } + return it->end - it->start; +} + constexpr char kData[] = "hello world!"; int SubmitCtx(aio_context_t ctx, long nr, struct iocb** iocbpp) { return syscall(__NR_io_submit, ctx, nr, iocbpp); } -} // namespace - class AIOTest : public FileTest { public: AIOTest() : ctx_(0) {} @@ -124,10 +150,10 @@ TEST_F(AIOTest, BasicWrite) { EXPECT_EQ(events[0].res, strlen(kData)); // Verify that the file contains the contents. - char verify_buf[32] = {}; - ASSERT_THAT(read(test_file_fd_.get(), &verify_buf[0], strlen(kData)), - SyscallSucceeds()); - EXPECT_EQ(strcmp(kData, &verify_buf[0]), 0); + char verify_buf[sizeof(kData)] = {}; + ASSERT_THAT(read(test_file_fd_.get(), verify_buf, sizeof(kData)), + SyscallSucceedsWithValue(strlen(kData))); + EXPECT_STREQ(verify_buf, kData); } TEST_F(AIOTest, BadWrite) { @@ -220,38 +246,25 @@ TEST_F(AIOTest, CloneVm) { TEST_F(AIOTest, Mremap) { // Setup a context that is 128 entries deep. ASSERT_THAT(SetupContext(128), SyscallSucceeds()); + const size_t ctx_size = + ASSERT_NO_ERRNO_AND_VALUE(VmaSizeAt(reinterpret_cast<uintptr_t>(ctx_))); struct iocb cb = CreateCallback(); struct iocb* cbs[1] = {&cb}; // Reserve address space for the mremap target so we have something safe to // map over. - // - // N.B. We reserve 2 pages because we'll attempt to remap to 2 pages below. - // That should fail with EFAULT, but will fail with EINVAL if this mmap - // returns the page immediately below ctx_, as - // [new_address, new_address+2*kPageSize) overlaps [ctx_, ctx_+kPageSize). - void* new_address = mmap(nullptr, 2 * kPageSize, PROT_READ, - MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - ASSERT_THAT(reinterpret_cast<intptr_t>(new_address), SyscallSucceeds()); - auto mmap_cleanup = Cleanup([new_address] { - EXPECT_THAT(munmap(new_address, 2 * kPageSize), SyscallSucceeds()); - }); - - // Test that remapping to a larger address fails. - void* res = mremap(reinterpret_cast<void*>(ctx_), kPageSize, 2 * kPageSize, - MREMAP_FIXED | MREMAP_MAYMOVE, new_address); - ASSERT_THAT(reinterpret_cast<intptr_t>(res), SyscallFailsWithErrno(EFAULT)); + Mapping dst = + ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(ctx_size, PROT_READ, MAP_PRIVATE)); // Remap context 'handle' to a different address. - res = mremap(reinterpret_cast<void*>(ctx_), kPageSize, kPageSize, - MREMAP_FIXED | MREMAP_MAYMOVE, new_address); - ASSERT_THAT( - reinterpret_cast<intptr_t>(res), - SyscallSucceedsWithValue(reinterpret_cast<intptr_t>(new_address))); - mmap_cleanup.Release(); + ASSERT_THAT(Mremap(reinterpret_cast<void*>(ctx_), ctx_size, dst.len(), + MREMAP_FIXED | MREMAP_MAYMOVE, dst.ptr()), + IsPosixErrorOkAndHolds(dst.ptr())); aio_context_t old_ctx = ctx_; - ctx_ = reinterpret_cast<aio_context_t>(new_address); + ctx_ = reinterpret_cast<aio_context_t>(dst.addr()); + // io_destroy() will unmap dst now. + dst.release(); // Check that submitting the request with the old 'ctx_' fails. ASSERT_THAT(SubmitCtx(old_ctx, 1, cbs), SyscallFailsWithErrno(EINVAL)); @@ -260,18 +273,12 @@ TEST_F(AIOTest, Mremap) { ASSERT_THAT(Submit(1, cbs), SyscallSucceedsWithValue(1)); // Remap again. - new_address = - mmap(nullptr, kPageSize, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - ASSERT_THAT(reinterpret_cast<int64_t>(new_address), SyscallSucceeds()); - auto mmap_cleanup2 = Cleanup([new_address] { - EXPECT_THAT(munmap(new_address, kPageSize), SyscallSucceeds()); - }); - res = mremap(reinterpret_cast<void*>(ctx_), kPageSize, kPageSize, - MREMAP_FIXED | MREMAP_MAYMOVE, new_address); - ASSERT_THAT(reinterpret_cast<int64_t>(res), - SyscallSucceedsWithValue(reinterpret_cast<int64_t>(new_address))); - mmap_cleanup2.Release(); - ctx_ = reinterpret_cast<aio_context_t>(new_address); + dst = ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(ctx_size, PROT_READ, MAP_PRIVATE)); + ASSERT_THAT(Mremap(reinterpret_cast<void*>(ctx_), ctx_size, dst.len(), + MREMAP_FIXED | MREMAP_MAYMOVE, dst.ptr()), + IsPosixErrorOkAndHolds(dst.ptr())); + ctx_ = reinterpret_cast<aio_context_t>(dst.addr()); + dst.release(); // Get the reply with yet another 'ctx_' and verify it. struct io_event events[1]; @@ -281,51 +288,33 @@ TEST_F(AIOTest, Mremap) { EXPECT_EQ(events[0].res, strlen(kData)); // Verify that the file contains the contents. - char verify_buf[32] = {}; - ASSERT_THAT(read(test_file_fd_.get(), &verify_buf[0], strlen(kData)), - SyscallSucceeds()); - EXPECT_EQ(strcmp(kData, &verify_buf[0]), 0); + char verify_buf[sizeof(kData)] = {}; + ASSERT_THAT(read(test_file_fd_.get(), verify_buf, sizeof(kData)), + SyscallSucceedsWithValue(strlen(kData))); + EXPECT_STREQ(verify_buf, kData); } -// Tests that AIO context can be replaced with a different mapping at the same -// address and continue working. Don't ask why, but Linux allows it. -TEST_F(AIOTest, MremapOver) { +// Tests that AIO context cannot be expanded with mremap. +TEST_F(AIOTest, MremapExpansion) { // Setup a context that is 128 entries deep. ASSERT_THAT(SetupContext(128), SyscallSucceeds()); + const size_t ctx_size = + ASSERT_NO_ERRNO_AND_VALUE(VmaSizeAt(reinterpret_cast<uintptr_t>(ctx_))); - struct iocb cb = CreateCallback(); - struct iocb* cbs[1] = {&cb}; - - ASSERT_THAT(Submit(1, cbs), SyscallSucceedsWithValue(1)); - - // Allocate a new VMA, copy 'ctx_' content over, and remap it on top - // of 'ctx_'. - void* new_address = mmap(nullptr, kPageSize, PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); - ASSERT_THAT(reinterpret_cast<int64_t>(new_address), SyscallSucceeds()); - auto mmap_cleanup = Cleanup([new_address] { - EXPECT_THAT(munmap(new_address, kPageSize), SyscallSucceeds()); - }); - - memcpy(new_address, reinterpret_cast<void*>(ctx_), kPageSize); - void* res = - mremap(new_address, kPageSize, kPageSize, MREMAP_FIXED | MREMAP_MAYMOVE, - reinterpret_cast<void*>(ctx_)); - ASSERT_THAT(reinterpret_cast<int64_t>(res), SyscallSucceedsWithValue(ctx_)); - mmap_cleanup.Release(); - - // Everything continues to work just fine. - struct io_event events[1]; - ASSERT_THAT(GetEvents(1, 1, events, nullptr), SyscallSucceedsWithValue(1)); - EXPECT_EQ(events[0].data, 0x123); - EXPECT_EQ(events[0].obj, reinterpret_cast<long>(&cb)); - EXPECT_EQ(events[0].res, strlen(kData)); - - // Verify that the file contains the contents. - char verify_buf[32] = {}; - ASSERT_THAT(read(test_file_fd_.get(), &verify_buf[0], strlen(kData)), - SyscallSucceeds()); - EXPECT_EQ(strcmp(kData, &verify_buf[0]), 0); + // Reserve address space for the mremap target so we have something safe to + // map over. + Mapping dst = ASSERT_NO_ERRNO_AND_VALUE( + MmapAnon(ctx_size + kPageSize, PROT_NONE, MAP_PRIVATE)); + + // Test that remapping to a larger address range fails. + ASSERT_THAT(Mremap(reinterpret_cast<void*>(ctx_), ctx_size, dst.len(), + MREMAP_FIXED | MREMAP_MAYMOVE, dst.ptr()), + PosixErrorIs(EFAULT, _)); + + // mm/mremap.c:sys_mremap() => mremap_to() does do_munmap() of the destination + // before it hits the VM_DONTEXPAND check in vma_to_resize(), so we should no + // longer munmap it (another thread may have created a mapping there). + dst.release(); } // Tests that AIO calls fail if context's address is inaccessible. @@ -429,5 +418,7 @@ TEST_P(AIOVectorizedParamTest, BadIOVecs) { INSTANTIATE_TEST_SUITE_P(BadIOVecs, AIOVectorizedParamTest, ::testing::Values(IOCB_CMD_PREADV, IOCB_CMD_PWRITEV)); +} // namespace + } // namespace testing } // namespace gvisor diff --git a/test/syscalls/linux/base_poll_test.h b/test/syscalls/linux/base_poll_test.h index 088831f9f..0d4a6701e 100644 --- a/test/syscalls/linux/base_poll_test.h +++ b/test/syscalls/linux/base_poll_test.h @@ -56,7 +56,7 @@ class TimerThread { private: mutable absl::Mutex mu_; - bool cancel_ GUARDED_BY(mu_) = false; + bool cancel_ ABSL_GUARDED_BY(mu_) = false; // Must be last to ensure that the destructor for the thread is run before // any other member of the object is destroyed. diff --git a/test/syscalls/linux/chown.cc b/test/syscalls/linux/chown.cc index 2e82f0b3a..7a28b674d 100644 --- a/test/syscalls/linux/chown.cc +++ b/test/syscalls/linux/chown.cc @@ -16,10 +16,12 @@ #include <grp.h> #include <sys/types.h> #include <unistd.h> + #include <vector> #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "absl/flags/flag.h" #include "absl/synchronization/notification.h" #include "test/util/capability_util.h" #include "test/util/file_descriptor.h" @@ -29,9 +31,9 @@ #include "test/util/test_util.h" #include "test/util/thread_util.h" -DEFINE_int32(scratch_uid1, 65534, "first scratch UID"); -DEFINE_int32(scratch_uid2, 65533, "second scratch UID"); -DEFINE_int32(scratch_gid, 65534, "first scratch GID"); +ABSL_FLAG(int32_t, scratch_uid1, 65534, "first scratch UID"); +ABSL_FLAG(int32_t, scratch_uid2, 65533, "second scratch UID"); +ABSL_FLAG(int32_t, scratch_gid, 65534, "first scratch GID"); namespace gvisor { namespace testing { @@ -100,10 +102,12 @@ TEST_P(ChownParamTest, ChownFilePermissionDenied) { // Change EUID and EGID. // // See note about POSIX below. - EXPECT_THAT(syscall(SYS_setresgid, -1, FLAGS_scratch_gid, -1), - SyscallSucceeds()); - EXPECT_THAT(syscall(SYS_setresuid, -1, FLAGS_scratch_uid1, -1), - SyscallSucceeds()); + EXPECT_THAT( + syscall(SYS_setresgid, -1, absl::GetFlag(FLAGS_scratch_gid), -1), + SyscallSucceeds()); + EXPECT_THAT( + syscall(SYS_setresuid, -1, absl::GetFlag(FLAGS_scratch_uid1), -1), + SyscallSucceeds()); EXPECT_THAT(GetParam()(file.path(), geteuid(), getegid()), PosixErrorIs(EPERM, ::testing::ContainsRegex("chown"))); @@ -125,8 +129,9 @@ TEST_P(ChownParamTest, ChownFileSucceedsAsRoot) { // setresuid syscall. However, we want this thread to have its own set of // credentials different from the parent process, so we use the raw // syscall. - EXPECT_THAT(syscall(SYS_setresuid, -1, FLAGS_scratch_uid2, -1), - SyscallSucceeds()); + EXPECT_THAT( + syscall(SYS_setresuid, -1, absl::GetFlag(FLAGS_scratch_uid2), -1), + SyscallSucceeds()); // Create file and immediately close it. FileDescriptor fd = @@ -143,12 +148,13 @@ TEST_P(ChownParamTest, ChownFileSucceedsAsRoot) { fileCreated.WaitForNotification(); // Set file's owners to someone different. - EXPECT_NO_ERRNO(GetParam()(filename, FLAGS_scratch_uid1, FLAGS_scratch_gid)); + EXPECT_NO_ERRNO(GetParam()(filename, absl::GetFlag(FLAGS_scratch_uid1), + absl::GetFlag(FLAGS_scratch_gid))); struct stat s; EXPECT_THAT(stat(filename.c_str(), &s), SyscallSucceeds()); - EXPECT_EQ(s.st_uid, FLAGS_scratch_uid1); - EXPECT_EQ(s.st_gid, FLAGS_scratch_gid); + EXPECT_EQ(s.st_uid, absl::GetFlag(FLAGS_scratch_uid1)); + EXPECT_EQ(s.st_gid, absl::GetFlag(FLAGS_scratch_gid)); fileChowned.Notify(); } diff --git a/test/syscalls/linux/clock_nanosleep.cc b/test/syscalls/linux/clock_nanosleep.cc index 52a69d230..b55cddc52 100644 --- a/test/syscalls/linux/clock_nanosleep.cc +++ b/test/syscalls/linux/clock_nanosleep.cc @@ -43,7 +43,7 @@ int sys_clock_nanosleep(clockid_t clkid, int flags, PosixErrorOr<absl::Time> GetTime(clockid_t clk) { struct timespec ts = {}; - int rc = clock_gettime(clk, &ts); + const int rc = clock_gettime(clk, &ts); MaybeSave(); if (rc < 0) { return PosixError(errno, "clock_gettime"); @@ -67,31 +67,32 @@ TEST_P(WallClockNanosleepTest, InvalidValues) { } TEST_P(WallClockNanosleepTest, SleepOneSecond) { - absl::Duration const duration = absl::Seconds(1); - struct timespec dur = absl::ToTimespec(duration); + constexpr absl::Duration kSleepDuration = absl::Seconds(1); + struct timespec duration = absl::ToTimespec(kSleepDuration); - absl::Time const before = ASSERT_NO_ERRNO_AND_VALUE(GetTime(GetParam())); - EXPECT_THAT(RetryEINTR(sys_clock_nanosleep)(GetParam(), 0, &dur, &dur), - SyscallSucceeds()); - absl::Time const after = ASSERT_NO_ERRNO_AND_VALUE(GetTime(GetParam())); + const absl::Time before = ASSERT_NO_ERRNO_AND_VALUE(GetTime(GetParam())); + EXPECT_THAT( + RetryEINTR(sys_clock_nanosleep)(GetParam(), 0, &duration, &duration), + SyscallSucceeds()); + const absl::Time after = ASSERT_NO_ERRNO_AND_VALUE(GetTime(GetParam())); - EXPECT_GE(after - before, duration); + EXPECT_GE(after - before, kSleepDuration); } TEST_P(WallClockNanosleepTest, InterruptedNanosleep) { - absl::Duration const duration = absl::Seconds(60); - struct timespec dur = absl::ToTimespec(duration); + constexpr absl::Duration kSleepDuration = absl::Seconds(60); + struct timespec duration = absl::ToTimespec(kSleepDuration); // Install no-op signal handler for SIGALRM. struct sigaction sa = {}; sigfillset(&sa.sa_mask); sa.sa_handler = +[](int signo) {}; - auto const cleanup_sa = + const auto cleanup_sa = ASSERT_NO_ERRNO_AND_VALUE(ScopedSigaction(SIGALRM, sa)); // Measure time since setting the alarm, since the alarm will interrupt the // sleep and hence determine how long we sleep. - absl::Time const before = ASSERT_NO_ERRNO_AND_VALUE(GetTime(GetParam())); + const absl::Time before = ASSERT_NO_ERRNO_AND_VALUE(GetTime(GetParam())); // Set an alarm to go off while sleeping. struct itimerval timer = {}; @@ -99,26 +100,51 @@ TEST_P(WallClockNanosleepTest, InterruptedNanosleep) { timer.it_value.tv_usec = 0; timer.it_interval.tv_sec = 1; timer.it_interval.tv_usec = 0; - auto const cleanup = + const auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(ScopedItimer(ITIMER_REAL, timer)); - EXPECT_THAT(sys_clock_nanosleep(GetParam(), 0, &dur, &dur), + EXPECT_THAT(sys_clock_nanosleep(GetParam(), 0, &duration, &duration), SyscallFailsWithErrno(EINTR)); - absl::Time const after = ASSERT_NO_ERRNO_AND_VALUE(GetTime(GetParam())); + const absl::Time after = ASSERT_NO_ERRNO_AND_VALUE(GetTime(GetParam())); - absl::Duration const remaining = absl::DurationFromTimespec(dur); - EXPECT_GE(after - before + remaining, duration); + // Remaining time updated. + const absl::Duration remaining = absl::DurationFromTimespec(duration); + EXPECT_GE(after - before + remaining, kSleepDuration); +} + +// Remaining time is *not* updated if nanosleep completes uninterrupted. +TEST_P(WallClockNanosleepTest, UninterruptedNanosleep) { + constexpr absl::Duration kSleepDuration = absl::Milliseconds(10); + const struct timespec duration = absl::ToTimespec(kSleepDuration); + + while (true) { + constexpr int kRemainingMagic = 42; + struct timespec remaining; + remaining.tv_sec = kRemainingMagic; + remaining.tv_nsec = kRemainingMagic; + + int ret = sys_clock_nanosleep(GetParam(), 0, &duration, &remaining); + if (ret == EINTR) { + // Retry from beginning. We want a single uninterrupted call. + continue; + } + + EXPECT_THAT(ret, SyscallSucceeds()); + EXPECT_EQ(remaining.tv_sec, kRemainingMagic); + EXPECT_EQ(remaining.tv_nsec, kRemainingMagic); + break; + } } TEST_P(WallClockNanosleepTest, SleepUntil) { - absl::Time const now = ASSERT_NO_ERRNO_AND_VALUE(GetTime(GetParam())); - absl::Time const until = now + absl::Seconds(2); - struct timespec ts = absl::ToTimespec(until); + const absl::Time now = ASSERT_NO_ERRNO_AND_VALUE(GetTime(GetParam())); + const absl::Time until = now + absl::Seconds(2); + const struct timespec ts = absl::ToTimespec(until); EXPECT_THAT( RetryEINTR(sys_clock_nanosleep)(GetParam(), TIMER_ABSTIME, &ts, nullptr), SyscallSucceeds()); - absl::Time const after = ASSERT_NO_ERRNO_AND_VALUE(GetTime(GetParam())); + const absl::Time after = ASSERT_NO_ERRNO_AND_VALUE(GetTime(GetParam())); EXPECT_GE(after, until); } @@ -127,8 +153,8 @@ INSTANTIATE_TEST_SUITE_P(Sleepers, WallClockNanosleepTest, ::testing::Values(CLOCK_REALTIME, CLOCK_MONOTONIC)); TEST(ClockNanosleepProcessTest, SleepFiveSeconds) { - absl::Duration const kDuration = absl::Seconds(5); - struct timespec dur = absl::ToTimespec(kDuration); + const absl::Duration kSleepDuration = absl::Seconds(5); + struct timespec duration = absl::ToTimespec(kSleepDuration); // Ensure that CLOCK_PROCESS_CPUTIME_ID advances. std::atomic<bool> done(false); @@ -136,16 +162,16 @@ TEST(ClockNanosleepProcessTest, SleepFiveSeconds) { while (!done.load()) { } }); - auto const cleanup_done = Cleanup([&] { done.store(true); }); + const auto cleanup_done = Cleanup([&] { done.store(true); }); - absl::Time const before = + const absl::Time before = ASSERT_NO_ERRNO_AND_VALUE(GetTime(CLOCK_PROCESS_CPUTIME_ID)); - EXPECT_THAT( - RetryEINTR(sys_clock_nanosleep)(CLOCK_PROCESS_CPUTIME_ID, 0, &dur, &dur), - SyscallSucceeds()); - absl::Time const after = + EXPECT_THAT(RetryEINTR(sys_clock_nanosleep)(CLOCK_PROCESS_CPUTIME_ID, 0, + &duration, &duration), + SyscallSucceeds()); + const absl::Time after = ASSERT_NO_ERRNO_AND_VALUE(GetTime(CLOCK_PROCESS_CPUTIME_ID)); - EXPECT_GE(after - before, kDuration); + EXPECT_GE(after - before, kSleepDuration); } } // namespace diff --git a/test/syscalls/linux/exec_binary.cc b/test/syscalls/linux/exec_binary.cc index 91b55015c..0a3931e5a 100644 --- a/test/syscalls/linux/exec_binary.cc +++ b/test/syscalls/linux/exec_binary.cc @@ -401,12 +401,17 @@ TEST(ElfTest, DataSegment) { }))); } -// Additonal pages beyond filesz are always RW. +// Additonal pages beyond filesz honor (only) execute protections. // -// N.B. Linux uses set_brk -> vm_brk to additional pages beyond filesz (even -// though start_brk itself will always be beyond memsz). As a result, the -// segment permissions don't apply; the mapping is always RW. +// N.B. Linux changed this in 4.11 (16e72e9b30986 "powerpc: do not make the +// entire heap executable"). Previously, extra pages were always RW. TEST(ElfTest, ExtraMemPages) { + // gVisor has the newer behavior. + if (!IsRunningOnGvisor()) { + auto version = ASSERT_NO_ERRNO_AND_VALUE(GetKernelVersion()); + SKIP_IF(version.major < 4 || (version.major == 4 && version.minor < 11)); + } + ElfBinary<64> elf = StandardElf(); // Create a standard ELF, but extend to 1.5 pages. The second page will be the @@ -415,7 +420,7 @@ TEST(ElfTest, ExtraMemPages) { decltype(elf)::ElfPhdr phdr = {}; phdr.p_type = PT_LOAD; - // RWX segment. The extra anon page will be RW anyways. + // RWX segment. The extra anon page will also be RWX. // // N.B. Linux uses clear_user to clear the end of the file-mapped page, which // respects the mapping protections. Thus if we map this RO with memsz > @@ -454,7 +459,7 @@ TEST(ElfTest, ExtraMemPages) { {0x41000, 0x42000, true, true, true, true, kPageSize, 0, 0, 0, file.path().c_str()}, // extra page from anon. - {0x42000, 0x43000, true, true, false, true, 0, 0, 0, 0, ""}, + {0x42000, 0x43000, true, true, true, true, 0, 0, 0, 0, ""}, }))); } @@ -469,7 +474,7 @@ TEST(ElfTest, AnonOnlySegment) { phdr.p_offset = 0; phdr.p_vaddr = 0x41000; phdr.p_filesz = 0; - phdr.p_memsz = kPageSize - 0xe8; + phdr.p_memsz = kPageSize; elf.phdrs.push_back(phdr); elf.UpdateOffsets(); @@ -854,6 +859,11 @@ TEST(ElfTest, ELFInterpreter) { // The first segment really needs to start at 0 for a normal PIE binary, and // thus includes the headers. uint64_t const offset = interpreter.phdrs[1].p_offset; + // N.B. Since Linux 4.10 (0036d1f7eb95b "binfmt_elf: fix calculations for bss + // padding"), Linux unconditionally zeroes the remainder of the highest mapped + // page in an interpreter, failing if the protections don't allow write. Thus + // we must mark this writeable. + interpreter.phdrs[1].p_flags = PF_R | PF_W | PF_X; interpreter.phdrs[1].p_offset = 0x0; interpreter.phdrs[1].p_vaddr = 0x0; interpreter.phdrs[1].p_filesz += offset; @@ -903,15 +913,15 @@ TEST(ElfTest, ELFInterpreter) { const uint64_t interp_load_addr = regs.rip & ~(kPageSize - 1); - EXPECT_THAT(child, - ContainsMappings(std::vector<ProcMapsEntry>({ - // Main binary - {0x40000, 0x41000, true, false, true, true, 0, 0, 0, 0, - binary_file.path().c_str()}, - // Interpreter - {interp_load_addr, interp_load_addr + 0x1000, true, false, - true, true, 0, 0, 0, 0, interpreter_file.path().c_str()}, - }))); + EXPECT_THAT( + child, ContainsMappings(std::vector<ProcMapsEntry>({ + // Main binary + {0x40000, 0x41000, true, false, true, true, 0, 0, 0, 0, + binary_file.path().c_str()}, + // Interpreter + {interp_load_addr, interp_load_addr + 0x1000, true, true, true, + true, 0, 0, 0, 0, interpreter_file.path().c_str()}, + }))); } // Test parameter to ElfInterpterStaticTest cases. The first item is a suffix to @@ -928,6 +938,8 @@ TEST_P(ElfInterpreterStaticTest, Test) { const int expected_errno = std::get<1>(GetParam()); ElfBinary<64> interpreter = StandardElf(); + // See comment in ElfTest.ELFInterpreter. + interpreter.phdrs[1].p_flags = PF_R | PF_W | PF_X; interpreter.UpdateOffsets(); TempPath interpreter_file = ASSERT_NO_ERRNO_AND_VALUE(CreateElfWith(interpreter)); @@ -957,7 +969,7 @@ TEST_P(ElfInterpreterStaticTest, Test) { EXPECT_THAT(child, ContainsMappings(std::vector<ProcMapsEntry>({ // Interpreter. - {0x40000, 0x41000, true, false, true, true, 0, 0, 0, + {0x40000, 0x41000, true, true, true, true, 0, 0, 0, 0, interpreter_file.path().c_str()}, }))); } @@ -1035,6 +1047,8 @@ TEST(ElfTest, ELFInterpreterRelative) { // The first segment really needs to start at 0 for a normal PIE binary, and // thus includes the headers. uint64_t const offset = interpreter.phdrs[1].p_offset; + // See comment in ElfTest.ELFInterpreter. + interpreter.phdrs[1].p_flags = PF_R | PF_W | PF_X; interpreter.phdrs[1].p_offset = 0x0; interpreter.phdrs[1].p_vaddr = 0x0; interpreter.phdrs[1].p_filesz += offset; @@ -1073,15 +1087,15 @@ TEST(ElfTest, ELFInterpreterRelative) { const uint64_t interp_load_addr = regs.rip & ~(kPageSize - 1); - EXPECT_THAT(child, - ContainsMappings(std::vector<ProcMapsEntry>({ - // Main binary - {0x40000, 0x41000, true, false, true, true, 0, 0, 0, 0, - binary_file.path().c_str()}, - // Interpreter - {interp_load_addr, interp_load_addr + 0x1000, true, false, - true, true, 0, 0, 0, 0, interpreter_file.path().c_str()}, - }))); + EXPECT_THAT( + child, ContainsMappings(std::vector<ProcMapsEntry>({ + // Main binary + {0x40000, 0x41000, true, false, true, true, 0, 0, 0, 0, + binary_file.path().c_str()}, + // Interpreter + {interp_load_addr, interp_load_addr + 0x1000, true, true, true, + true, 0, 0, 0, 0, interpreter_file.path().c_str()}, + }))); } // ELF interpreter architecture doesn't match the binary. @@ -1095,6 +1109,8 @@ TEST(ElfTest, ELFInterpreterWrongArch) { // The first segment really needs to start at 0 for a normal PIE binary, and // thus includes the headers. uint64_t const offset = interpreter.phdrs[1].p_offset; + // See comment in ElfTest.ELFInterpreter. + interpreter.phdrs[1].p_flags = PF_R | PF_W | PF_X; interpreter.phdrs[1].p_offset = 0x0; interpreter.phdrs[1].p_vaddr = 0x0; interpreter.phdrs[1].p_filesz += offset; @@ -1174,6 +1190,8 @@ TEST(ElfTest, ElfInterpreterNoExecute) { // The first segment really needs to start at 0 for a normal PIE binary, and // thus includes the headers. uint64_t const offset = interpreter.phdrs[1].p_offset; + // See comment in ElfTest.ELFInterpreter. + interpreter.phdrs[1].p_flags = PF_R | PF_W | PF_X; interpreter.phdrs[1].p_offset = 0x0; interpreter.phdrs[1].p_vaddr = 0x0; interpreter.phdrs[1].p_filesz += offset; diff --git a/test/syscalls/linux/fcntl.cc b/test/syscalls/linux/fcntl.cc index 2f8e7c9dd..8a45be12a 100644 --- a/test/syscalls/linux/fcntl.cc +++ b/test/syscalls/linux/fcntl.cc @@ -17,9 +17,12 @@ #include <syscall.h> #include <unistd.h> +#include <string> + #include "gtest/gtest.h" #include "absl/base/macros.h" #include "absl/base/port.h" +#include "absl/flags/flag.h" #include "absl/memory/memory.h" #include "absl/strings/str_cat.h" #include "absl/time/clock.h" @@ -33,18 +36,19 @@ #include "test/util/test_util.h" #include "test/util/timer_util.h" -DEFINE_string(child_setlock_on, "", - "Contains the path to try to set a file lock on."); -DEFINE_bool(child_setlock_write, false, - "Whether to set a writable lock (otherwise readable)"); -DEFINE_bool(blocking, false, - "Whether to set a blocking lock (otherwise non-blocking)."); -DEFINE_bool(retry_eintr, false, "Whether to retry in the subprocess on EINTR."); -DEFINE_uint64(child_setlock_start, 0, "The value of struct flock start"); -DEFINE_uint64(child_setlock_len, 0, "The value of struct flock len"); -DEFINE_int32(socket_fd, -1, - "A socket to use for communicating more state back " - "to the parent."); +ABSL_FLAG(std::string, child_setlock_on, "", + "Contains the path to try to set a file lock on."); +ABSL_FLAG(bool, child_setlock_write, false, + "Whether to set a writable lock (otherwise readable)"); +ABSL_FLAG(bool, blocking, false, + "Whether to set a blocking lock (otherwise non-blocking)."); +ABSL_FLAG(bool, retry_eintr, false, + "Whether to retry in the subprocess on EINTR."); +ABSL_FLAG(uint64_t, child_setlock_start, 0, "The value of struct flock start"); +ABSL_FLAG(uint64_t, child_setlock_len, 0, "The value of struct flock len"); +ABSL_FLAG(int32_t, socket_fd, -1, + "A socket to use for communicating more state back " + "to the parent."); namespace gvisor { namespace testing { @@ -918,25 +922,26 @@ TEST(FcntlTest, GetOwn) { int main(int argc, char** argv) { gvisor::testing::TestInit(&argc, &argv); - if (!FLAGS_child_setlock_on.empty()) { - int socket_fd = FLAGS_socket_fd; - int fd = open(FLAGS_child_setlock_on.c_str(), O_RDWR, 0666); + const std::string setlock_on = absl::GetFlag(FLAGS_child_setlock_on); + if (!setlock_on.empty()) { + int socket_fd = absl::GetFlag(FLAGS_socket_fd); + int fd = open(setlock_on.c_str(), O_RDWR, 0666); if (fd == -1 && errno != 0) { int err = errno; - std::cerr << "CHILD open " << FLAGS_child_setlock_on << " failed " << err + std::cerr << "CHILD open " << setlock_on << " failed " << err << std::endl; exit(err); } struct flock fl; - if (FLAGS_child_setlock_write) { + if (absl::GetFlag(FLAGS_child_setlock_write)) { fl.l_type = F_WRLCK; } else { fl.l_type = F_RDLCK; } fl.l_whence = SEEK_SET; - fl.l_start = FLAGS_child_setlock_start; - fl.l_len = FLAGS_child_setlock_len; + fl.l_start = absl::GetFlag(FLAGS_child_setlock_start); + fl.l_len = absl::GetFlag(FLAGS_child_setlock_len); // Test the fcntl, no need to log, the error is unambiguously // from fcntl at this point. @@ -946,8 +951,8 @@ int main(int argc, char** argv) { gvisor::testing::MonotonicTimer timer; timer.Start(); do { - ret = fcntl(fd, FLAGS_blocking ? F_SETLKW : F_SETLK, &fl); - } while (FLAGS_retry_eintr && ret == -1 && errno == EINTR); + ret = fcntl(fd, absl::GetFlag(FLAGS_blocking) ? F_SETLKW : F_SETLK, &fl); + } while (absl::GetFlag(FLAGS_retry_eintr) && ret == -1 && errno == EINTR); auto usec = absl::ToInt64Microseconds(timer.Duration()); if (ret == -1 && errno != 0) { diff --git a/test/syscalls/linux/futex.cc b/test/syscalls/linux/futex.cc index d2cbbdb49..d3e3f998c 100644 --- a/test/syscalls/linux/futex.cc +++ b/test/syscalls/linux/futex.cc @@ -125,6 +125,10 @@ int futex_lock_pi(bool priv, std::atomic<int>* uaddr) { if (priv) { op |= FUTEX_PRIVATE_FLAG; } + int zero = 0; + if (uaddr->compare_exchange_strong(zero, gettid())) { + return 0; + } return RetryEINTR(syscall)(SYS_futex, uaddr, op, nullptr, nullptr); } @@ -133,6 +137,10 @@ int futex_trylock_pi(bool priv, std::atomic<int>* uaddr) { if (priv) { op |= FUTEX_PRIVATE_FLAG; } + int zero = 0; + if (uaddr->compare_exchange_strong(zero, gettid())) { + return 0; + } return RetryEINTR(syscall)(SYS_futex, uaddr, op, nullptr, nullptr); } @@ -141,6 +149,10 @@ int futex_unlock_pi(bool priv, std::atomic<int>* uaddr) { if (priv) { op |= FUTEX_PRIVATE_FLAG; } + int tid = gettid(); + if (uaddr->compare_exchange_strong(tid, 0)) { + return 0; + } return RetryEINTR(syscall)(SYS_futex, uaddr, op, nullptr, nullptr); } @@ -692,8 +704,8 @@ TEST_P(PrivateAndSharedFutexTest, PITryLockConcurrency_NoRandomSave) { std::unique_ptr<ScopedThread> threads[10]; for (size_t i = 0; i < ABSL_ARRAYSIZE(threads); ++i) { threads[i] = absl::make_unique<ScopedThread>([is_priv, &a] { - for (size_t j = 0; j < 100;) { - if (futex_trylock_pi(is_priv, &a) >= 0) { + for (size_t j = 0; j < 10;) { + if (futex_trylock_pi(is_priv, &a) == 0) { ++j; EXPECT_EQ(a.load() & FUTEX_TID_MASK, gettid()); SleepSafe(absl::Milliseconds(5)); diff --git a/test/syscalls/linux/ip_socket_test_util.cc b/test/syscalls/linux/ip_socket_test_util.cc index c73262e72..410b42a47 100644 --- a/test/syscalls/linux/ip_socket_test_util.cc +++ b/test/syscalls/linux/ip_socket_test_util.cc @@ -23,6 +23,16 @@ namespace gvisor { namespace testing { +uint32_t IPFromInetSockaddr(const struct sockaddr* addr) { + auto* in_addr = reinterpret_cast<const struct sockaddr_in*>(addr); + return in_addr->sin_addr.s_addr; +} + +uint16_t PortFromInetSockaddr(const struct sockaddr* addr) { + auto* in_addr = reinterpret_cast<const struct sockaddr_in*>(addr); + return ntohs(in_addr->sin_port); +} + PosixErrorOr<int> InterfaceIndex(std::string name) { // TODO(igudger): Consider using netlink. ifreq req = {}; diff --git a/test/syscalls/linux/ip_socket_test_util.h b/test/syscalls/linux/ip_socket_test_util.h index b498a053d..3d36b9620 100644 --- a/test/syscalls/linux/ip_socket_test_util.h +++ b/test/syscalls/linux/ip_socket_test_util.h @@ -26,6 +26,31 @@ namespace gvisor { namespace testing { +// Possible values of the "st" field in a /proc/net/{tcp,udp} entry. Source: +// Linux kernel, include/net/tcp_states.h. +enum { + TCP_ESTABLISHED = 1, + TCP_SYN_SENT, + TCP_SYN_RECV, + TCP_FIN_WAIT1, + TCP_FIN_WAIT2, + TCP_TIME_WAIT, + TCP_CLOSE, + TCP_CLOSE_WAIT, + TCP_LAST_ACK, + TCP_LISTEN, + TCP_CLOSING, + TCP_NEW_SYN_RECV, + + TCP_MAX_STATES +}; + +// Extracts the IP address from an inet sockaddr in network byte order. +uint32_t IPFromInetSockaddr(const struct sockaddr* addr); + +// Extracts the port from an inet sockaddr in host byte order. +uint16_t PortFromInetSockaddr(const struct sockaddr* addr); + // InterfaceIndex returns the index of the named interface. PosixErrorOr<int> InterfaceIndex(std::string name); diff --git a/test/syscalls/linux/itimer.cc b/test/syscalls/linux/itimer.cc index 51ce323b9..930d2b940 100644 --- a/test/syscalls/linux/itimer.cc +++ b/test/syscalls/linux/itimer.cc @@ -336,7 +336,9 @@ int main(int argc, char** argv) { } if (arg == gvisor::testing::kSIGPROFFairnessIdle) { MaskSIGPIPE(); - return gvisor::testing::TestSIGPROFFairness(absl::Milliseconds(10)); + // Sleep time > ClockTick (10ms) exercises sleeping gVisor's + // kernel.cpuClockTicker. + return gvisor::testing::TestSIGPROFFairness(absl::Milliseconds(25)); } } diff --git a/test/syscalls/linux/kill.cc b/test/syscalls/linux/kill.cc index 18ad923b8..db29bd59c 100644 --- a/test/syscalls/linux/kill.cc +++ b/test/syscalls/linux/kill.cc @@ -21,6 +21,7 @@ #include <csignal> #include "gtest/gtest.h" +#include "absl/flags/flag.h" #include "absl/synchronization/mutex.h" #include "absl/time/clock.h" #include "absl/time/time.h" @@ -31,8 +32,8 @@ #include "test/util/test_util.h" #include "test/util/thread_util.h" -DEFINE_int32(scratch_uid, 65534, "scratch UID"); -DEFINE_int32(scratch_gid, 65534, "scratch GID"); +ABSL_FLAG(int32_t, scratch_uid, 65534, "scratch UID"); +ABSL_FLAG(int32_t, scratch_gid, 65534, "scratch GID"); using ::testing::Ge; @@ -255,8 +256,8 @@ TEST(KillTest, ProcessGroups) { TEST(KillTest, ChildDropsPrivsCannotKill) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETUID))); - int uid = FLAGS_scratch_uid; - int gid = FLAGS_scratch_gid; + const int uid = absl::GetFlag(FLAGS_scratch_uid); + const int gid = absl::GetFlag(FLAGS_scratch_gid); // Create the child that drops privileges and tries to kill the parent. pid_t pid = fork(); @@ -331,8 +332,8 @@ TEST(KillTest, CanSIGCONTSameSession) { EXPECT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP) << "status " << status; - int uid = FLAGS_scratch_uid; - int gid = FLAGS_scratch_gid; + const int uid = absl::GetFlag(FLAGS_scratch_uid); + const int gid = absl::GetFlag(FLAGS_scratch_gid); // Drop privileges only in child process, or else this parent process won't be // able to open some log files after the test ends. diff --git a/test/syscalls/linux/link.cc b/test/syscalls/linux/link.cc index a91703070..dd5352954 100644 --- a/test/syscalls/linux/link.cc +++ b/test/syscalls/linux/link.cc @@ -22,6 +22,7 @@ #include <string> #include "gtest/gtest.h" +#include "absl/flags/flag.h" #include "absl/strings/str_cat.h" #include "test/util/capability_util.h" #include "test/util/file_descriptor.h" @@ -31,7 +32,7 @@ #include "test/util/test_util.h" #include "test/util/thread_util.h" -DEFINE_int32(scratch_uid, 65534, "scratch UID"); +ABSL_FLAG(int32_t, scratch_uid, 65534, "scratch UID"); namespace gvisor { namespace testing { @@ -92,7 +93,8 @@ TEST(LinkTest, PermissionDenied) { // threads have the same UIDs, so using the setuid wrapper sets all threads' // real UID. // Also drops capabilities. - EXPECT_THAT(syscall(SYS_setuid, FLAGS_scratch_uid), SyscallSucceeds()); + EXPECT_THAT(syscall(SYS_setuid, absl::GetFlag(FLAGS_scratch_uid)), + SyscallSucceeds()); EXPECT_THAT(link(oldfile.path().c_str(), newname.c_str()), SyscallFailsWithErrno(EPERM)); diff --git a/test/syscalls/linux/mlock.cc b/test/syscalls/linux/mlock.cc index aee4f7d1a..283c21ed3 100644 --- a/test/syscalls/linux/mlock.cc +++ b/test/syscalls/linux/mlock.cc @@ -169,26 +169,24 @@ TEST(MlockallTest, Future) { // Run this test in a separate (single-threaded) subprocess to ensure that a // background thread doesn't try to mmap a large amount of memory, fail due // to hitting RLIMIT_MEMLOCK, and explode the process violently. - EXPECT_THAT(InForkedProcess([] { - auto const mapping = - MmapAnon(kPageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE) - .ValueOrDie(); - TEST_CHECK(!IsPageMlocked(mapping.addr())); - TEST_PCHECK(mlockall(MCL_FUTURE) == 0); - // Ensure that mlockall(MCL_FUTURE) is turned off before the end - // of the test, as otherwise mmaps may fail unexpectedly. - Cleanup do_munlockall([] { TEST_PCHECK(munlockall() == 0); }); - auto const mapping2 = ASSERT_NO_ERRNO_AND_VALUE( - MmapAnon(kPageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE)); - TEST_CHECK(IsPageMlocked(mapping2.addr())); - // Fire munlockall() and check that it disables - // mlockall(MCL_FUTURE). - do_munlockall.Release()(); - auto const mapping3 = ASSERT_NO_ERRNO_AND_VALUE( - MmapAnon(kPageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE)); - TEST_CHECK(!IsPageMlocked(mapping2.addr())); - }), - IsPosixErrorOkAndHolds(0)); + auto const do_test = [] { + auto const mapping = + MmapAnon(kPageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE).ValueOrDie(); + TEST_CHECK(!IsPageMlocked(mapping.addr())); + TEST_PCHECK(mlockall(MCL_FUTURE) == 0); + // Ensure that mlockall(MCL_FUTURE) is turned off before the end of the + // test, as otherwise mmaps may fail unexpectedly. + Cleanup do_munlockall([] { TEST_PCHECK(munlockall() == 0); }); + auto const mapping2 = + MmapAnon(kPageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE).ValueOrDie(); + TEST_CHECK(IsPageMlocked(mapping2.addr())); + // Fire munlockall() and check that it disables mlockall(MCL_FUTURE). + do_munlockall.Release()(); + auto const mapping3 = + MmapAnon(kPageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE).ValueOrDie(); + TEST_CHECK(!IsPageMlocked(mapping2.addr())); + }; + EXPECT_THAT(InForkedProcess(do_test), IsPosixErrorOkAndHolds(0)); } TEST(MunlockallTest, Basic) { diff --git a/test/syscalls/linux/mremap.cc b/test/syscalls/linux/mremap.cc index 64e435cb7..f0e5f7d82 100644 --- a/test/syscalls/linux/mremap.cc +++ b/test/syscalls/linux/mremap.cc @@ -35,17 +35,6 @@ namespace testing { namespace { -// Wrapper for mremap that returns a PosixErrorOr<>, since the return type of -// void* isn't directly compatible with SyscallSucceeds. -PosixErrorOr<void*> Mremap(void* old_address, size_t old_size, size_t new_size, - int flags, void* new_address) { - void* rv = mremap(old_address, old_size, new_size, flags, new_address); - if (rv == MAP_FAILED) { - return PosixError(errno, "mremap failed"); - } - return rv; -} - // Fixture for mremap tests parameterized by mmap flags. using MremapParamTest = ::testing::TestWithParam<int>; diff --git a/test/syscalls/linux/open.cc b/test/syscalls/linux/open.cc index e0525f386..2b1df52ce 100644 --- a/test/syscalls/linux/open.cc +++ b/test/syscalls/linux/open.cc @@ -21,6 +21,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "absl/memory/memory.h" #include "test/syscalls/linux/file_base.h" #include "test/util/capability_util.h" #include "test/util/cleanup.h" diff --git a/test/syscalls/linux/packet_socket.cc b/test/syscalls/linux/packet_socket.cc index 7a3379b9e..37b4e6575 100644 --- a/test/syscalls/linux/packet_socket.cc +++ b/test/syscalls/linux/packet_socket.cc @@ -83,9 +83,15 @@ void SendUDPMessage(int sock) { // Send an IP packet and make sure ETH_P_<something else> doesn't pick it up. TEST(BasicCookedPacketTest, WrongType) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); + // (b/129292371): Remove once we support packet sockets. SKIP_IF(IsRunningOnGvisor()); + if (!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))) { + ASSERT_THAT(socket(AF_PACKET, SOCK_DGRAM, ETH_P_PUP), + SyscallFailsWithErrno(EPERM)); + GTEST_SKIP(); + } + FileDescriptor sock = ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_PACKET, SOCK_DGRAM, ETH_P_PUP)); @@ -118,18 +124,27 @@ class CookedPacketTest : public ::testing::TestWithParam<int> { }; void CookedPacketTest::SetUp() { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); + // (b/129292371): Remove once we support packet sockets. SKIP_IF(IsRunningOnGvisor()); + if (!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))) { + ASSERT_THAT(socket(AF_PACKET, SOCK_DGRAM, htons(GetParam())), + SyscallFailsWithErrno(EPERM)); + GTEST_SKIP(); + } + ASSERT_THAT(socket_ = socket(AF_PACKET, SOCK_DGRAM, htons(GetParam())), SyscallSucceeds()); } void CookedPacketTest::TearDown() { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); + // (b/129292371): Remove once we support packet sockets. SKIP_IF(IsRunningOnGvisor()); - EXPECT_THAT(close(socket_), SyscallSucceeds()); + // TearDown will be run even if we skip the test. + if (ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))) { + EXPECT_THAT(close(socket_), SyscallSucceeds()); + } } int CookedPacketTest::GetLoopbackIndex() { @@ -142,9 +157,6 @@ int CookedPacketTest::GetLoopbackIndex() { // Receive via a packet socket. TEST_P(CookedPacketTest, Receive) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - SKIP_IF(IsRunningOnGvisor()); - // Let's use a simple IP payload: a UDP datagram. FileDescriptor udp_sock = ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET, SOCK_DGRAM, 0)); @@ -201,9 +213,6 @@ TEST_P(CookedPacketTest, Receive) { // Send via a packet socket. TEST_P(CookedPacketTest, Send) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - SKIP_IF(IsRunningOnGvisor()); - // Let's send a UDP packet and receive it using a regular UDP socket. FileDescriptor udp_sock = ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET, SOCK_DGRAM, 0)); diff --git a/test/syscalls/linux/packet_socket_raw.cc b/test/syscalls/linux/packet_socket_raw.cc index 9e96460ee..6491453b6 100644 --- a/test/syscalls/linux/packet_socket_raw.cc +++ b/test/syscalls/linux/packet_socket_raw.cc @@ -97,9 +97,15 @@ class RawPacketTest : public ::testing::TestWithParam<int> { }; void RawPacketTest::SetUp() { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); + // (b/129292371): Remove once we support packet sockets. SKIP_IF(IsRunningOnGvisor()); + if (!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))) { + ASSERT_THAT(socket(AF_PACKET, SOCK_RAW, htons(GetParam())), + SyscallFailsWithErrno(EPERM)); + GTEST_SKIP(); + } + if (!IsRunningOnGvisor()) { FileDescriptor acceptLocal = ASSERT_NO_ERRNO_AND_VALUE( Open("/proc/sys/net/ipv4/conf/lo/accept_local", O_RDONLY)); @@ -119,10 +125,13 @@ void RawPacketTest::SetUp() { } void RawPacketTest::TearDown() { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); + // (b/129292371): Remove once we support packet sockets. SKIP_IF(IsRunningOnGvisor()); - EXPECT_THAT(close(socket_), SyscallSucceeds()); + // TearDown will be run even if we skip the test. + if (ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))) { + EXPECT_THAT(close(socket_), SyscallSucceeds()); + } } int RawPacketTest::GetLoopbackIndex() { @@ -135,9 +144,6 @@ int RawPacketTest::GetLoopbackIndex() { // Receive via a packet socket. TEST_P(RawPacketTest, Receive) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - SKIP_IF(IsRunningOnGvisor()); - // Let's use a simple IP payload: a UDP datagram. FileDescriptor udp_sock = ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET, SOCK_DGRAM, 0)); @@ -208,9 +214,6 @@ TEST_P(RawPacketTest, Receive) { // Send via a packet socket. TEST_P(RawPacketTest, Send) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - SKIP_IF(IsRunningOnGvisor()); - // Let's send a UDP packet and receive it using a regular UDP socket. FileDescriptor udp_sock = ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET, SOCK_DGRAM, 0)); diff --git a/test/syscalls/linux/pipe.cc b/test/syscalls/linux/pipe.cc index 65afb90f3..10e2a6dfc 100644 --- a/test/syscalls/linux/pipe.cc +++ b/test/syscalls/linux/pipe.cc @@ -168,6 +168,20 @@ TEST_P(PipeTest, Write) { EXPECT_EQ(wbuf, rbuf); } +TEST_P(PipeTest, WritePage) { + SKIP_IF(!CreateBlocking()); + + std::vector<char> wbuf(kPageSize); + RandomizeBuffer(wbuf.data(), wbuf.size()); + std::vector<char> rbuf(wbuf.size()); + + ASSERT_THAT(write(wfd_.get(), wbuf.data(), wbuf.size()), + SyscallSucceedsWithValue(wbuf.size())); + ASSERT_THAT(read(rfd_.get(), rbuf.data(), rbuf.size()), + SyscallSucceedsWithValue(rbuf.size())); + EXPECT_EQ(memcmp(rbuf.data(), wbuf.data(), wbuf.size()), 0); +} + TEST_P(PipeTest, NonBlocking) { SKIP_IF(!CreateNonBlocking()); diff --git a/test/syscalls/linux/prctl.cc b/test/syscalls/linux/prctl.cc index bd1779557..d07571a5f 100644 --- a/test/syscalls/linux/prctl.cc +++ b/test/syscalls/linux/prctl.cc @@ -21,6 +21,7 @@ #include <string> #include "gtest/gtest.h" +#include "absl/flags/flag.h" #include "test/util/capability_util.h" #include "test/util/cleanup.h" #include "test/util/multiprocess_util.h" @@ -28,9 +29,9 @@ #include "test/util/test_util.h" #include "test/util/thread_util.h" -DEFINE_bool(prctl_no_new_privs_test_child, false, - "If true, exit with the return value of prctl(PR_GET_NO_NEW_PRIVS) " - "plus an offset (see test source)."); +ABSL_FLAG(bool, prctl_no_new_privs_test_child, false, + "If true, exit with the return value of prctl(PR_GET_NO_NEW_PRIVS) " + "plus an offset (see test source)."); namespace gvisor { namespace testing { @@ -220,7 +221,7 @@ TEST(PrctlTest, RootDumpability) { int main(int argc, char** argv) { gvisor::testing::TestInit(&argc, &argv); - if (FLAGS_prctl_no_new_privs_test_child) { + if (absl::GetFlag(FLAGS_prctl_no_new_privs_test_child)) { exit(gvisor::testing::kPrctlNoNewPrivsTestChildExitBase + prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0)); } diff --git a/test/syscalls/linux/prctl_setuid.cc b/test/syscalls/linux/prctl_setuid.cc index 00dd6523e..30f0d75b3 100644 --- a/test/syscalls/linux/prctl_setuid.cc +++ b/test/syscalls/linux/prctl_setuid.cc @@ -14,9 +14,11 @@ #include <sched.h> #include <sys/prctl.h> + #include <string> #include "gtest/gtest.h" +#include "absl/flags/flag.h" #include "test/util/capability_util.h" #include "test/util/logging.h" #include "test/util/multiprocess_util.h" @@ -24,12 +26,12 @@ #include "test/util/test_util.h" #include "test/util/thread_util.h" -DEFINE_int32(scratch_uid, 65534, "scratch UID"); +ABSL_FLAG(int32_t, scratch_uid, 65534, "scratch UID"); // This flag is used to verify that after an exec PR_GET_KEEPCAPS // returns 0, the return code will be offset by kPrGetKeepCapsExitBase. -DEFINE_bool(prctl_pr_get_keepcaps, false, - "If true the test will verify that prctl with pr_get_keepcaps" - "returns 0. The test will exit with the result of that check."); +ABSL_FLAG(bool, prctl_pr_get_keepcaps, false, + "If true the test will verify that prctl with pr_get_keepcaps" + "returns 0. The test will exit with the result of that check."); // These tests exist seperately from prctl because we need to start // them as root. Setuid() has the behavior that permissions are fully @@ -113,10 +115,12 @@ TEST_F(PrctlKeepCapsSetuidTest, SetUidNoKeepCaps) { // call to only apply to this task. POSIX threads, however, require that // all threads have the same UIDs, so using the setuid wrapper sets all // threads' real UID. - EXPECT_THAT(syscall(SYS_setuid, FLAGS_scratch_uid), SyscallSucceeds()); + EXPECT_THAT(syscall(SYS_setuid, absl::GetFlag(FLAGS_scratch_uid)), + SyscallSucceeds()); // Verify that we changed uid. - EXPECT_THAT(getuid(), SyscallSucceedsWithValue(FLAGS_scratch_uid)); + EXPECT_THAT(getuid(), + SyscallSucceedsWithValue(absl::GetFlag(FLAGS_scratch_uid))); // Verify we lost the capability in the effective set, this always happens. TEST_CHECK(!HaveCapability(CAP_SYS_ADMIN).ValueOrDie()); @@ -157,10 +161,12 @@ TEST_F(PrctlKeepCapsSetuidTest, SetUidKeepCaps) { // call to only apply to this task. POSIX threads, however, require that // all threads have the same UIDs, so using the setuid wrapper sets all // threads' real UID. - EXPECT_THAT(syscall(SYS_setuid, FLAGS_scratch_uid), SyscallSucceeds()); + EXPECT_THAT(syscall(SYS_setuid, absl::GetFlag(FLAGS_scratch_uid)), + SyscallSucceeds()); // Verify that we changed uid. - EXPECT_THAT(getuid(), SyscallSucceedsWithValue(FLAGS_scratch_uid)); + EXPECT_THAT(getuid(), + SyscallSucceedsWithValue(absl::GetFlag(FLAGS_scratch_uid))); // Verify we lost the capability in the effective set, this always happens. TEST_CHECK(!HaveCapability(CAP_SYS_ADMIN).ValueOrDie()); @@ -253,7 +259,7 @@ TEST_F(PrctlKeepCapsSetuidTest, PrGetKeepCaps) { int main(int argc, char** argv) { gvisor::testing::TestInit(&argc, &argv); - if (FLAGS_prctl_pr_get_keepcaps) { + if (absl::GetFlag(FLAGS_prctl_pr_get_keepcaps)) { return gvisor::testing::kPrGetKeepCapsExitBase + prctl(PR_GET_KEEPCAPS, 0, 0, 0, 0); } diff --git a/test/syscalls/linux/proc.cc b/test/syscalls/linux/proc.cc index b440ba0df..e4c030bbb 100644 --- a/test/syscalls/linux/proc.cc +++ b/test/syscalls/linux/proc.cc @@ -440,6 +440,11 @@ TEST(ProcSelfAuxv, EntryPresence) { EXPECT_EQ(auxv_entries.count(AT_PHENT), 1); EXPECT_EQ(auxv_entries.count(AT_PHNUM), 1); EXPECT_EQ(auxv_entries.count(AT_BASE), 1); + EXPECT_EQ(auxv_entries.count(AT_UID), 1); + EXPECT_EQ(auxv_entries.count(AT_EUID), 1); + EXPECT_EQ(auxv_entries.count(AT_GID), 1); + EXPECT_EQ(auxv_entries.count(AT_EGID), 1); + EXPECT_EQ(auxv_entries.count(AT_SECURE), 1); EXPECT_EQ(auxv_entries.count(AT_CLKTCK), 1); EXPECT_EQ(auxv_entries.count(AT_RANDOM), 1); EXPECT_EQ(auxv_entries.count(AT_EXECFN), 1); @@ -1602,9 +1607,9 @@ class BlockingChild { } mutable absl::Mutex mu_; - bool stop_ GUARDED_BY(mu_) = false; + bool stop_ ABSL_GUARDED_BY(mu_) = false; pid_t tid_; - bool tid_ready_ GUARDED_BY(mu_) = false; + bool tid_ready_ ABSL_GUARDED_BY(mu_) = false; // Must be last to ensure that the destructor for the thread is run before // any other member of the object is destroyed. @@ -1882,7 +1887,9 @@ void CheckDuplicatesRecursively(std::string path) { errno = 0; DIR* dir = opendir(path.c_str()); if (dir == nullptr) { - ASSERT_THAT(errno, ::testing::AnyOf(EPERM, EACCES)) << path; + // Ignore any directories we can't read or missing directories as the + // directory could have been deleted/mutated from the time the parent + // directory contents were read. return; } auto dir_closer = Cleanup([&dir]() { closedir(dir); }); diff --git a/test/syscalls/linux/proc_net.cc b/test/syscalls/linux/proc_net.cc index 03d0665eb..efdaf202b 100644 --- a/test/syscalls/linux/proc_net.cc +++ b/test/syscalls/linux/proc_net.cc @@ -14,6 +14,7 @@ #include "gtest/gtest.h" #include "gtest/gtest.h" +#include "test/util/capability_util.h" #include "test/util/file_descriptor.h" #include "test/util/fs_util.h" #include "test/util/test_util.h" @@ -27,7 +28,7 @@ TEST(ProcNetIfInet6, Format) { EXPECT_THAT(ifinet6, ::testing::MatchesRegex( // Ex: "00000000000000000000000000000001 01 80 10 80 lo\n" - "^([a-f\\d]{32}( [a-f\\d]{2}){4} +[a-z][a-z\\d]*\\n)+$")); + "^([a-f0-9]{32}( [a-f0-9]{2}){4} +[a-z][a-z0-9]*\n)+$")); } TEST(ProcSysNetIpv4Sack, Exists) { @@ -35,6 +36,8 @@ TEST(ProcSysNetIpv4Sack, Exists) { } TEST(ProcSysNetIpv4Sack, CanReadAndWrite) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability((CAP_DAC_OVERRIDE)))); + auto const fd = ASSERT_NO_ERRNO_AND_VALUE(Open("/proc/sys/net/ipv4/tcp_sack", O_RDWR)); diff --git a/test/syscalls/linux/proc_net_tcp.cc b/test/syscalls/linux/proc_net_tcp.cc index 498f62d9c..f6d7ad0bb 100644 --- a/test/syscalls/linux/proc_net_tcp.cc +++ b/test/syscalls/linux/proc_net_tcp.cc @@ -38,25 +38,6 @@ constexpr char kProcNetTCPHeader[] = "retrnsmt uid timeout inode " " "; -// Possible values of the "st" field in a /proc/net/tcp entry. Source: Linux -// kernel, include/net/tcp_states.h. -enum { - TCP_ESTABLISHED = 1, - TCP_SYN_SENT, - TCP_SYN_RECV, - TCP_FIN_WAIT1, - TCP_FIN_WAIT2, - TCP_TIME_WAIT, - TCP_CLOSE, - TCP_CLOSE_WAIT, - TCP_LAST_ACK, - TCP_LISTEN, - TCP_CLOSING, - TCP_NEW_SYN_RECV, - - TCP_MAX_STATES -}; - // TCPEntry represents a single entry from /proc/net/tcp. struct TCPEntry { uint32_t local_addr; @@ -70,42 +51,35 @@ struct TCPEntry { uint64_t inode; }; -uint32_t IP(const struct sockaddr* addr) { - auto* in_addr = reinterpret_cast<const struct sockaddr_in*>(addr); - return in_addr->sin_addr.s_addr; -} - -uint16_t Port(const struct sockaddr* addr) { - auto* in_addr = reinterpret_cast<const struct sockaddr_in*>(addr); - return ntohs(in_addr->sin_port); -} - // Finds the first entry in 'entries' for which 'predicate' returns true. -// Returns true on match, and sets 'match' to point to the matching entry. -bool FindBy(std::vector<TCPEntry> entries, TCPEntry* match, +// Returns true on match, and sets 'match' to a copy of the matching entry. If +// 'match' is null, it's ignored. +bool FindBy(const std::vector<TCPEntry>& entries, TCPEntry* match, std::function<bool(const TCPEntry&)> predicate) { - for (int i = 0; i < entries.size(); ++i) { - if (predicate(entries[i])) { - *match = entries[i]; + for (const TCPEntry& entry : entries) { + if (predicate(entry)) { + if (match != nullptr) { + *match = entry; + } return true; } } return false; } -bool FindByLocalAddr(std::vector<TCPEntry> entries, TCPEntry* match, +bool FindByLocalAddr(const std::vector<TCPEntry>& entries, TCPEntry* match, const struct sockaddr* addr) { - uint32_t host = IP(addr); - uint16_t port = Port(addr); + uint32_t host = IPFromInetSockaddr(addr); + uint16_t port = PortFromInetSockaddr(addr); return FindBy(entries, match, [host, port](const TCPEntry& e) { return (e.local_addr == host && e.local_port == port); }); } -bool FindByRemoteAddr(std::vector<TCPEntry> entries, TCPEntry* match, +bool FindByRemoteAddr(const std::vector<TCPEntry>& entries, TCPEntry* match, const struct sockaddr* addr) { - uint32_t host = IP(addr); - uint16_t port = Port(addr); + uint32_t host = IPFromInetSockaddr(addr); + uint16_t port = PortFromInetSockaddr(addr); return FindBy(entries, match, [host, port](const TCPEntry& e) { return (e.remote_addr == host && e.remote_port == port); }); @@ -120,7 +94,7 @@ PosixErrorOr<std::vector<TCPEntry>> ProcNetTCPEntries() { std::vector<TCPEntry> entries; std::vector<std::string> lines = StrSplit(content, '\n'); std::cerr << "<contents of /proc/net/tcp>" << std::endl; - for (std::string line : lines) { + for (const std::string& line : lines) { std::cerr << line << std::endl; if (!found_header) { @@ -204,9 +178,8 @@ TEST(ProcNetTCP, BindAcceptConnect) { EXPECT_EQ(entries.size(), 2); } - TCPEntry e; - EXPECT_TRUE(FindByLocalAddr(entries, &e, sockets->first_addr())); - EXPECT_TRUE(FindByRemoteAddr(entries, &e, sockets->first_addr())); + EXPECT_TRUE(FindByLocalAddr(entries, nullptr, sockets->first_addr())); + EXPECT_TRUE(FindByRemoteAddr(entries, nullptr, sockets->first_addr())); } TEST(ProcNetTCP, InodeReasonable) { @@ -261,8 +234,8 @@ TEST(ProcNetTCP, State) { FileDescriptor accepted = ASSERT_NO_ERRNO_AND_VALUE(Accept(server->get(), nullptr, nullptr)); - const uint32_t accepted_local_host = IP(&addr); - const uint16_t accepted_local_port = Port(&addr); + const uint32_t accepted_local_host = IPFromInetSockaddr(&addr); + const uint16_t accepted_local_port = PortFromInetSockaddr(&addr); entries = ASSERT_NO_ERRNO_AND_VALUE(ProcNetTCPEntries()); TCPEntry accepted_entry; diff --git a/test/syscalls/linux/proc_net_udp.cc b/test/syscalls/linux/proc_net_udp.cc new file mode 100644 index 000000000..369df8e0e --- /dev/null +++ b/test/syscalls/linux/proc_net_udp.cc @@ -0,0 +1,309 @@ +// Copyright 2019 Google LLC +// +// 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. + +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "gtest/gtest.h" +#include "gtest/gtest.h" +#include "absl/strings/numbers.h" +#include "absl/strings/str_join.h" +#include "absl/strings/str_split.h" +#include "test/syscalls/linux/ip_socket_test_util.h" +#include "test/util/file_descriptor.h" +#include "test/util/test_util.h" + +namespace gvisor { +namespace testing { +namespace { + +using absl::StrCat; +using absl::StrFormat; +using absl::StrSplit; + +constexpr char kProcNetUDPHeader[] = + " sl local_address rem_address st tx_queue rx_queue tr tm->when " + "retrnsmt uid timeout inode ref pointer drops "; + +// UDPEntry represents a single entry from /proc/net/udp. +struct UDPEntry { + uint32_t local_addr; + uint16_t local_port; + + uint32_t remote_addr; + uint16_t remote_port; + + uint64_t state; + uint64_t uid; + uint64_t inode; +}; + +std::string DescribeFirstInetSocket(const SocketPair& sockets) { + const struct sockaddr* addr = sockets.first_addr(); + return StrFormat("First test socket: fd:%d %8X:%4X", sockets.first_fd(), + IPFromInetSockaddr(addr), PortFromInetSockaddr(addr)); +} + +std::string DescribeSecondInetSocket(const SocketPair& sockets) { + const struct sockaddr* addr = sockets.second_addr(); + return StrFormat("Second test socket fd:%d %8X:%4X", sockets.second_fd(), + IPFromInetSockaddr(addr), PortFromInetSockaddr(addr)); +} + +// Finds the first entry in 'entries' for which 'predicate' returns true. +// Returns true on match, and set 'match' to a copy of the matching entry. If +// 'match' is null, it's ignored. +bool FindBy(const std::vector<UDPEntry>& entries, UDPEntry* match, + std::function<bool(const UDPEntry&)> predicate) { + for (const UDPEntry& entry : entries) { + if (predicate(entry)) { + if (match != nullptr) { + *match = entry; + } + return true; + } + } + return false; +} + +bool FindByLocalAddr(const std::vector<UDPEntry>& entries, UDPEntry* match, + const struct sockaddr* addr) { + uint32_t host = IPFromInetSockaddr(addr); + uint16_t port = PortFromInetSockaddr(addr); + return FindBy(entries, match, [host, port](const UDPEntry& e) { + return (e.local_addr == host && e.local_port == port); + }); +} + +bool FindByRemoteAddr(const std::vector<UDPEntry>& entries, UDPEntry* match, + const struct sockaddr* addr) { + uint32_t host = IPFromInetSockaddr(addr); + uint16_t port = PortFromInetSockaddr(addr); + return FindBy(entries, match, [host, port](const UDPEntry& e) { + return (e.remote_addr == host && e.remote_port == port); + }); +} + +PosixErrorOr<uint64_t> InodeFromSocketFD(int fd) { + ASSIGN_OR_RETURN_ERRNO(struct stat s, Fstat(fd)); + if (!S_ISSOCK(s.st_mode)) { + return PosixError(EINVAL, StrFormat("FD %d is not a socket", fd)); + } + return s.st_ino; +} + +PosixErrorOr<bool> FindByFD(const std::vector<UDPEntry>& entries, + UDPEntry* match, int fd) { + ASSIGN_OR_RETURN_ERRNO(uint64_t inode, InodeFromSocketFD(fd)); + return FindBy(entries, match, + [inode](const UDPEntry& e) { return (e.inode == inode); }); +} + +// Returns a parsed representation of /proc/net/udp entries. +PosixErrorOr<std::vector<UDPEntry>> ProcNetUDPEntries() { + std::string content; + RETURN_IF_ERRNO(GetContents("/proc/net/udp", &content)); + + bool found_header = false; + std::vector<UDPEntry> entries; + std::vector<std::string> lines = StrSplit(content, '\n'); + std::cerr << "<contents of /proc/net/udp>" << std::endl; + for (const std::string& line : lines) { + std::cerr << line << std::endl; + + if (!found_header) { + EXPECT_EQ(line, kProcNetUDPHeader); + found_header = true; + continue; + } + if (line.empty()) { + continue; + } + + // Parse a single entry from /proc/net/udp. + // + // Example entries: + // + // clang-format off + // + // sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops + // 3503: 0100007F:0035 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 33317 2 0000000000000000 0 + // 3518: 00000000:0044 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 40394 2 0000000000000000 0 + // ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 + // + // clang-format on + + UDPEntry entry; + std::vector<std::string> fields = + StrSplit(line, absl::ByAnyChar(": "), absl::SkipEmpty()); + + ASSIGN_OR_RETURN_ERRNO(entry.local_addr, AtoiBase(fields[1], 16)); + ASSIGN_OR_RETURN_ERRNO(entry.local_port, AtoiBase(fields[2], 16)); + + ASSIGN_OR_RETURN_ERRNO(entry.remote_addr, AtoiBase(fields[3], 16)); + ASSIGN_OR_RETURN_ERRNO(entry.remote_port, AtoiBase(fields[4], 16)); + + ASSIGN_OR_RETURN_ERRNO(entry.state, AtoiBase(fields[5], 16)); + ASSIGN_OR_RETURN_ERRNO(entry.uid, Atoi<uint64_t>(fields[11])); + ASSIGN_OR_RETURN_ERRNO(entry.inode, Atoi<uint64_t>(fields[13])); + + // Linux shares internal data structures between TCP and UDP sockets. The + // proc entries for UDP sockets share some fields with TCP sockets, but + // these fields should always be zero as they're not meaningful for UDP + // sockets. + EXPECT_EQ(fields[8], "00") << StrFormat("sl:%s, tr", fields[0]); + EXPECT_EQ(fields[9], "00000000") << StrFormat("sl:%s, tm->when", fields[0]); + EXPECT_EQ(fields[10], "00000000") + << StrFormat("sl:%s, retrnsmt", fields[0]); + EXPECT_EQ(fields[12], "0") << StrFormat("sl:%s, timeout", fields[0]); + + entries.push_back(entry); + } + std::cerr << "<end of /proc/net/udp>" << std::endl; + + return entries; +} + +TEST(ProcNetUDP, Exists) { + const std::string content = + ASSERT_NO_ERRNO_AND_VALUE(GetContents("/proc/net/udp")); + const std::string header_line = StrCat(kProcNetUDPHeader, "\n"); + EXPECT_THAT(content, ::testing::StartsWith(header_line)); +} + +TEST(ProcNetUDP, EntryUID) { + auto sockets = + ASSERT_NO_ERRNO_AND_VALUE(IPv4UDPBidirectionalBindSocketPair(0).Create()); + std::vector<UDPEntry> entries = + ASSERT_NO_ERRNO_AND_VALUE(ProcNetUDPEntries()); + UDPEntry e; + ASSERT_TRUE(FindByLocalAddr(entries, &e, sockets->first_addr())) + << DescribeFirstInetSocket(*sockets); + EXPECT_EQ(e.uid, geteuid()); + ASSERT_TRUE(FindByRemoteAddr(entries, &e, sockets->first_addr())) + << DescribeSecondInetSocket(*sockets); + EXPECT_EQ(e.uid, geteuid()); +} + +TEST(ProcNetUDP, FindMutualEntries) { + auto sockets = + ASSERT_NO_ERRNO_AND_VALUE(IPv4UDPBidirectionalBindSocketPair(0).Create()); + std::vector<UDPEntry> entries = + ASSERT_NO_ERRNO_AND_VALUE(ProcNetUDPEntries()); + + EXPECT_TRUE(FindByLocalAddr(entries, nullptr, sockets->first_addr())) + << DescribeFirstInetSocket(*sockets); + EXPECT_TRUE(FindByRemoteAddr(entries, nullptr, sockets->first_addr())) + << DescribeSecondInetSocket(*sockets); + + EXPECT_TRUE(FindByLocalAddr(entries, nullptr, sockets->second_addr())) + << DescribeSecondInetSocket(*sockets); + EXPECT_TRUE(FindByRemoteAddr(entries, nullptr, sockets->second_addr())) + << DescribeFirstInetSocket(*sockets); +} + +TEST(ProcNetUDP, EntriesRemovedOnClose) { + auto sockets = + ASSERT_NO_ERRNO_AND_VALUE(IPv4UDPBidirectionalBindSocketPair(0).Create()); + std::vector<UDPEntry> entries = + ASSERT_NO_ERRNO_AND_VALUE(ProcNetUDPEntries()); + + EXPECT_TRUE(FindByLocalAddr(entries, nullptr, sockets->first_addr())) + << DescribeFirstInetSocket(*sockets); + EXPECT_TRUE(FindByLocalAddr(entries, nullptr, sockets->second_addr())) + << DescribeSecondInetSocket(*sockets); + + EXPECT_THAT(close(sockets->release_first_fd()), SyscallSucceeds()); + entries = ASSERT_NO_ERRNO_AND_VALUE(ProcNetUDPEntries()); + // First socket's entry should be gone, but the second socket's entry should + // still exist. + EXPECT_FALSE(FindByLocalAddr(entries, nullptr, sockets->first_addr())) + << DescribeFirstInetSocket(*sockets); + EXPECT_TRUE(FindByLocalAddr(entries, nullptr, sockets->second_addr())) + << DescribeSecondInetSocket(*sockets); + + EXPECT_THAT(close(sockets->release_second_fd()), SyscallSucceeds()); + entries = ASSERT_NO_ERRNO_AND_VALUE(ProcNetUDPEntries()); + // Both entries should be gone. + EXPECT_FALSE(FindByLocalAddr(entries, nullptr, sockets->first_addr())) + << DescribeFirstInetSocket(*sockets); + EXPECT_FALSE(FindByLocalAddr(entries, nullptr, sockets->second_addr())) + << DescribeSecondInetSocket(*sockets); +} + +PosixErrorOr<std::unique_ptr<FileDescriptor>> BoundUDPSocket() { + ASSIGN_OR_RETURN_ERRNO(std::unique_ptr<FileDescriptor> socket, + IPv4UDPUnboundSocket(0).Create()); + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = 0; + + int res = bind(socket->get(), reinterpret_cast<const struct sockaddr*>(&addr), + sizeof(addr)); + if (res) { + return PosixError(errno, "bind()"); + } + return socket; +} + +TEST(ProcNetUDP, BoundEntry) { + std::unique_ptr<FileDescriptor> socket = + ASSERT_NO_ERRNO_AND_VALUE(BoundUDPSocket()); + struct sockaddr addr; + socklen_t len = sizeof(addr); + ASSERT_THAT(getsockname(socket->get(), &addr, &len), SyscallSucceeds()); + uint16_t port = PortFromInetSockaddr(&addr); + + std::vector<UDPEntry> entries = + ASSERT_NO_ERRNO_AND_VALUE(ProcNetUDPEntries()); + UDPEntry e; + ASSERT_TRUE(ASSERT_NO_ERRNO_AND_VALUE(FindByFD(entries, &e, socket->get()))); + EXPECT_EQ(e.local_port, port); + EXPECT_EQ(e.remote_addr, 0); + EXPECT_EQ(e.remote_port, 0); +} + +TEST(ProcNetUDP, BoundSocketStateClosed) { + std::unique_ptr<FileDescriptor> socket = + ASSERT_NO_ERRNO_AND_VALUE(BoundUDPSocket()); + std::vector<UDPEntry> entries = + ASSERT_NO_ERRNO_AND_VALUE(ProcNetUDPEntries()); + UDPEntry e; + ASSERT_TRUE(ASSERT_NO_ERRNO_AND_VALUE(FindByFD(entries, &e, socket->get()))); + EXPECT_EQ(e.state, TCP_CLOSE); +} + +TEST(ProcNetUDP, ConnectedSocketStateEstablished) { + auto sockets = + ASSERT_NO_ERRNO_AND_VALUE(IPv4UDPBidirectionalBindSocketPair(0).Create()); + std::vector<UDPEntry> entries = + ASSERT_NO_ERRNO_AND_VALUE(ProcNetUDPEntries()); + + UDPEntry e; + ASSERT_TRUE(FindByLocalAddr(entries, &e, sockets->first_addr())) + << DescribeFirstInetSocket(*sockets); + EXPECT_EQ(e.state, TCP_ESTABLISHED); + + ASSERT_TRUE(FindByLocalAddr(entries, &e, sockets->second_addr())) + << DescribeSecondInetSocket(*sockets); + EXPECT_EQ(e.state, TCP_ESTABLISHED); +} + +} // namespace +} // namespace testing +} // namespace gvisor diff --git a/test/syscalls/linux/proc_net_unix.cc b/test/syscalls/linux/proc_net_unix.cc index 9b9be66ff..83dbd1364 100644 --- a/test/syscalls/linux/proc_net_unix.cc +++ b/test/syscalls/linux/proc_net_unix.cc @@ -56,19 +56,44 @@ struct UnixEntry { std::string path; }; +// Abstract socket paths can have either trailing null bytes or '@'s as padding +// at the end, depending on the linux version. This function strips any such +// padding. +void StripAbstractPathPadding(std::string* s) { + const char pad_char = s->back(); + if (pad_char != '\0' && pad_char != '@') { + return; + } + + const auto last_pos = s->find_last_not_of(pad_char); + if (last_pos != std::string::npos) { + s->resize(last_pos + 1); + } +} + +// Precondition: addr must be a unix socket address (i.e. sockaddr_un) and +// addr->sun_path must be null-terminated. This is always the case if addr comes +// from Linux: +// +// Per man unix(7): +// +// "When the address of a pathname socket is returned (by [getsockname(2)]), its +// length is +// +// offsetof(struct sockaddr_un, sun_path) + strlen(sun_path) + 1 +// +// and sun_path contains the null-terminated pathname." std::string ExtractPath(const struct sockaddr* addr) { const char* path = reinterpret_cast<const struct sockaddr_un*>(addr)->sun_path; // Note: sockaddr_un.sun_path is an embedded character array of length // UNIX_PATH_MAX, so we can always safely dereference the first 2 bytes below. // - // The kernel also enforces that the path is always null terminated. + // We also rely on the path being null-terminated. if (path[0] == 0) { - // Abstract socket paths are null padded to the end of the struct - // sockaddr. However, these null bytes may or may not show up in - // /proc/net/unix depending on the kernel version. Truncate after the first - // null byte (by treating path as a c-string). - return StrCat("@", &path[1]); + std::string abstract_path = StrCat("@", &path[1]); + StripAbstractPathPadding(&abstract_path); + return abstract_path; } return std::string(path); } @@ -96,14 +121,6 @@ PosixErrorOr<std::vector<UnixEntry>> ProcNetUnixEntries() { continue; } - // Abstract socket paths can have trailing null bytes in them depending on - // the linux version. Strip off everything after a null byte, including the - // null byte. - std::size_t null_pos = line.find('\0'); - if (null_pos != std::string::npos) { - line.erase(null_pos); - } - // Parse a single entry from /proc/net/unix. // // Sample file: @@ -151,6 +168,7 @@ PosixErrorOr<std::vector<UnixEntry>> ProcNetUnixEntries() { entry.path = ""; if (fields.size() > 1) { entry.path = fields[1]; + StripAbstractPathPadding(&entry.path); } entries.push_back(entry); @@ -200,8 +218,8 @@ TEST(ProcNetUnix, FilesystemBindAcceptConnect) { std::string path1 = ExtractPath(sockets->first_addr()); std::string path2 = ExtractPath(sockets->second_addr()); - std::cout << StreamFormat("Server socket address: %s\n", path1); - std::cout << StreamFormat("Client socket address: %s\n", path2); + std::cerr << StreamFormat("Server socket address (path1): %s\n", path1); + std::cerr << StreamFormat("Client socket address (path2): %s\n", path2); std::vector<UnixEntry> entries = ASSERT_NO_ERRNO_AND_VALUE(ProcNetUnixEntries()); @@ -224,8 +242,8 @@ TEST(ProcNetUnix, AbstractBindAcceptConnect) { std::string path1 = ExtractPath(sockets->first_addr()); std::string path2 = ExtractPath(sockets->second_addr()); - std::cout << StreamFormat("Server socket address: '%s'\n", path1); - std::cout << StreamFormat("Client socket address: '%s'\n", path2); + std::cerr << StreamFormat("Server socket address (path1): '%s'\n", path1); + std::cerr << StreamFormat("Client socket address (path2): '%s'\n", path2); std::vector<UnixEntry> entries = ASSERT_NO_ERRNO_AND_VALUE(ProcNetUnixEntries()); diff --git a/test/syscalls/linux/ptrace.cc b/test/syscalls/linux/ptrace.cc index abf2b1a04..8f3800380 100644 --- a/test/syscalls/linux/ptrace.cc +++ b/test/syscalls/linux/ptrace.cc @@ -27,6 +27,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "absl/flags/flag.h" #include "absl/time/clock.h" #include "absl/time/time.h" #include "test/util/logging.h" @@ -36,10 +37,10 @@ #include "test/util/thread_util.h" #include "test/util/time_util.h" -DEFINE_bool(ptrace_test_execve_child, false, - "If true, run the " - "PtraceExecveTest_Execve_GetRegs_PeekUser_SIGKILL_TraceClone_" - "TraceExit child workload."); +ABSL_FLAG(bool, ptrace_test_execve_child, false, + "If true, run the " + "PtraceExecveTest_Execve_GetRegs_PeekUser_SIGKILL_TraceClone_" + "TraceExit child workload."); namespace gvisor { namespace testing { @@ -1206,7 +1207,7 @@ TEST(PtraceTest, SeizeSetOptions) { int main(int argc, char** argv) { gvisor::testing::TestInit(&argc, &argv); - if (FLAGS_ptrace_test_execve_child) { + if (absl::GetFlag(FLAGS_ptrace_test_execve_child)) { gvisor::testing::RunExecveChild(); } diff --git a/test/syscalls/linux/pty.cc b/test/syscalls/linux/pty.cc index bd6907876..99a0df235 100644 --- a/test/syscalls/linux/pty.cc +++ b/test/syscalls/linux/pty.cc @@ -1292,10 +1292,9 @@ TEST_F(JobControlTest, ReleaseTTY) { // Make sure we're ignoring SIGHUP, which will be sent to this process once we // disconnect they TTY. - struct sigaction sa = { - .sa_handler = SIG_IGN, - .sa_flags = 0, - }; + struct sigaction sa = {}; + sa.sa_handler = SIG_IGN; + sa.sa_flags = 0; sigemptyset(&sa.sa_mask); struct sigaction old_sa; EXPECT_THAT(sigaction(SIGHUP, &sa, &old_sa), SyscallSucceeds()); @@ -1362,10 +1361,9 @@ TEST_F(JobControlTest, ReleaseTTYSignals) { ASSERT_THAT(ioctl(slave_.get(), TIOCSCTTY, 0), SyscallSucceeds()); received = 0; - struct sigaction sa = { - .sa_handler = sig_handler, - .sa_flags = 0, - }; + struct sigaction sa = {}; + sa.sa_handler = sig_handler; + sa.sa_flags = 0; sigemptyset(&sa.sa_mask); sigaddset(&sa.sa_mask, SIGHUP); sigaddset(&sa.sa_mask, SIGCONT); @@ -1403,10 +1401,9 @@ TEST_F(JobControlTest, ReleaseTTYSignals) { // Make sure we're ignoring SIGHUP, which will be sent to this process once we // disconnect they TTY. - struct sigaction sighup_sa = { - .sa_handler = SIG_IGN, - .sa_flags = 0, - }; + struct sigaction sighup_sa = {}; + sighup_sa.sa_handler = SIG_IGN; + sighup_sa.sa_flags = 0; sigemptyset(&sighup_sa.sa_mask); struct sigaction old_sa; EXPECT_THAT(sigaction(SIGHUP, &sighup_sa, &old_sa), SyscallSucceeds()); @@ -1456,10 +1453,9 @@ TEST_F(JobControlTest, SetForegroundProcessGroup) { ASSERT_THAT(ioctl(slave_.get(), TIOCSCTTY, 0), SyscallSucceeds()); // Ignore SIGTTOU so that we don't stop ourself when calling tcsetpgrp. - struct sigaction sa = { - .sa_handler = SIG_IGN, - .sa_flags = 0, - }; + struct sigaction sa = {}; + sa.sa_handler = SIG_IGN; + sa.sa_flags = 0; sigemptyset(&sa.sa_mask); sigaction(SIGTTOU, &sa, NULL); @@ -1531,27 +1527,70 @@ TEST_F(JobControlTest, SetForegroundProcessGroupEmptyProcessGroup) { TEST_F(JobControlTest, SetForegroundProcessGroupDifferentSession) { ASSERT_THAT(ioctl(slave_.get(), TIOCSCTTY, 0), SyscallSucceeds()); + int sync_setsid[2]; + int sync_exit[2]; + ASSERT_THAT(pipe(sync_setsid), SyscallSucceeds()); + ASSERT_THAT(pipe(sync_exit), SyscallSucceeds()); + // Create a new process and put it in a new session. pid_t child = fork(); if (!child) { TEST_PCHECK(setsid() >= 0); // Tell the parent we're in a new session. - TEST_PCHECK(!raise(SIGSTOP)); - TEST_PCHECK(!pause()); - _exit(1); + char c = 'c'; + TEST_PCHECK(WriteFd(sync_setsid[1], &c, 1) == 1); + TEST_PCHECK(ReadFd(sync_exit[0], &c, 1) == 1); + _exit(0); } // Wait for the child to tell us it's in a new session. - int wstatus; - EXPECT_THAT(waitpid(child, &wstatus, WUNTRACED), - SyscallSucceedsWithValue(child)); - EXPECT_TRUE(WSTOPSIG(wstatus)); + char c = 'c'; + ASSERT_THAT(ReadFd(sync_setsid[0], &c, 1), SyscallSucceedsWithValue(1)); // Child is in a new session, so we can't make it the foregroup process group. EXPECT_THAT(ioctl(slave_.get(), TIOCSPGRP, &child), SyscallFailsWithErrno(EPERM)); - EXPECT_THAT(kill(child, SIGKILL), SyscallSucceeds()); + EXPECT_THAT(WriteFd(sync_exit[1], &c, 1), SyscallSucceedsWithValue(1)); + + int wstatus; + EXPECT_THAT(waitpid(child, &wstatus, 0), SyscallSucceedsWithValue(child)); + EXPECT_TRUE(WIFEXITED(wstatus)); + EXPECT_EQ(WEXITSTATUS(wstatus), 0); +} + +// Verify that we don't hang when creating a new session from an orphaned +// process group (b/139968068). Calling setsid() creates an orphaned process +// group, as process groups that contain the session's leading process are +// orphans. +// +// We create 2 sessions in this test. The init process in gVisor is considered +// not to be an orphan (see sessions.go), so we have to create a session from +// which to create a session. The latter session is being created from an +// orphaned process group. +TEST_F(JobControlTest, OrphanRegression) { + pid_t session_2_leader = fork(); + if (!session_2_leader) { + TEST_PCHECK(setsid() >= 0); + + pid_t session_3_leader = fork(); + if (!session_3_leader) { + TEST_PCHECK(setsid() >= 0); + + _exit(0); + } + + int wstatus; + TEST_PCHECK(waitpid(session_3_leader, &wstatus, 0) == session_3_leader); + TEST_PCHECK(wstatus == 0); + + _exit(0); + } + + int wstatus; + ASSERT_THAT(waitpid(session_2_leader, &wstatus, 0), + SyscallSucceedsWithValue(session_2_leader)); + ASSERT_EQ(wstatus, 0); } } // namespace diff --git a/test/syscalls/linux/pty_root.cc b/test/syscalls/linux/pty_root.cc index d2a321a6e..14a4af980 100644 --- a/test/syscalls/linux/pty_root.cc +++ b/test/syscalls/linux/pty_root.cc @@ -50,7 +50,7 @@ TEST(JobControlRootTest, StealTTY) { // of 1. pid_t child = fork(); if (!child) { - TEST_PCHECK(setsid() >= 0); + ASSERT_THAT(setsid(), SyscallSucceeds()); // We shouldn't be able to steal the terminal with the wrong arg value. TEST_PCHECK(ioctl(slave.get(), TIOCSCTTY, 0)); // We should be able to steal it here. diff --git a/test/syscalls/linux/pwritev2.cc b/test/syscalls/linux/pwritev2.cc index db519f4e0..f6a0fc96c 100644 --- a/test/syscalls/linux/pwritev2.cc +++ b/test/syscalls/linux/pwritev2.cc @@ -244,8 +244,10 @@ TEST(Pwritev2Test, TestInvalidOffset) { const FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDWR)); + char buf[16]; struct iovec iov; - iov.iov_base = nullptr; + iov.iov_base = buf; + iov.iov_len = sizeof(buf); EXPECT_THAT(pwritev2(fd.get(), &iov, /*iovcnt=*/1, /*offset=*/static_cast<off_t>(-8), /*flags=*/0), @@ -286,8 +288,10 @@ TEST(Pwritev2Test, TestUnseekableFileInValid) { SKIP_IF(pwritev2(-1, nullptr, 0, 0, 0) < 0 && errno == ENOSYS); int pipe_fds[2]; + char buf[16]; struct iovec iov; - iov.iov_base = nullptr; + iov.iov_base = buf; + iov.iov_len = sizeof(buf); ASSERT_THAT(pipe(pipe_fds), SyscallSucceeds()); @@ -307,8 +311,10 @@ TEST(Pwritev2Test, TestReadOnlyFile) { const FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDONLY)); + char buf[16]; struct iovec iov; - iov.iov_base = nullptr; + iov.iov_base = buf; + iov.iov_len = sizeof(buf); EXPECT_THAT(pwritev2(fd.get(), &iov, /*iovcnt=*/1, /*offset=*/0, /*flags=*/0), @@ -324,8 +330,10 @@ TEST(Pwritev2Test, TestInvalidFlag) { const FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDWR | O_DIRECT)); + char buf[16]; struct iovec iov; - iov.iov_base = nullptr; + iov.iov_base = buf; + iov.iov_len = sizeof(buf); EXPECT_THAT(pwritev2(fd.get(), &iov, /*iovcnt=*/1, /*offset=*/0, /*flags=*/0xF0), diff --git a/test/syscalls/linux/raw_socket_hdrincl.cc b/test/syscalls/linux/raw_socket_hdrincl.cc index a070817eb..0a27506aa 100644 --- a/test/syscalls/linux/raw_socket_hdrincl.cc +++ b/test/syscalls/linux/raw_socket_hdrincl.cc @@ -63,7 +63,11 @@ class RawHDRINCL : public ::testing::Test { }; void RawHDRINCL::SetUp() { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); + if (!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))) { + ASSERT_THAT(socket(AF_INET, SOCK_RAW, IPPROTO_RAW), + SyscallFailsWithErrno(EPERM)); + GTEST_SKIP(); + } ASSERT_THAT(socket_ = socket(AF_INET, SOCK_RAW, IPPROTO_RAW), SyscallSucceeds()); @@ -76,9 +80,10 @@ void RawHDRINCL::SetUp() { } void RawHDRINCL::TearDown() { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - - EXPECT_THAT(close(socket_), SyscallSucceeds()); + // TearDown will be run even if we skip the test. + if (ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))) { + EXPECT_THAT(close(socket_), SyscallSucceeds()); + } } struct iphdr RawHDRINCL::LoopbackHeader() { @@ -123,8 +128,6 @@ bool RawHDRINCL::FillPacket(char* buf, size_t buf_size, int port, // We should be able to create multiple IPPROTO_RAW sockets. RawHDRINCL::Setup // creates the first one, so we only have to create one more here. TEST_F(RawHDRINCL, MultipleCreation) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - int s2; ASSERT_THAT(s2 = socket(AF_INET, SOCK_RAW, IPPROTO_RAW), SyscallSucceeds()); @@ -133,23 +136,17 @@ TEST_F(RawHDRINCL, MultipleCreation) { // Test that shutting down an unconnected socket fails. TEST_F(RawHDRINCL, FailShutdownWithoutConnect) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - ASSERT_THAT(shutdown(socket_, SHUT_WR), SyscallFailsWithErrno(ENOTCONN)); ASSERT_THAT(shutdown(socket_, SHUT_RD), SyscallFailsWithErrno(ENOTCONN)); } // Test that listen() fails. TEST_F(RawHDRINCL, FailListen) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - ASSERT_THAT(listen(socket_, 1), SyscallFailsWithErrno(ENOTSUP)); } // Test that accept() fails. TEST_F(RawHDRINCL, FailAccept) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - struct sockaddr saddr; socklen_t addrlen; ASSERT_THAT(accept(socket_, &saddr, &addrlen), @@ -158,8 +155,6 @@ TEST_F(RawHDRINCL, FailAccept) { // Test that the socket is writable immediately. TEST_F(RawHDRINCL, PollWritableImmediately) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - struct pollfd pfd = {}; pfd.fd = socket_; pfd.events = POLLOUT; @@ -168,8 +163,6 @@ TEST_F(RawHDRINCL, PollWritableImmediately) { // Test that the socket isn't readable. TEST_F(RawHDRINCL, NotReadable) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - // Try to receive data with MSG_DONTWAIT, which returns immediately if there's // nothing to be read. char buf[117]; @@ -179,16 +172,12 @@ TEST_F(RawHDRINCL, NotReadable) { // Test that we can connect() to a valid IP (loopback). TEST_F(RawHDRINCL, ConnectToLoopback) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - ASSERT_THAT(connect(socket_, reinterpret_cast<struct sockaddr*>(&addr_), sizeof(addr_)), SyscallSucceeds()); } TEST_F(RawHDRINCL, SendWithoutConnectSucceeds) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - struct iphdr hdr = LoopbackHeader(); ASSERT_THAT(send(socket_, &hdr, sizeof(hdr), 0), SyscallSucceedsWithValue(sizeof(hdr))); @@ -197,8 +186,6 @@ TEST_F(RawHDRINCL, SendWithoutConnectSucceeds) { // HDRINCL implies write-only. Verify that we can't read a packet sent to // loopback. TEST_F(RawHDRINCL, NotReadableAfterWrite) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - ASSERT_THAT(connect(socket_, reinterpret_cast<struct sockaddr*>(&addr_), sizeof(addr_)), SyscallSucceeds()); @@ -221,8 +208,6 @@ TEST_F(RawHDRINCL, NotReadableAfterWrite) { } TEST_F(RawHDRINCL, WriteTooSmall) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - ASSERT_THAT(connect(socket_, reinterpret_cast<struct sockaddr*>(&addr_), sizeof(addr_)), SyscallSucceeds()); @@ -235,8 +220,6 @@ TEST_F(RawHDRINCL, WriteTooSmall) { // Bind to localhost. TEST_F(RawHDRINCL, BindToLocalhost) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - ASSERT_THAT( bind(socket_, reinterpret_cast<struct sockaddr*>(&addr_), sizeof(addr_)), SyscallSucceeds()); @@ -244,8 +227,6 @@ TEST_F(RawHDRINCL, BindToLocalhost) { // Bind to a different address. TEST_F(RawHDRINCL, BindToInvalid) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - struct sockaddr_in bind_addr = {}; bind_addr.sin_family = AF_INET; bind_addr.sin_addr = {1}; // 1.0.0.0 - An address that we can't bind to. @@ -256,8 +237,6 @@ TEST_F(RawHDRINCL, BindToInvalid) { // Send and receive a packet. TEST_F(RawHDRINCL, SendAndReceive) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - int port = 40000; if (!IsRunningOnGvisor()) { port = static_cast<short>(ASSERT_NO_ERRNO_AND_VALUE( @@ -302,8 +281,6 @@ TEST_F(RawHDRINCL, SendAndReceive) { // Send and receive a packet with nonzero IP ID. TEST_F(RawHDRINCL, SendAndReceiveNonzeroID) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - int port = 40000; if (!IsRunningOnGvisor()) { port = static_cast<short>(ASSERT_NO_ERRNO_AND_VALUE( @@ -349,8 +326,6 @@ TEST_F(RawHDRINCL, SendAndReceiveNonzeroID) { // Send and receive a packet where the sendto address is not the same as the // provided destination. TEST_F(RawHDRINCL, SendAndReceiveDifferentAddress) { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - int port = 40000; if (!IsRunningOnGvisor()) { port = static_cast<short>(ASSERT_NO_ERRNO_AND_VALUE( diff --git a/test/syscalls/linux/raw_socket_icmp.cc b/test/syscalls/linux/raw_socket_icmp.cc index 971592d7d..8bcaba6f1 100644 --- a/test/syscalls/linux/raw_socket_icmp.cc +++ b/test/syscalls/linux/raw_socket_icmp.cc @@ -77,7 +77,11 @@ class RawSocketICMPTest : public ::testing::Test { }; void RawSocketICMPTest::SetUp() { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); + if (!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))) { + ASSERT_THAT(socket(AF_INET, SOCK_RAW, IPPROTO_ICMP), + SyscallFailsWithErrno(EPERM)); + GTEST_SKIP(); + } ASSERT_THAT(s_ = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP), SyscallSucceeds()); @@ -90,9 +94,10 @@ void RawSocketICMPTest::SetUp() { } void RawSocketICMPTest::TearDown() { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - - EXPECT_THAT(close(s_), SyscallSucceeds()); + // TearDown will be run even if we skip the test. + if (ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))) { + EXPECT_THAT(close(s_), SyscallSucceeds()); + } } // We'll only read an echo in this case, as the kernel won't respond to the diff --git a/test/syscalls/linux/raw_socket_ipv4.cc b/test/syscalls/linux/raw_socket_ipv4.cc index 352037c88..cde2f07c9 100644 --- a/test/syscalls/linux/raw_socket_ipv4.cc +++ b/test/syscalls/linux/raw_socket_ipv4.cc @@ -67,7 +67,11 @@ class RawSocketTest : public ::testing::TestWithParam<int> { }; void RawSocketTest::SetUp() { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); + if (!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))) { + ASSERT_THAT(socket(AF_INET, SOCK_RAW, Protocol()), + SyscallFailsWithErrno(EPERM)); + GTEST_SKIP(); + } ASSERT_THAT(s_ = socket(AF_INET, SOCK_RAW, Protocol()), SyscallSucceeds()); @@ -79,9 +83,10 @@ void RawSocketTest::SetUp() { } void RawSocketTest::TearDown() { - SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))); - - EXPECT_THAT(close(s_), SyscallSucceeds()); + // TearDown will be run even if we skip the test. + if (ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))) { + EXPECT_THAT(close(s_), SyscallSucceeds()); + } } // We should be able to create multiple raw sockets for the same protocol. diff --git a/test/syscalls/linux/readahead.cc b/test/syscalls/linux/readahead.cc new file mode 100644 index 000000000..09703b5c1 --- /dev/null +++ b/test/syscalls/linux/readahead.cc @@ -0,0 +1,91 @@ +// Copyright 2019 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. + +#include <errno.h> +#include <fcntl.h> + +#include "gtest/gtest.h" +#include "test/util/file_descriptor.h" +#include "test/util/temp_path.h" +#include "test/util/test_util.h" + +namespace gvisor { +namespace testing { + +namespace { + +TEST(ReadaheadTest, InvalidFD) { + EXPECT_THAT(readahead(-1, 1, 1), SyscallFailsWithErrno(EBADF)); +} + +TEST(ReadaheadTest, InvalidOffset) { + const TempPath in_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); + const FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(Open(in_file.path(), O_RDWR)); + EXPECT_THAT(readahead(fd.get(), -1, 1), SyscallFailsWithErrno(EINVAL)); +} + +TEST(ReadaheadTest, ValidOffset) { + constexpr char kData[] = "123"; + const TempPath in_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( + GetAbsoluteTestTmpdir(), kData, TempPath::kDefaultFileMode)); + const FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(Open(in_file.path(), O_RDWR)); + + // N.B. The implementation of readahead is filesystem-specific, and a file + // backed by ram may return EINVAL because there is nothing to be read. + EXPECT_THAT(readahead(fd.get(), 1, 1), AnyOf(SyscallSucceedsWithValue(0), + SyscallFailsWithErrno(EINVAL))); +} + +TEST(ReadaheadTest, PastEnd) { + constexpr char kData[] = "123"; + const TempPath in_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( + GetAbsoluteTestTmpdir(), kData, TempPath::kDefaultFileMode)); + const FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(Open(in_file.path(), O_RDWR)); + // See above. + EXPECT_THAT(readahead(fd.get(), 2, 2), AnyOf(SyscallSucceedsWithValue(0), + SyscallFailsWithErrno(EINVAL))); +} + +TEST(ReadaheadTest, CrossesEnd) { + constexpr char kData[] = "123"; + const TempPath in_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( + GetAbsoluteTestTmpdir(), kData, TempPath::kDefaultFileMode)); + const FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(Open(in_file.path(), O_RDWR)); + // See above. + EXPECT_THAT(readahead(fd.get(), 4, 2), AnyOf(SyscallSucceedsWithValue(0), + SyscallFailsWithErrno(EINVAL))); +} + +TEST(ReadaheadTest, WriteOnly) { + const TempPath in_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); + const FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(Open(in_file.path(), O_WRONLY)); + EXPECT_THAT(readahead(fd.get(), 0, 1), SyscallFailsWithErrno(EBADF)); +} + +TEST(ReadaheadTest, InvalidSize) { + const TempPath in_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); + const FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(Open(in_file.path(), O_RDWR)); + EXPECT_THAT(readahead(fd.get(), 0, -1), SyscallFailsWithErrno(EINVAL)); +} + +} // namespace + +} // namespace testing +} // namespace gvisor diff --git a/test/syscalls/linux/semaphore.cc b/test/syscalls/linux/semaphore.cc index 421318fcb..40c57f543 100644 --- a/test/syscalls/linux/semaphore.cc +++ b/test/syscalls/linux/semaphore.cc @@ -15,6 +15,7 @@ #include <sys/ipc.h> #include <sys/sem.h> #include <sys/types.h> + #include <atomic> #include <cerrno> #include <ctime> @@ -22,6 +23,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/base/macros.h" +#include "absl/memory/memory.h" #include "absl/synchronization/mutex.h" #include "absl/time/clock.h" #include "test/util/capability_util.h" diff --git a/test/syscalls/linux/sendfile.cc b/test/syscalls/linux/sendfile.cc index e5d72e28a..4502e7fb4 100644 --- a/test/syscalls/linux/sendfile.cc +++ b/test/syscalls/linux/sendfile.cc @@ -19,9 +19,12 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/strings/string_view.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" #include "test/util/file_descriptor.h" #include "test/util/temp_path.h" #include "test/util/test_util.h" +#include "test/util/thread_util.h" namespace gvisor { namespace testing { @@ -299,10 +302,30 @@ TEST(SendFileTest, DoNotSendfileIfOutfileIsAppendOnly) { // Open the output file as append only. const FileDescriptor outf = - ASSERT_NO_ERRNO_AND_VALUE(Open(out_file.path(), O_APPEND)); + ASSERT_NO_ERRNO_AND_VALUE(Open(out_file.path(), O_WRONLY | O_APPEND)); // Send data and verify that sendfile returns the correct errno. EXPECT_THAT(sendfile(outf.get(), inf.get(), nullptr, kDataSize), + SyscallFailsWithErrno(EINVAL)); +} + +TEST(SendFileTest, AppendCheckOrdering) { + constexpr char kData[] = "And by opposing end them: to die, to sleep"; + constexpr int kDataSize = sizeof(kData) - 1; + const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( + GetAbsoluteTestTmpdir(), kData, TempPath::kDefaultFileMode)); + + const FileDescriptor read = + ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDONLY)); + const FileDescriptor write = + ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_WRONLY)); + const FileDescriptor append = + ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_APPEND)); + + // Check that read/write file mode is verified before append. + EXPECT_THAT(sendfile(append.get(), read.get(), nullptr, kDataSize), + SyscallFailsWithErrno(EBADF)); + EXPECT_THAT(sendfile(write.get(), write.get(), nullptr, kDataSize), SyscallFailsWithErrno(EBADF)); } @@ -422,6 +445,72 @@ TEST(SendFileTest, SendToNotARegularFile) { EXPECT_THAT(sendfile(outf.get(), inf.get(), nullptr, 0), SyscallFailsWithErrno(EINVAL)); } + +TEST(SendFileTest, SendPipeWouldBlock) { + // Create temp file. + constexpr char kData[] = + "The fool doth think he is wise, but the wise man knows himself to be a " + "fool."; + constexpr int kDataSize = sizeof(kData) - 1; + const TempPath in_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( + GetAbsoluteTestTmpdir(), kData, TempPath::kDefaultFileMode)); + + // Open the input file as read only. + const FileDescriptor inf = + ASSERT_NO_ERRNO_AND_VALUE(Open(in_file.path(), O_RDONLY)); + + // Setup the output named pipe. + int fds[2]; + ASSERT_THAT(pipe2(fds, O_NONBLOCK), SyscallSucceeds()); + const FileDescriptor rfd(fds[0]); + const FileDescriptor wfd(fds[1]); + + // Fill up the pipe's buffer. + int pipe_size = -1; + ASSERT_THAT(pipe_size = fcntl(wfd.get(), F_GETPIPE_SZ), SyscallSucceeds()); + std::vector<char> buf(2 * pipe_size); + ASSERT_THAT(write(wfd.get(), buf.data(), buf.size()), + SyscallSucceedsWithValue(pipe_size)); + + EXPECT_THAT(sendfile(wfd.get(), inf.get(), nullptr, kDataSize), + SyscallFailsWithErrno(EWOULDBLOCK)); +} + +TEST(SendFileTest, SendPipeBlocks) { + // Create temp file. + constexpr char kData[] = + "The fault, dear Brutus, is not in our stars, but in ourselves."; + constexpr int kDataSize = sizeof(kData) - 1; + const TempPath in_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( + GetAbsoluteTestTmpdir(), kData, TempPath::kDefaultFileMode)); + + // Open the input file as read only. + const FileDescriptor inf = + ASSERT_NO_ERRNO_AND_VALUE(Open(in_file.path(), O_RDONLY)); + + // Setup the output named pipe. + int fds[2]; + ASSERT_THAT(pipe(fds), SyscallSucceeds()); + const FileDescriptor rfd(fds[0]); + const FileDescriptor wfd(fds[1]); + + // Fill up the pipe's buffer. + int pipe_size = -1; + ASSERT_THAT(pipe_size = fcntl(wfd.get(), F_GETPIPE_SZ), SyscallSucceeds()); + std::vector<char> buf(pipe_size); + ASSERT_THAT(write(wfd.get(), buf.data(), buf.size()), + SyscallSucceedsWithValue(pipe_size)); + + ScopedThread t([&]() { + absl::SleepFor(absl::Milliseconds(100)); + ASSERT_THAT(read(rfd.get(), buf.data(), buf.size()), + SyscallSucceedsWithValue(pipe_size)); + }); + + EXPECT_THAT(sendfile(wfd.get(), inf.get(), nullptr, kDataSize), + SyscallSucceedsWithValue(kDataSize)); +} + } // namespace } // namespace testing diff --git a/test/syscalls/linux/signalfd.cc b/test/syscalls/linux/signalfd.cc new file mode 100644 index 000000000..54c598627 --- /dev/null +++ b/test/syscalls/linux/signalfd.cc @@ -0,0 +1,333 @@ +// Copyright 2019 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. + +#include <errno.h> +#include <poll.h> +#include <signal.h> +#include <stdio.h> +#include <string.h> +#include <sys/signalfd.h> +#include <unistd.h> + +#include <functional> +#include <vector> + +#include "gtest/gtest.h" +#include "gtest/gtest.h" +#include "absl/synchronization/mutex.h" +#include "test/util/file_descriptor.h" +#include "test/util/posix_error.h" +#include "test/util/signal_util.h" +#include "test/util/test_util.h" +#include "test/util/thread_util.h" + +using ::testing::KilledBySignal; + +namespace gvisor { +namespace testing { + +namespace { + +constexpr int kSigno = SIGUSR1; +constexpr int kSignoAlt = SIGUSR2; + +// Returns a new signalfd. +inline PosixErrorOr<FileDescriptor> NewSignalFD(sigset_t* mask, int flags = 0) { + int fd = signalfd(-1, mask, flags); + MaybeSave(); + if (fd < 0) { + return PosixError(errno, "signalfd"); + } + return FileDescriptor(fd); +} + +TEST(Signalfd, Basic) { + // Create the signalfd. + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, kSigno); + FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(NewSignalFD(&mask, 0)); + + // Deliver the blocked signal. + const auto scoped_sigmask = + ASSERT_NO_ERRNO_AND_VALUE(ScopedSignalMask(SIG_BLOCK, kSigno)); + ASSERT_THAT(tgkill(getpid(), gettid(), kSigno), SyscallSucceeds()); + + // We should now read the signal. + struct signalfd_siginfo rbuf; + ASSERT_THAT(read(fd.get(), &rbuf, sizeof(rbuf)), + SyscallSucceedsWithValue(sizeof(rbuf))); + EXPECT_EQ(rbuf.ssi_signo, kSigno); +} + +TEST(Signalfd, MaskWorks) { + // Create two signalfds with different masks. + sigset_t mask1, mask2; + sigemptyset(&mask1); + sigemptyset(&mask2); + sigaddset(&mask1, kSigno); + sigaddset(&mask2, kSignoAlt); + FileDescriptor fd1 = ASSERT_NO_ERRNO_AND_VALUE(NewSignalFD(&mask1, 0)); + FileDescriptor fd2 = ASSERT_NO_ERRNO_AND_VALUE(NewSignalFD(&mask2, 0)); + + // Deliver the two signals. + const auto scoped_sigmask1 = + ASSERT_NO_ERRNO_AND_VALUE(ScopedSignalMask(SIG_BLOCK, kSigno)); + const auto scoped_sigmask2 = + ASSERT_NO_ERRNO_AND_VALUE(ScopedSignalMask(SIG_BLOCK, kSignoAlt)); + ASSERT_THAT(tgkill(getpid(), gettid(), kSigno), SyscallSucceeds()); + ASSERT_THAT(tgkill(getpid(), gettid(), kSignoAlt), SyscallSucceeds()); + + // We should see the signals on the appropriate signalfds. + // + // We read in the opposite order as the signals deliver above, to ensure that + // we don't happen to read the correct signal from the correct signalfd. + struct signalfd_siginfo rbuf1, rbuf2; + ASSERT_THAT(read(fd2.get(), &rbuf2, sizeof(rbuf2)), + SyscallSucceedsWithValue(sizeof(rbuf2))); + EXPECT_EQ(rbuf2.ssi_signo, kSignoAlt); + ASSERT_THAT(read(fd1.get(), &rbuf1, sizeof(rbuf1)), + SyscallSucceedsWithValue(sizeof(rbuf1))); + EXPECT_EQ(rbuf1.ssi_signo, kSigno); +} + +TEST(Signalfd, Cloexec) { + // Exec tests confirm that O_CLOEXEC has the intended effect. We just create a + // signalfd with the appropriate flag here and assert that the FD has it set. + sigset_t mask; + sigemptyset(&mask); + FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(NewSignalFD(&mask, SFD_CLOEXEC)); + EXPECT_THAT(fcntl(fd.get(), F_GETFD), SyscallSucceedsWithValue(FD_CLOEXEC)); +} + +TEST(Signalfd, Blocking) { + // Create the signalfd in blocking mode. + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, kSigno); + FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(NewSignalFD(&mask, 0)); + + // Shared tid variable. + absl::Mutex mu; + bool has_tid; + pid_t tid; + + // Start a thread reading. + ScopedThread t([&] { + // Copy the tid and notify the caller. + { + absl::MutexLock ml(&mu); + tid = gettid(); + has_tid = true; + } + + // Read the signal from the signalfd. + struct signalfd_siginfo rbuf; + ASSERT_THAT(read(fd.get(), &rbuf, sizeof(rbuf)), + SyscallSucceedsWithValue(sizeof(rbuf))); + EXPECT_EQ(rbuf.ssi_signo, kSigno); + }); + + // Wait until blocked. + absl::MutexLock ml(&mu); + mu.Await(absl::Condition(&has_tid)); + + // Deliver the signal to either the waiting thread, or + // to this thread. N.B. this is a bug in the core gVisor + // behavior for signalfd, and needs to be fixed. + // + // See gvisor.dev/issue/139. + if (IsRunningOnGvisor()) { + ASSERT_THAT(tgkill(getpid(), gettid(), kSigno), SyscallSucceeds()); + } else { + ASSERT_THAT(tgkill(getpid(), tid, kSigno), SyscallSucceeds()); + } + + // Ensure that it was received. + t.Join(); +} + +TEST(Signalfd, ThreadGroup) { + // Create the signalfd in blocking mode. + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, kSigno); + FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(NewSignalFD(&mask, 0)); + + // Shared variable. + absl::Mutex mu; + bool first = false; + bool second = false; + + // Start a thread reading. + ScopedThread t([&] { + // Read the signal from the signalfd. + struct signalfd_siginfo rbuf; + ASSERT_THAT(read(fd.get(), &rbuf, sizeof(rbuf)), + SyscallSucceedsWithValue(sizeof(rbuf))); + EXPECT_EQ(rbuf.ssi_signo, kSigno); + + // Wait for the other thread. + absl::MutexLock ml(&mu); + first = true; + mu.Await(absl::Condition(&second)); + }); + + // Deliver the signal to the threadgroup. + ASSERT_THAT(kill(getpid(), kSigno), SyscallSucceeds()); + + // Wait for the first thread to process. + { + absl::MutexLock ml(&mu); + mu.Await(absl::Condition(&first)); + } + + // Deliver to the thread group again (other thread still exists). + ASSERT_THAT(kill(getpid(), kSigno), SyscallSucceeds()); + + // Ensure that we can also receive it. + struct signalfd_siginfo rbuf; + ASSERT_THAT(read(fd.get(), &rbuf, sizeof(rbuf)), + SyscallSucceedsWithValue(sizeof(rbuf))); + EXPECT_EQ(rbuf.ssi_signo, kSigno); + + // Mark the test as done. + { + absl::MutexLock ml(&mu); + second = true; + } + + // The other thread should be joinable. + t.Join(); +} + +TEST(Signalfd, Nonblock) { + // Create the signalfd in non-blocking mode. + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, kSigno); + FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(NewSignalFD(&mask, SFD_NONBLOCK)); + + // We should return if we attempt to read. + struct signalfd_siginfo rbuf; + ASSERT_THAT(read(fd.get(), &rbuf, sizeof(rbuf)), + SyscallFailsWithErrno(EWOULDBLOCK)); + + // Block and deliver the signal. + const auto scoped_sigmask = + ASSERT_NO_ERRNO_AND_VALUE(ScopedSignalMask(SIG_BLOCK, kSigno)); + ASSERT_THAT(tgkill(getpid(), gettid(), kSigno), SyscallSucceeds()); + + // Ensure that a read actually works. + ASSERT_THAT(read(fd.get(), &rbuf, sizeof(rbuf)), + SyscallSucceedsWithValue(sizeof(rbuf))); + EXPECT_EQ(rbuf.ssi_signo, kSigno); + + // Should block again. + EXPECT_THAT(read(fd.get(), &rbuf, sizeof(rbuf)), + SyscallFailsWithErrno(EWOULDBLOCK)); +} + +TEST(Signalfd, SetMask) { + // Create the signalfd matching nothing. + sigset_t mask; + sigemptyset(&mask); + FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(NewSignalFD(&mask, SFD_NONBLOCK)); + + // Block and deliver a signal. + const auto scoped_sigmask = + ASSERT_NO_ERRNO_AND_VALUE(ScopedSignalMask(SIG_BLOCK, kSigno)); + ASSERT_THAT(tgkill(getpid(), gettid(), kSigno), SyscallSucceeds()); + + // We should have nothing. + struct signalfd_siginfo rbuf; + ASSERT_THAT(read(fd.get(), &rbuf, sizeof(rbuf)), + SyscallFailsWithErrno(EWOULDBLOCK)); + + // Change the signal mask. + sigaddset(&mask, kSigno); + ASSERT_THAT(signalfd(fd.get(), &mask, 0), SyscallSucceeds()); + + // We should now have the signal. + ASSERT_THAT(read(fd.get(), &rbuf, sizeof(rbuf)), + SyscallSucceedsWithValue(sizeof(rbuf))); + EXPECT_EQ(rbuf.ssi_signo, kSigno); +} + +TEST(Signalfd, Poll) { + // Create the signalfd. + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, kSigno); + FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(NewSignalFD(&mask, 0)); + + // Block the signal, and start a thread to deliver it. + const auto scoped_sigmask = + ASSERT_NO_ERRNO_AND_VALUE(ScopedSignalMask(SIG_BLOCK, kSigno)); + pid_t orig_tid = gettid(); + ScopedThread t([&] { + absl::SleepFor(absl::Seconds(5)); + ASSERT_THAT(tgkill(getpid(), orig_tid, kSigno), SyscallSucceeds()); + }); + + // Start polling for the signal. We expect that it is not available at the + // outset, but then becomes available when the signal is sent. We give a + // timeout of 10000ms (or the delay above + 5 seconds of additional grace + // time). + struct pollfd poll_fd = {fd.get(), POLLIN, 0}; + EXPECT_THAT(RetryEINTR(poll)(&poll_fd, 1, 10000), + SyscallSucceedsWithValue(1)); + + // Actually read the signal to prevent delivery. + struct signalfd_siginfo rbuf; + EXPECT_THAT(read(fd.get(), &rbuf, sizeof(rbuf)), + SyscallSucceedsWithValue(sizeof(rbuf))); +} + +TEST(Signalfd, KillStillKills) { + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, SIGKILL); + FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(NewSignalFD(&mask, SFD_CLOEXEC)); + + // Just because there is a signalfd, we shouldn't see any change in behavior + // for unblockable signals. It's easier to test this with SIGKILL. + const auto scoped_sigmask = + ASSERT_NO_ERRNO_AND_VALUE(ScopedSignalMask(SIG_BLOCK, SIGKILL)); + EXPECT_EXIT(tgkill(getpid(), gettid(), SIGKILL), KilledBySignal(SIGKILL), ""); +} + +} // namespace + +} // namespace testing +} // namespace gvisor + +int main(int argc, char** argv) { + // These tests depend on delivering signals. Block them up front so that all + // other threads created by TestInit will also have them blocked, and they + // will not interface with the rest of the test. + sigset_t set; + sigemptyset(&set); + sigaddset(&set, gvisor::testing::kSigno); + sigaddset(&set, gvisor::testing::kSignoAlt); + TEST_PCHECK(sigprocmask(SIG_BLOCK, &set, nullptr) == 0); + + gvisor::testing::TestInit(&argc, &argv); + + return RUN_ALL_TESTS(); +} diff --git a/test/syscalls/linux/sigstop.cc b/test/syscalls/linux/sigstop.cc index 9c7210e17..7db57d968 100644 --- a/test/syscalls/linux/sigstop.cc +++ b/test/syscalls/linux/sigstop.cc @@ -17,6 +17,7 @@ #include <sys/select.h> #include "gtest/gtest.h" +#include "absl/flags/flag.h" #include "absl/time/clock.h" #include "absl/time/time.h" #include "test/util/multiprocess_util.h" @@ -24,8 +25,8 @@ #include "test/util/test_util.h" #include "test/util/thread_util.h" -DEFINE_bool(sigstop_test_child, false, - "If true, run the SigstopTest child workload."); +ABSL_FLAG(bool, sigstop_test_child, false, + "If true, run the SigstopTest child workload."); namespace gvisor { namespace testing { @@ -141,7 +142,7 @@ void RunChild() { int main(int argc, char** argv) { gvisor::testing::TestInit(&argc, &argv); - if (FLAGS_sigstop_test_child) { + if (absl::GetFlag(FLAGS_sigstop_test_child)) { gvisor::testing::RunChild(); return 1; } diff --git a/test/syscalls/linux/socket.cc b/test/syscalls/linux/socket.cc index 0404190a0..caae215b8 100644 --- a/test/syscalls/linux/socket.cc +++ b/test/syscalls/linux/socket.cc @@ -30,12 +30,25 @@ TEST(SocketTest, UnixSocketPairProtocol) { close(socks[1]); } -TEST(SocketTest, Protocol) { +TEST(SocketTest, ProtocolUnix) { struct { int domain, type, protocol; } tests[] = { - {AF_UNIX, SOCK_STREAM, PF_UNIX}, {AF_UNIX, SOCK_SEQPACKET, PF_UNIX}, - {AF_UNIX, SOCK_DGRAM, PF_UNIX}, {AF_INET, SOCK_DGRAM, IPPROTO_UDP}, + {AF_UNIX, SOCK_STREAM, PF_UNIX}, + {AF_UNIX, SOCK_SEQPACKET, PF_UNIX}, + {AF_UNIX, SOCK_DGRAM, PF_UNIX}, + }; + for (int i = 0; i < ABSL_ARRAYSIZE(tests); i++) { + ASSERT_NO_ERRNO_AND_VALUE( + Socket(tests[i].domain, tests[i].type, tests[i].protocol)); + } +} + +TEST(SocketTest, ProtocolInet) { + struct { + int domain, type, protocol; + } tests[] = { + {AF_INET, SOCK_DGRAM, IPPROTO_UDP}, {AF_INET, SOCK_STREAM, IPPROTO_TCP}, }; for (int i = 0; i < ABSL_ARRAYSIZE(tests); i++) { diff --git a/test/syscalls/linux/socket_bind_to_device.cc b/test/syscalls/linux/socket_bind_to_device.cc new file mode 100644 index 000000000..d20821cac --- /dev/null +++ b/test/syscalls/linux/socket_bind_to_device.cc @@ -0,0 +1,314 @@ +// Copyright 2019 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. + +#include <arpa/inet.h> +#include <linux/if_tun.h> +#include <net/if.h> +#include <netinet/in.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/un.h> + +#include <cstdio> +#include <cstring> +#include <map> +#include <memory> +#include <unordered_map> +#include <unordered_set> +#include <utility> +#include <vector> + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "gtest/gtest.h" +#include "test/syscalls/linux/ip_socket_test_util.h" +#include "test/syscalls/linux/socket_bind_to_device_util.h" +#include "test/syscalls/linux/socket_test_util.h" +#include "test/util/capability_util.h" +#include "test/util/test_util.h" +#include "test/util/thread_util.h" + +namespace gvisor { +namespace testing { + +using std::string; + +// Test fixture for SO_BINDTODEVICE tests. +class BindToDeviceTest : public ::testing::TestWithParam<SocketKind> { + protected: + void SetUp() override { + printf("Testing case: %s\n", GetParam().description.c_str()); + ASSERT_TRUE(ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))) + << "CAP_NET_RAW is required to use SO_BINDTODEVICE"; + + interface_name_ = "eth1"; + auto interface_names = GetInterfaceNames(); + if (interface_names.find(interface_name_) == interface_names.end()) { + // Need a tunnel. + tunnel_ = ASSERT_NO_ERRNO_AND_VALUE(Tunnel::New()); + interface_name_ = tunnel_->GetName(); + ASSERT_FALSE(interface_name_.empty()); + } + socket_ = ASSERT_NO_ERRNO_AND_VALUE(GetParam().Create()); + } + + string interface_name() const { return interface_name_; } + + int socket_fd() const { return socket_->get(); } + + private: + std::unique_ptr<Tunnel> tunnel_; + string interface_name_; + std::unique_ptr<FileDescriptor> socket_; +}; + +constexpr char kIllegalIfnameChar = '/'; + +// Tests getsockopt of the default value. +TEST_P(BindToDeviceTest, GetsockoptDefault) { + char name_buffer[IFNAMSIZ * 2]; + char original_name_buffer[IFNAMSIZ * 2]; + socklen_t name_buffer_size; + + // Read the default SO_BINDTODEVICE. + memset(original_name_buffer, kIllegalIfnameChar, sizeof(name_buffer)); + for (size_t i = 0; i <= sizeof(name_buffer); i++) { + memset(name_buffer, kIllegalIfnameChar, sizeof(name_buffer)); + name_buffer_size = i; + EXPECT_THAT(getsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, + name_buffer, &name_buffer_size), + SyscallSucceedsWithValue(0)); + EXPECT_EQ(name_buffer_size, 0); + EXPECT_EQ(memcmp(name_buffer, original_name_buffer, sizeof(name_buffer)), + 0); + } +} + +// Tests setsockopt of invalid device name. +TEST_P(BindToDeviceTest, SetsockoptInvalidDeviceName) { + char name_buffer[IFNAMSIZ * 2]; + socklen_t name_buffer_size; + + // Set an invalid device name. + memset(name_buffer, kIllegalIfnameChar, 5); + name_buffer_size = 5; + EXPECT_THAT(setsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, name_buffer, + name_buffer_size), + SyscallFailsWithErrno(ENODEV)); +} + +// Tests setsockopt of a buffer with a valid device name but not +// null-terminated, with different sizes of buffer. +TEST_P(BindToDeviceTest, SetsockoptValidDeviceNameWithoutNullTermination) { + char name_buffer[IFNAMSIZ * 2]; + socklen_t name_buffer_size; + + strncpy(name_buffer, interface_name().c_str(), interface_name().size() + 1); + // Intentionally overwrite the null at the end. + memset(name_buffer + interface_name().size(), kIllegalIfnameChar, + sizeof(name_buffer) - interface_name().size()); + for (size_t i = 1; i <= sizeof(name_buffer); i++) { + name_buffer_size = i; + SCOPED_TRACE(absl::StrCat("Buffer size: ", i)); + // It should only work if the size provided is exactly right. + if (name_buffer_size == interface_name().size()) { + EXPECT_THAT(setsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, + name_buffer, name_buffer_size), + SyscallSucceeds()); + } else { + EXPECT_THAT(setsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, + name_buffer, name_buffer_size), + SyscallFailsWithErrno(ENODEV)); + } + } +} + +// Tests setsockopt of a buffer with a valid device name and null-terminated, +// with different sizes of buffer. +TEST_P(BindToDeviceTest, SetsockoptValidDeviceNameWithNullTermination) { + char name_buffer[IFNAMSIZ * 2]; + socklen_t name_buffer_size; + + strncpy(name_buffer, interface_name().c_str(), interface_name().size() + 1); + // Don't overwrite the null at the end. + memset(name_buffer + interface_name().size() + 1, kIllegalIfnameChar, + sizeof(name_buffer) - interface_name().size() - 1); + for (size_t i = 1; i <= sizeof(name_buffer); i++) { + name_buffer_size = i; + SCOPED_TRACE(absl::StrCat("Buffer size: ", i)); + // It should only work if the size provided is at least the right size. + if (name_buffer_size >= interface_name().size()) { + EXPECT_THAT(setsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, + name_buffer, name_buffer_size), + SyscallSucceeds()); + } else { + EXPECT_THAT(setsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, + name_buffer, name_buffer_size), + SyscallFailsWithErrno(ENODEV)); + } + } +} + +// Tests that setsockopt of an invalid device name doesn't unset the previous +// valid setsockopt. +TEST_P(BindToDeviceTest, SetsockoptValidThenInvalid) { + char name_buffer[IFNAMSIZ * 2]; + socklen_t name_buffer_size; + + // Write successfully. + strncpy(name_buffer, interface_name().c_str(), sizeof(name_buffer)); + ASSERT_THAT(setsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, name_buffer, + sizeof(name_buffer)), + SyscallSucceeds()); + + // Read it back successfully. + memset(name_buffer, kIllegalIfnameChar, sizeof(name_buffer)); + name_buffer_size = sizeof(name_buffer); + EXPECT_THAT(getsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, name_buffer, + &name_buffer_size), + SyscallSucceeds()); + EXPECT_EQ(name_buffer_size, interface_name().size() + 1); + EXPECT_STREQ(name_buffer, interface_name().c_str()); + + // Write unsuccessfully. + memset(name_buffer, kIllegalIfnameChar, sizeof(name_buffer)); + name_buffer_size = 5; + EXPECT_THAT(setsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, name_buffer, + sizeof(name_buffer)), + SyscallFailsWithErrno(ENODEV)); + + // Read it back successfully, it's unchanged. + memset(name_buffer, kIllegalIfnameChar, sizeof(name_buffer)); + name_buffer_size = sizeof(name_buffer); + EXPECT_THAT(getsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, name_buffer, + &name_buffer_size), + SyscallSucceeds()); + EXPECT_EQ(name_buffer_size, interface_name().size() + 1); + EXPECT_STREQ(name_buffer, interface_name().c_str()); +} + +// Tests that setsockopt of zero-length string correctly unsets the previous +// value. +TEST_P(BindToDeviceTest, SetsockoptValidThenClear) { + char name_buffer[IFNAMSIZ * 2]; + socklen_t name_buffer_size; + + // Write successfully. + strncpy(name_buffer, interface_name().c_str(), sizeof(name_buffer)); + EXPECT_THAT(setsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, name_buffer, + sizeof(name_buffer)), + SyscallSucceeds()); + + // Read it back successfully. + memset(name_buffer, kIllegalIfnameChar, sizeof(name_buffer)); + name_buffer_size = sizeof(name_buffer); + EXPECT_THAT(getsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, name_buffer, + &name_buffer_size), + SyscallSucceeds()); + EXPECT_EQ(name_buffer_size, interface_name().size() + 1); + EXPECT_STREQ(name_buffer, interface_name().c_str()); + + // Clear it successfully. + name_buffer_size = 0; + EXPECT_THAT(setsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, name_buffer, + name_buffer_size), + SyscallSucceeds()); + + // Read it back successfully, it's cleared. + memset(name_buffer, kIllegalIfnameChar, sizeof(name_buffer)); + name_buffer_size = sizeof(name_buffer); + EXPECT_THAT(getsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, name_buffer, + &name_buffer_size), + SyscallSucceeds()); + EXPECT_EQ(name_buffer_size, 0); +} + +// Tests that setsockopt of empty string correctly unsets the previous +// value. +TEST_P(BindToDeviceTest, SetsockoptValidThenClearWithNull) { + char name_buffer[IFNAMSIZ * 2]; + socklen_t name_buffer_size; + + // Write successfully. + strncpy(name_buffer, interface_name().c_str(), sizeof(name_buffer)); + EXPECT_THAT(setsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, name_buffer, + sizeof(name_buffer)), + SyscallSucceeds()); + + // Read it back successfully. + memset(name_buffer, kIllegalIfnameChar, sizeof(name_buffer)); + name_buffer_size = sizeof(name_buffer); + EXPECT_THAT(getsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, name_buffer, + &name_buffer_size), + SyscallSucceeds()); + EXPECT_EQ(name_buffer_size, interface_name().size() + 1); + EXPECT_STREQ(name_buffer, interface_name().c_str()); + + // Clear it successfully. + memset(name_buffer, kIllegalIfnameChar, sizeof(name_buffer)); + name_buffer[0] = 0; + name_buffer_size = sizeof(name_buffer); + EXPECT_THAT(setsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, name_buffer, + name_buffer_size), + SyscallSucceeds()); + + // Read it back successfully, it's cleared. + memset(name_buffer, kIllegalIfnameChar, sizeof(name_buffer)); + name_buffer_size = sizeof(name_buffer); + EXPECT_THAT(getsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, name_buffer, + &name_buffer_size), + SyscallSucceeds()); + EXPECT_EQ(name_buffer_size, 0); +} + +// Tests getsockopt with different buffer sizes. +TEST_P(BindToDeviceTest, GetsockoptDevice) { + char name_buffer[IFNAMSIZ * 2]; + socklen_t name_buffer_size; + + // Write successfully. + strncpy(name_buffer, interface_name().c_str(), sizeof(name_buffer)); + ASSERT_THAT(setsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, name_buffer, + sizeof(name_buffer)), + SyscallSucceeds()); + + // Read it back at various buffer sizes. + for (size_t i = 0; i <= sizeof(name_buffer); i++) { + memset(name_buffer, kIllegalIfnameChar, sizeof(name_buffer)); + name_buffer_size = i; + SCOPED_TRACE(absl::StrCat("Buffer size: ", i)); + // Linux only allows a buffer at least IFNAMSIZ, even if less would suffice + // for this interface name. + if (name_buffer_size >= IFNAMSIZ) { + EXPECT_THAT(getsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, + name_buffer, &name_buffer_size), + SyscallSucceeds()); + EXPECT_EQ(name_buffer_size, interface_name().size() + 1); + EXPECT_STREQ(name_buffer, interface_name().c_str()); + } else { + EXPECT_THAT(getsockopt(socket_fd(), SOL_SOCKET, SO_BINDTODEVICE, + name_buffer, &name_buffer_size), + SyscallFailsWithErrno(EINVAL)); + EXPECT_EQ(name_buffer_size, i); + } + } +} + +INSTANTIATE_TEST_SUITE_P(BindToDeviceTest, BindToDeviceTest, + ::testing::Values(IPv4UDPUnboundSocket(0), + IPv4TCPUnboundSocket(0))); + +} // namespace testing +} // namespace gvisor diff --git a/test/syscalls/linux/socket_bind_to_device_distribution.cc b/test/syscalls/linux/socket_bind_to_device_distribution.cc new file mode 100644 index 000000000..4d2400328 --- /dev/null +++ b/test/syscalls/linux/socket_bind_to_device_distribution.cc @@ -0,0 +1,381 @@ +// Copyright 2019 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. + +#include <arpa/inet.h> +#include <linux/if_tun.h> +#include <net/if.h> +#include <netinet/in.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/un.h> + +#include <atomic> +#include <cstdio> +#include <cstring> +#include <map> +#include <memory> +#include <unordered_map> +#include <unordered_set> +#include <utility> +#include <vector> + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "gtest/gtest.h" +#include "test/syscalls/linux/ip_socket_test_util.h" +#include "test/syscalls/linux/socket_bind_to_device_util.h" +#include "test/syscalls/linux/socket_test_util.h" +#include "test/util/capability_util.h" +#include "test/util/test_util.h" +#include "test/util/thread_util.h" + +namespace gvisor { +namespace testing { + +using std::string; +using std::vector; + +struct EndpointConfig { + std::string bind_to_device; + double expected_ratio; +}; + +struct DistributionTestCase { + std::string name; + std::vector<EndpointConfig> endpoints; +}; + +struct ListenerConnector { + TestAddress listener; + TestAddress connector; +}; + +// Test fixture for SO_BINDTODEVICE tests the distribution of packets received +// with varying SO_BINDTODEVICE settings. +class BindToDeviceDistributionTest + : public ::testing::TestWithParam< + ::testing::tuple<ListenerConnector, DistributionTestCase>> { + protected: + void SetUp() override { + printf("Testing case: %s, listener=%s, connector=%s\n", + ::testing::get<1>(GetParam()).name.c_str(), + ::testing::get<0>(GetParam()).listener.description.c_str(), + ::testing::get<0>(GetParam()).connector.description.c_str()); + ASSERT_TRUE(ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))) + << "CAP_NET_RAW is required to use SO_BINDTODEVICE"; + } +}; + +PosixErrorOr<uint16_t> AddrPort(int family, sockaddr_storage const& addr) { + switch (family) { + case AF_INET: + return static_cast<uint16_t>( + reinterpret_cast<sockaddr_in const*>(&addr)->sin_port); + case AF_INET6: + return static_cast<uint16_t>( + reinterpret_cast<sockaddr_in6 const*>(&addr)->sin6_port); + default: + return PosixError(EINVAL, + absl::StrCat("unknown socket family: ", family)); + } +} + +PosixError SetAddrPort(int family, sockaddr_storage* addr, uint16_t port) { + switch (family) { + case AF_INET: + reinterpret_cast<sockaddr_in*>(addr)->sin_port = port; + return NoError(); + case AF_INET6: + reinterpret_cast<sockaddr_in6*>(addr)->sin6_port = port; + return NoError(); + default: + return PosixError(EINVAL, + absl::StrCat("unknown socket family: ", family)); + } +} + +// Binds sockets to different devices and then creates many TCP connections. +// Checks that the distribution of connections received on the sockets matches +// the expectation. +TEST_P(BindToDeviceDistributionTest, Tcp) { + auto const& [listener_connector, test] = GetParam(); + + TestAddress const& listener = listener_connector.listener; + TestAddress const& connector = listener_connector.connector; + sockaddr_storage listen_addr = listener.addr; + sockaddr_storage conn_addr = connector.addr; + + auto interface_names = GetInterfaceNames(); + + // Create the listening sockets. + std::vector<FileDescriptor> listener_fds; + std::vector<std::unique_ptr<Tunnel>> all_tunnels; + for (auto const& endpoint : test.endpoints) { + if (!endpoint.bind_to_device.empty() && + interface_names.find(endpoint.bind_to_device) == + interface_names.end()) { + all_tunnels.push_back( + ASSERT_NO_ERRNO_AND_VALUE(Tunnel::New(endpoint.bind_to_device))); + interface_names.insert(endpoint.bind_to_device); + } + + listener_fds.push_back(ASSERT_NO_ERRNO_AND_VALUE( + Socket(listener.family(), SOCK_STREAM, IPPROTO_TCP))); + int fd = listener_fds.back().get(); + + ASSERT_THAT(setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &kSockOptOn, + sizeof(kSockOptOn)), + SyscallSucceeds()); + ASSERT_THAT(setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, + endpoint.bind_to_device.c_str(), + endpoint.bind_to_device.size() + 1), + SyscallSucceeds()); + ASSERT_THAT( + bind(fd, reinterpret_cast<sockaddr*>(&listen_addr), listener.addr_len), + SyscallSucceeds()); + ASSERT_THAT(listen(fd, 40), SyscallSucceeds()); + + // On the first bind we need to determine which port was bound. + if (listener_fds.size() > 1) { + continue; + } + + // Get the port bound by the listening socket. + socklen_t addrlen = listener.addr_len; + ASSERT_THAT( + getsockname(listener_fds[0].get(), + reinterpret_cast<sockaddr*>(&listen_addr), &addrlen), + SyscallSucceeds()); + uint16_t const port = + ASSERT_NO_ERRNO_AND_VALUE(AddrPort(listener.family(), listen_addr)); + ASSERT_NO_ERRNO(SetAddrPort(connector.family(), &conn_addr, port)); + } + + constexpr int kConnectAttempts = 10000; + std::atomic<int> connects_received = ATOMIC_VAR_INIT(0); + std::vector<int> accept_counts(listener_fds.size(), 0); + std::vector<std::unique_ptr<ScopedThread>> listen_threads( + listener_fds.size()); + + for (int i = 0; i < listener_fds.size(); i++) { + listen_threads[i] = absl::make_unique<ScopedThread>( + [&listener_fds, &accept_counts, &connects_received, i, + kConnectAttempts]() { + do { + auto fd = Accept(listener_fds[i].get(), nullptr, nullptr); + if (!fd.ok()) { + // Another thread has shutdown our read side causing the accept to + // fail. + ASSERT_GE(connects_received, kConnectAttempts) + << "errno = " << fd.error(); + return; + } + // Receive some data from a socket to be sure that the connect() + // system call has been completed on another side. + int data; + EXPECT_THAT( + RetryEINTR(recv)(fd.ValueOrDie().get(), &data, sizeof(data), 0), + SyscallSucceedsWithValue(sizeof(data))); + accept_counts[i]++; + } while (++connects_received < kConnectAttempts); + + // Shutdown all sockets to wake up other threads. + for (auto const& listener_fd : listener_fds) { + shutdown(listener_fd.get(), SHUT_RDWR); + } + }); + } + + for (int i = 0; i < kConnectAttempts; i++) { + FileDescriptor const fd = ASSERT_NO_ERRNO_AND_VALUE( + Socket(connector.family(), SOCK_STREAM, IPPROTO_TCP)); + ASSERT_THAT( + RetryEINTR(connect)(fd.get(), reinterpret_cast<sockaddr*>(&conn_addr), + connector.addr_len), + SyscallSucceeds()); + + EXPECT_THAT(RetryEINTR(send)(fd.get(), &i, sizeof(i), 0), + SyscallSucceedsWithValue(sizeof(i))); + } + + // Join threads to be sure that all connections have been counted. + for (auto const& listen_thread : listen_threads) { + listen_thread->Join(); + } + // Check that connections are distributed correctly among listening sockets. + for (int i = 0; i < accept_counts.size(); i++) { + EXPECT_THAT( + accept_counts[i], + EquivalentWithin(static_cast<int>(kConnectAttempts * + test.endpoints[i].expected_ratio), + 0.10)) + << "endpoint " << i << " got the wrong number of packets"; + } +} + +// Binds sockets to different devices and then sends many UDP packets. Checks +// that the distribution of packets received on the sockets matches the +// expectation. +TEST_P(BindToDeviceDistributionTest, Udp) { + auto const& [listener_connector, test] = GetParam(); + + TestAddress const& listener = listener_connector.listener; + TestAddress const& connector = listener_connector.connector; + sockaddr_storage listen_addr = listener.addr; + sockaddr_storage conn_addr = connector.addr; + + auto interface_names = GetInterfaceNames(); + + // Create the listening socket. + std::vector<FileDescriptor> listener_fds; + std::vector<std::unique_ptr<Tunnel>> all_tunnels; + for (auto const& endpoint : test.endpoints) { + if (!endpoint.bind_to_device.empty() && + interface_names.find(endpoint.bind_to_device) == + interface_names.end()) { + all_tunnels.push_back( + ASSERT_NO_ERRNO_AND_VALUE(Tunnel::New(endpoint.bind_to_device))); + interface_names.insert(endpoint.bind_to_device); + } + + listener_fds.push_back( + ASSERT_NO_ERRNO_AND_VALUE(Socket(listener.family(), SOCK_DGRAM, 0))); + int fd = listener_fds.back().get(); + + ASSERT_THAT(setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &kSockOptOn, + sizeof(kSockOptOn)), + SyscallSucceeds()); + ASSERT_THAT(setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, + endpoint.bind_to_device.c_str(), + endpoint.bind_to_device.size() + 1), + SyscallSucceeds()); + ASSERT_THAT( + bind(fd, reinterpret_cast<sockaddr*>(&listen_addr), listener.addr_len), + SyscallSucceeds()); + + // On the first bind we need to determine which port was bound. + if (listener_fds.size() > 1) { + continue; + } + + // Get the port bound by the listening socket. + socklen_t addrlen = listener.addr_len; + ASSERT_THAT( + getsockname(listener_fds[0].get(), + reinterpret_cast<sockaddr*>(&listen_addr), &addrlen), + SyscallSucceeds()); + uint16_t const port = + ASSERT_NO_ERRNO_AND_VALUE(AddrPort(listener.family(), listen_addr)); + ASSERT_NO_ERRNO(SetAddrPort(listener.family(), &listen_addr, port)); + ASSERT_NO_ERRNO(SetAddrPort(connector.family(), &conn_addr, port)); + } + + constexpr int kConnectAttempts = 10000; + std::atomic<int> packets_received = ATOMIC_VAR_INIT(0); + std::vector<int> packets_per_socket(listener_fds.size(), 0); + std::vector<std::unique_ptr<ScopedThread>> receiver_threads( + listener_fds.size()); + + for (int i = 0; i < listener_fds.size(); i++) { + receiver_threads[i] = absl::make_unique<ScopedThread>( + [&listener_fds, &packets_per_socket, &packets_received, i]() { + do { + struct sockaddr_storage addr = {}; + socklen_t addrlen = sizeof(addr); + int data; + + auto ret = RetryEINTR(recvfrom)( + listener_fds[i].get(), &data, sizeof(data), 0, + reinterpret_cast<struct sockaddr*>(&addr), &addrlen); + + if (packets_received < kConnectAttempts) { + ASSERT_THAT(ret, SyscallSucceedsWithValue(sizeof(data))); + } + + if (ret != sizeof(data)) { + // Another thread may have shutdown our read side causing the + // recvfrom to fail. + break; + } + + packets_received++; + packets_per_socket[i]++; + + // A response is required to synchronize with the main thread, + // otherwise the main thread can send more than can fit into receive + // queues. + EXPECT_THAT(RetryEINTR(sendto)( + listener_fds[i].get(), &data, sizeof(data), 0, + reinterpret_cast<sockaddr*>(&addr), addrlen), + SyscallSucceedsWithValue(sizeof(data))); + } while (packets_received < kConnectAttempts); + + // Shutdown all sockets to wake up other threads. + for (auto const& listener_fd : listener_fds) { + shutdown(listener_fd.get(), SHUT_RDWR); + } + }); + } + + for (int i = 0; i < kConnectAttempts; i++) { + FileDescriptor const fd = + ASSERT_NO_ERRNO_AND_VALUE(Socket(connector.family(), SOCK_DGRAM, 0)); + EXPECT_THAT(RetryEINTR(sendto)(fd.get(), &i, sizeof(i), 0, + reinterpret_cast<sockaddr*>(&conn_addr), + connector.addr_len), + SyscallSucceedsWithValue(sizeof(i))); + int data; + EXPECT_THAT(RetryEINTR(recv)(fd.get(), &data, sizeof(data), 0), + SyscallSucceedsWithValue(sizeof(data))); + } + + // Join threads to be sure that all connections have been counted. + for (auto const& receiver_thread : receiver_threads) { + receiver_thread->Join(); + } + // Check that packets are distributed correctly among listening sockets. + for (int i = 0; i < packets_per_socket.size(); i++) { + EXPECT_THAT( + packets_per_socket[i], + EquivalentWithin(static_cast<int>(kConnectAttempts * + test.endpoints[i].expected_ratio), + 0.10)) + << "endpoint " << i << " got the wrong number of packets"; + } +} + +std::vector<DistributionTestCase> GetDistributionTestCases() { + return std::vector<DistributionTestCase>{ + {"Even distribution among sockets not bound to device", + {{"", 1. / 3}, {"", 1. / 3}, {"", 1. / 3}}}, + {"Sockets bound to other interfaces get no packets", + {{"eth1", 0}, {"", 1. / 2}, {"", 1. / 2}}}, + {"Bound has priority over unbound", {{"eth1", 0}, {"", 0}, {"lo", 1}}}, + {"Even distribution among sockets bound to device", + {{"eth1", 0}, {"lo", 1. / 2}, {"lo", 1. / 2}}}, + }; +} + +INSTANTIATE_TEST_SUITE_P( + BindToDeviceTest, BindToDeviceDistributionTest, + ::testing::Combine(::testing::Values( + // Listeners bound to IPv4 addresses refuse + // connections using IPv6 addresses. + ListenerConnector{V4Any(), V4Loopback()}, + ListenerConnector{V4Loopback(), V4MappedLoopback()}), + ::testing::ValuesIn(GetDistributionTestCases()))); + +} // namespace testing +} // namespace gvisor diff --git a/test/syscalls/linux/socket_bind_to_device_sequence.cc b/test/syscalls/linux/socket_bind_to_device_sequence.cc new file mode 100644 index 000000000..a7365d139 --- /dev/null +++ b/test/syscalls/linux/socket_bind_to_device_sequence.cc @@ -0,0 +1,316 @@ +// Copyright 2019 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. + +#include <arpa/inet.h> +#include <linux/capability.h> +#include <linux/if_tun.h> +#include <net/if.h> +#include <netinet/in.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/un.h> + +#include <cstdio> +#include <cstring> +#include <map> +#include <memory> +#include <unordered_map> +#include <unordered_set> +#include <utility> +#include <vector> + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "gtest/gtest.h" +#include "test/syscalls/linux/ip_socket_test_util.h" +#include "test/syscalls/linux/socket_bind_to_device_util.h" +#include "test/syscalls/linux/socket_test_util.h" +#include "test/util/capability_util.h" +#include "test/util/test_util.h" +#include "test/util/thread_util.h" + +namespace gvisor { +namespace testing { + +using std::string; +using std::vector; + +// Test fixture for SO_BINDTODEVICE tests the results of sequences of socket +// binding. +class BindToDeviceSequenceTest : public ::testing::TestWithParam<SocketKind> { + protected: + void SetUp() override { + printf("Testing case: %s\n", GetParam().description.c_str()); + ASSERT_TRUE(ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW))) + << "CAP_NET_RAW is required to use SO_BINDTODEVICE"; + socket_factory_ = GetParam(); + + interface_names_ = GetInterfaceNames(); + } + + PosixErrorOr<std::unique_ptr<FileDescriptor>> NewSocket() const { + return socket_factory_.Create(); + } + + // Gets a device by device_id. If the device_id has been seen before, returns + // the previously returned device. If not, finds or creates a new device. + // Returns an empty string on failure. + void GetDevice(int device_id, string *device_name) { + auto device = devices_.find(device_id); + if (device != devices_.end()) { + *device_name = device->second; + return; + } + + // Need to pick a new device. Try ethernet first. + *device_name = absl::StrCat("eth", next_unused_eth_); + if (interface_names_.find(*device_name) != interface_names_.end()) { + devices_[device_id] = *device_name; + next_unused_eth_++; + return; + } + + // Need to make a new tunnel device. gVisor tests should have enough + // ethernet devices to never reach here. + ASSERT_FALSE(IsRunningOnGvisor()); + // Need a tunnel. + tunnels_.push_back(ASSERT_NO_ERRNO_AND_VALUE(Tunnel::New())); + devices_[device_id] = tunnels_.back()->GetName(); + *device_name = devices_[device_id]; + } + + // Release the socket + void ReleaseSocket(int socket_id) { + // Close the socket that was made in a previous action. The socket_id + // indicates which socket to close based on index into the list of actions. + sockets_to_close_.erase(socket_id); + } + + // Bind a socket with the reuse option and bind_to_device options. Checks + // that all steps succeed and that the bind command's error matches want. + // Sets the socket_id to uniquely identify the socket bound if it is not + // nullptr. + void BindSocket(bool reuse, int device_id = 0, int want = 0, + int *socket_id = nullptr) { + next_socket_id_++; + sockets_to_close_[next_socket_id_] = ASSERT_NO_ERRNO_AND_VALUE(NewSocket()); + auto socket_fd = sockets_to_close_[next_socket_id_]->get(); + if (socket_id != nullptr) { + *socket_id = next_socket_id_; + } + + // If reuse is indicated, do that. + if (reuse) { + EXPECT_THAT(setsockopt(socket_fd, SOL_SOCKET, SO_REUSEPORT, &kSockOptOn, + sizeof(kSockOptOn)), + SyscallSucceedsWithValue(0)); + } + + // If the device is non-zero, bind to that device. + if (device_id != 0) { + string device_name; + ASSERT_NO_FATAL_FAILURE(GetDevice(device_id, &device_name)); + EXPECT_THAT(setsockopt(socket_fd, SOL_SOCKET, SO_BINDTODEVICE, + device_name.c_str(), device_name.size() + 1), + SyscallSucceedsWithValue(0)); + char get_device[100]; + socklen_t get_device_size = 100; + EXPECT_THAT(getsockopt(socket_fd, SOL_SOCKET, SO_BINDTODEVICE, get_device, + &get_device_size), + SyscallSucceedsWithValue(0)); + } + + struct sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = port_; + if (want == 0) { + ASSERT_THAT( + bind(socket_fd, reinterpret_cast<const struct sockaddr *>(&addr), + sizeof(addr)), + SyscallSucceeds()); + } else { + ASSERT_THAT( + bind(socket_fd, reinterpret_cast<const struct sockaddr *>(&addr), + sizeof(addr)), + SyscallFailsWithErrno(want)); + } + + if (port_ == 0) { + // We don't yet know what port we'll be using so we need to fetch it and + // remember it for future commands. + socklen_t addr_size = sizeof(addr); + ASSERT_THAT( + getsockname(socket_fd, reinterpret_cast<struct sockaddr *>(&addr), + &addr_size), + SyscallSucceeds()); + port_ = addr.sin_port; + } + } + + private: + SocketKind socket_factory_; + // devices maps from the device id in the test case to the name of the device. + std::unordered_map<int, string> devices_; + // These are the tunnels that were created for the test and will be destroyed + // by the destructor. + vector<std::unique_ptr<Tunnel>> tunnels_; + // A list of all interface names before the test started. + std::unordered_set<string> interface_names_; + // The next ethernet device to use when requested a device. + int next_unused_eth_ = 1; + // The port for all tests. Originally 0 (any) and later set to the port that + // all further commands will use. + in_port_t port_ = 0; + // sockets_to_close_ is a map from action index to the socket that was + // created. + std::unordered_map<int, + std::unique_ptr<gvisor::testing::FileDescriptor>> + sockets_to_close_; + int next_socket_id_ = 0; +}; + +TEST_P(BindToDeviceSequenceTest, BindTwiceWithDeviceFails) { + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 3)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 3, EADDRINUSE)); +} + +TEST_P(BindToDeviceSequenceTest, BindToDevice) { + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 1)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 2)); +} + +TEST_P(BindToDeviceSequenceTest, BindToDeviceAndThenWithoutDevice) { + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 123)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 0, EADDRINUSE)); +} + +TEST_P(BindToDeviceSequenceTest, BindWithoutDevice) { + ASSERT_NO_FATAL_FAILURE(BindSocket(/* reuse */ false)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 123, EADDRINUSE)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 123, EADDRINUSE)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 0, EADDRINUSE)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 0, EADDRINUSE)); +} + +TEST_P(BindToDeviceSequenceTest, BindWithDevice) { + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 123, 0)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 123, EADDRINUSE)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 123, EADDRINUSE)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 0, EADDRINUSE)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 0, EADDRINUSE)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 456, 0)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 789, 0)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 0, EADDRINUSE)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 0, EADDRINUSE)); +} + +TEST_P(BindToDeviceSequenceTest, BindWithReuse) { + ASSERT_NO_FATAL_FAILURE(BindSocket(/* reuse */ true)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 123, EADDRINUSE)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 123)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 0, EADDRINUSE)); + ASSERT_NO_FATAL_FAILURE(BindSocket(/* reuse */ true, /* bind_to_device */ 0)); +} + +TEST_P(BindToDeviceSequenceTest, BindingWithReuseAndDevice) { + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 123)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 123, EADDRINUSE)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 123)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 0, EADDRINUSE)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 456)); + ASSERT_NO_FATAL_FAILURE(BindSocket(/* reuse */ true)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 789)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 999, EADDRINUSE)); +} + +TEST_P(BindToDeviceSequenceTest, MixingReuseAndNotReuseByBindingToDevice) { + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 123, 0)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 456, 0)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 789, 0)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 999, 0)); +} + +TEST_P(BindToDeviceSequenceTest, CannotBindTo0AfterMixingReuseAndNotReuse) { + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 123)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 456)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 0, EADDRINUSE)); +} + +TEST_P(BindToDeviceSequenceTest, BindAndRelease) { + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 123)); + int to_release; + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 0, 0, &to_release)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 345, EADDRINUSE)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 789)); + // Release the bind to device 0 and try again. + ASSERT_NO_FATAL_FAILURE(ReleaseSocket(to_release)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 345)); +} + +TEST_P(BindToDeviceSequenceTest, BindTwiceWithReuseOnce) { + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ false, /* bind_to_device */ 123)); + ASSERT_NO_FATAL_FAILURE( + BindSocket(/* reuse */ true, /* bind_to_device */ 0, EADDRINUSE)); +} + +INSTANTIATE_TEST_SUITE_P(BindToDeviceTest, BindToDeviceSequenceTest, + ::testing::Values(IPv4UDPUnboundSocket(0), + IPv4TCPUnboundSocket(0))); + +} // namespace testing +} // namespace gvisor diff --git a/test/syscalls/linux/socket_bind_to_device_util.cc b/test/syscalls/linux/socket_bind_to_device_util.cc new file mode 100644 index 000000000..f4ee775bd --- /dev/null +++ b/test/syscalls/linux/socket_bind_to_device_util.cc @@ -0,0 +1,75 @@ +// Copyright 2019 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. + +#include "test/syscalls/linux/socket_bind_to_device_util.h" + +#include <arpa/inet.h> +#include <fcntl.h> +#include <linux/if_tun.h> +#include <net/if.h> +#include <netinet/in.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/un.h> +#include <unistd.h> + +#include <cstdio> +#include <cstring> +#include <map> +#include <memory> +#include <unordered_map> +#include <unordered_set> +#include <utility> +#include <vector> + +#include "test/util/test_util.h" + +namespace gvisor { +namespace testing { + +using std::string; + +PosixErrorOr<std::unique_ptr<Tunnel>> Tunnel::New(string tunnel_name) { + int fd; + RETURN_ERROR_IF_SYSCALL_FAIL(fd = open("/dev/net/tun", O_RDWR)); + + // Using `new` to access a non-public constructor. + auto new_tunnel = absl::WrapUnique(new Tunnel(fd)); + + ifreq ifr = {}; + ifr.ifr_flags = IFF_TUN; + strncpy(ifr.ifr_name, tunnel_name.c_str(), sizeof(ifr.ifr_name)); + + RETURN_ERROR_IF_SYSCALL_FAIL(ioctl(fd, TUNSETIFF, &ifr)); + new_tunnel->name_ = ifr.ifr_name; + return new_tunnel; +} + +std::unordered_set<string> GetInterfaceNames() { + struct if_nameindex* interfaces = if_nameindex(); + std::unordered_set<string> names; + if (interfaces == nullptr) { + return names; + } + for (auto interface = interfaces; + interface->if_index != 0 || interface->if_name != nullptr; interface++) { + names.insert(interface->if_name); + } + if_freenameindex(interfaces); + return names; +} + +} // namespace testing +} // namespace gvisor diff --git a/test/syscalls/linux/socket_bind_to_device_util.h b/test/syscalls/linux/socket_bind_to_device_util.h new file mode 100644 index 000000000..f941ccc86 --- /dev/null +++ b/test/syscalls/linux/socket_bind_to_device_util.h @@ -0,0 +1,67 @@ +// Copyright 2019 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. + +#ifndef GVISOR_TEST_SYSCALLS_SOCKET_BIND_TO_DEVICE_UTILS_H_ +#define GVISOR_TEST_SYSCALLS_SOCKET_BIND_TO_DEVICE_UTILS_H_ + +#include <arpa/inet.h> +#include <linux/if_tun.h> +#include <net/if.h> +#include <netinet/in.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/un.h> +#include <unistd.h> + +#include <cstdio> +#include <cstring> +#include <map> +#include <memory> +#include <string> +#include <unordered_map> +#include <unordered_set> +#include <utility> +#include <vector> + +#include "absl/memory/memory.h" +#include "test/util/test_util.h" + +namespace gvisor { +namespace testing { + +class Tunnel { + public: + static PosixErrorOr<std::unique_ptr<Tunnel>> New( + std::string tunnel_name = ""); + const std::string& GetName() const { return name_; } + + ~Tunnel() { + if (fd_ != -1) { + close(fd_); + } + } + + private: + Tunnel(int fd) : fd_(fd) {} + int fd_ = -1; + std::string name_; +}; + +std::unordered_set<std::string> GetInterfaceNames(); + +} // namespace testing +} // namespace gvisor + +#endif // GVISOR_TEST_SYSCALLS_SOCKET_BIND_TO_DEVICE_UTILS_H_ diff --git a/test/syscalls/linux/socket_ip_tcp_generic.cc b/test/syscalls/linux/socket_ip_tcp_generic.cc index a43cf9bce..bfa7943b1 100644 --- a/test/syscalls/linux/socket_ip_tcp_generic.cc +++ b/test/syscalls/linux/socket_ip_tcp_generic.cc @@ -117,7 +117,7 @@ TEST_P(TCPSocketPairTest, RSTCausesPollHUP) { struct pollfd poll_fd3 = {sockets->first_fd(), POLLHUP, 0}; ASSERT_THAT(RetryEINTR(poll)(&poll_fd3, 1, kPollTimeoutMs), SyscallSucceedsWithValue(1)); - ASSERT_NE(poll_fd.revents & (POLLHUP | POLLIN), 0); + ASSERT_NE(poll_fd3.revents & POLLHUP, 0); } // This test validates that even if a RST is sent the other end will not diff --git a/test/syscalls/linux/socket_test_util.cc b/test/syscalls/linux/socket_test_util.cc index 3c716235b..eff7d577e 100644 --- a/test/syscalls/linux/socket_test_util.cc +++ b/test/syscalls/linux/socket_test_util.cc @@ -588,8 +588,9 @@ ssize_t SendLargeSendMsg(const std::unique_ptr<SocketPair>& sockets, return RetryEINTR(sendmsg)(sockets->first_fd(), &msg, 0); } -PosixErrorOr<int> PortAvailable(int port, AddressFamily family, SocketType type, - bool reuse_addr) { +namespace internal { +PosixErrorOr<int> TryPortAvailable(int port, AddressFamily family, + SocketType type, bool reuse_addr) { if (port < 0) { return PosixError(EINVAL, "Invalid port"); } @@ -664,10 +665,7 @@ PosixErrorOr<int> PortAvailable(int port, AddressFamily family, SocketType type, return available_port; } - -PosixError FreeAvailablePort(int port) { - return NoError(); -} +} // namespace internal PosixErrorOr<int> SendMsg(int sock, msghdr* msg, char buf[], int buf_size) { struct iovec iov; diff --git a/test/syscalls/linux/socket_test_util.h b/test/syscalls/linux/socket_test_util.h index ae0da2679..6efa8055f 100644 --- a/test/syscalls/linux/socket_test_util.h +++ b/test/syscalls/linux/socket_test_util.h @@ -492,6 +492,11 @@ uint16_t UDPChecksum(struct iphdr iphdr, struct udphdr udphdr, uint16_t ICMPChecksum(struct icmphdr icmphdr, const char* payload, ssize_t payload_len); +namespace internal { +PosixErrorOr<int> TryPortAvailable(int port, AddressFamily family, + SocketType type, bool reuse_addr); +} // namespace internal + } // namespace testing } // namespace gvisor diff --git a/test/syscalls/linux/socket_test_util_impl.cc b/test/syscalls/linux/socket_test_util_impl.cc new file mode 100644 index 000000000..ef661a0e3 --- /dev/null +++ b/test/syscalls/linux/socket_test_util_impl.cc @@ -0,0 +1,28 @@ +// Copyright 2019 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. + +#include "test/syscalls/linux/socket_test_util.h" + +namespace gvisor { +namespace testing { + +PosixErrorOr<int> PortAvailable(int port, AddressFamily family, SocketType type, + bool reuse_addr) { + return internal::TryPortAvailable(port, family, type, reuse_addr); +} + +PosixError FreeAvailablePort(int port) { return NoError(); } + +} // namespace testing +} // namespace gvisor diff --git a/test/syscalls/linux/splice.cc b/test/syscalls/linux/splice.cc index e25f264f6..85232cb1f 100644 --- a/test/syscalls/linux/splice.cc +++ b/test/syscalls/linux/splice.cc @@ -14,12 +14,16 @@ #include <fcntl.h> #include <sys/eventfd.h> +#include <sys/resource.h> #include <sys/sendfile.h> +#include <sys/time.h> #include <unistd.h> #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/strings/string_view.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" #include "test/util/file_descriptor.h" #include "test/util/temp_path.h" #include "test/util/test_util.h" @@ -36,23 +40,23 @@ TEST(SpliceTest, TwoRegularFiles) { const TempPath out_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); // Open the input file as read only. - const FileDescriptor inf = + const FileDescriptor in_fd = ASSERT_NO_ERRNO_AND_VALUE(Open(in_file.path(), O_RDONLY)); // Open the output file as write only. - const FileDescriptor outf = + const FileDescriptor out_fd = ASSERT_NO_ERRNO_AND_VALUE(Open(out_file.path(), O_WRONLY)); // Verify that it is rejected as expected; regardless of offsets. loff_t in_offset = 0; loff_t out_offset = 0; - EXPECT_THAT(splice(inf.get(), &in_offset, outf.get(), &out_offset, 1, 0), + EXPECT_THAT(splice(in_fd.get(), &in_offset, out_fd.get(), &out_offset, 1, 0), SyscallFailsWithErrno(EINVAL)); - EXPECT_THAT(splice(inf.get(), nullptr, outf.get(), &out_offset, 1, 0), + EXPECT_THAT(splice(in_fd.get(), nullptr, out_fd.get(), &out_offset, 1, 0), SyscallFailsWithErrno(EINVAL)); - EXPECT_THAT(splice(inf.get(), &in_offset, outf.get(), nullptr, 1, 0), + EXPECT_THAT(splice(in_fd.get(), &in_offset, out_fd.get(), nullptr, 1, 0), SyscallFailsWithErrno(EINVAL)); - EXPECT_THAT(splice(inf.get(), nullptr, outf.get(), nullptr, 1, 0), + EXPECT_THAT(splice(in_fd.get(), nullptr, out_fd.get(), nullptr, 1, 0), SyscallFailsWithErrno(EINVAL)); } @@ -75,8 +79,6 @@ TEST(SpliceTest, SamePipe) { } TEST(TeeTest, SamePipe) { - SKIP_IF(IsRunningOnGvisor()); - // Create a new pipe. int fds[2]; ASSERT_THAT(pipe(fds), SyscallSucceeds()); @@ -95,11 +97,9 @@ TEST(TeeTest, SamePipe) { } TEST(TeeTest, RegularFile) { - SKIP_IF(IsRunningOnGvisor()); - // Open some file. const TempPath in_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); - const FileDescriptor inf = + const FileDescriptor in_fd = ASSERT_NO_ERRNO_AND_VALUE(Open(in_file.path(), O_RDWR)); // Create a new pipe. @@ -109,9 +109,9 @@ TEST(TeeTest, RegularFile) { const FileDescriptor wfd(fds[1]); // Attempt to tee from the file. - EXPECT_THAT(tee(inf.get(), wfd.get(), kPageSize, 0), + EXPECT_THAT(tee(in_fd.get(), wfd.get(), kPageSize, 0), SyscallFailsWithErrno(EINVAL)); - EXPECT_THAT(tee(rfd.get(), inf.get(), kPageSize, 0), + EXPECT_THAT(tee(rfd.get(), in_fd.get(), kPageSize, 0), SyscallFailsWithErrno(EINVAL)); } @@ -142,7 +142,7 @@ TEST(SpliceTest, FromEventFD) { constexpr uint64_t kEventFDValue = 1; int efd; ASSERT_THAT(efd = eventfd(kEventFDValue, 0), SyscallSucceeds()); - const FileDescriptor inf(efd); + const FileDescriptor in_fd(efd); // Create a new pipe. int fds[2]; @@ -152,7 +152,7 @@ TEST(SpliceTest, FromEventFD) { // Splice 8-byte eventfd value to pipe. constexpr int kEventFDSize = 8; - EXPECT_THAT(splice(inf.get(), nullptr, wfd.get(), nullptr, kEventFDSize, 0), + EXPECT_THAT(splice(in_fd.get(), nullptr, wfd.get(), nullptr, kEventFDSize, 0), SyscallSucceedsWithValue(kEventFDSize)); // Contents should be equal. @@ -166,7 +166,7 @@ TEST(SpliceTest, FromEventFD) { TEST(SpliceTest, FromEventFDOffset) { int efd; ASSERT_THAT(efd = eventfd(0, 0), SyscallSucceeds()); - const FileDescriptor inf(efd); + const FileDescriptor in_fd(efd); // Create a new pipe. int fds[2]; @@ -179,7 +179,7 @@ TEST(SpliceTest, FromEventFDOffset) { // This is not allowed because eventfd doesn't support pread. constexpr int kEventFDSize = 8; loff_t in_off = 0; - EXPECT_THAT(splice(inf.get(), &in_off, wfd.get(), nullptr, kEventFDSize, 0), + EXPECT_THAT(splice(in_fd.get(), &in_off, wfd.get(), nullptr, kEventFDSize, 0), SyscallFailsWithErrno(EINVAL)); } @@ -200,28 +200,29 @@ TEST(SpliceTest, ToEventFDOffset) { int efd; ASSERT_THAT(efd = eventfd(0, 0), SyscallSucceeds()); - const FileDescriptor outf(efd); + const FileDescriptor out_fd(efd); // Attempt to splice 8-byte eventfd value to pipe with offset. // // This is not allowed because eventfd doesn't support pwrite. loff_t out_off = 0; - EXPECT_THAT(splice(rfd.get(), nullptr, outf.get(), &out_off, kEventFDSize, 0), - SyscallFailsWithErrno(EINVAL)); + EXPECT_THAT( + splice(rfd.get(), nullptr, out_fd.get(), &out_off, kEventFDSize, 0), + SyscallFailsWithErrno(EINVAL)); } TEST(SpliceTest, ToPipe) { // Open the input file. const TempPath in_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); - const FileDescriptor inf = + const FileDescriptor in_fd = ASSERT_NO_ERRNO_AND_VALUE(Open(in_file.path(), O_RDWR)); // Fill with some random data. std::vector<char> buf(kPageSize); RandomizeBuffer(buf.data(), buf.size()); - ASSERT_THAT(write(inf.get(), buf.data(), buf.size()), + ASSERT_THAT(write(in_fd.get(), buf.data(), buf.size()), SyscallSucceedsWithValue(kPageSize)); - ASSERT_THAT(lseek(inf.get(), 0, SEEK_SET), SyscallSucceedsWithValue(0)); + ASSERT_THAT(lseek(in_fd.get(), 0, SEEK_SET), SyscallSucceedsWithValue(0)); // Create a new pipe. int fds[2]; @@ -230,7 +231,7 @@ TEST(SpliceTest, ToPipe) { const FileDescriptor wfd(fds[1]); // Splice to the pipe. - EXPECT_THAT(splice(inf.get(), nullptr, wfd.get(), nullptr, kPageSize, 0), + EXPECT_THAT(splice(in_fd.get(), nullptr, wfd.get(), nullptr, kPageSize, 0), SyscallSucceedsWithValue(kPageSize)); // Contents should be equal. @@ -243,13 +244,13 @@ TEST(SpliceTest, ToPipe) { TEST(SpliceTest, ToPipeOffset) { // Open the input file. const TempPath in_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); - const FileDescriptor inf = + const FileDescriptor in_fd = ASSERT_NO_ERRNO_AND_VALUE(Open(in_file.path(), O_RDWR)); // Fill with some random data. std::vector<char> buf(kPageSize); RandomizeBuffer(buf.data(), buf.size()); - ASSERT_THAT(write(inf.get(), buf.data(), buf.size()), + ASSERT_THAT(write(in_fd.get(), buf.data(), buf.size()), SyscallSucceedsWithValue(kPageSize)); // Create a new pipe. @@ -261,7 +262,7 @@ TEST(SpliceTest, ToPipeOffset) { // Splice to the pipe. loff_t in_offset = kPageSize / 2; EXPECT_THAT( - splice(inf.get(), &in_offset, wfd.get(), nullptr, kPageSize / 2, 0), + splice(in_fd.get(), &in_offset, wfd.get(), nullptr, kPageSize / 2, 0), SyscallSucceedsWithValue(kPageSize / 2)); // Contents should be equal to only the second part. @@ -286,22 +287,22 @@ TEST(SpliceTest, FromPipe) { // Open the input file. const TempPath out_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); - const FileDescriptor outf = + const FileDescriptor out_fd = ASSERT_NO_ERRNO_AND_VALUE(Open(out_file.path(), O_RDWR)); // Splice to the output file. - EXPECT_THAT(splice(rfd.get(), nullptr, outf.get(), nullptr, kPageSize, 0), + EXPECT_THAT(splice(rfd.get(), nullptr, out_fd.get(), nullptr, kPageSize, 0), SyscallSucceedsWithValue(kPageSize)); // The offset of the output should be equal to kPageSize. We assert that and // reset to zero so that we can read the contents and ensure they match. - EXPECT_THAT(lseek(outf.get(), 0, SEEK_CUR), + EXPECT_THAT(lseek(out_fd.get(), 0, SEEK_CUR), SyscallSucceedsWithValue(kPageSize)); - ASSERT_THAT(lseek(outf.get(), 0, SEEK_SET), SyscallSucceedsWithValue(0)); + ASSERT_THAT(lseek(out_fd.get(), 0, SEEK_SET), SyscallSucceedsWithValue(0)); // Contents should be equal. std::vector<char> rbuf(kPageSize); - ASSERT_THAT(read(outf.get(), rbuf.data(), rbuf.size()), + ASSERT_THAT(read(out_fd.get(), rbuf.data(), rbuf.size()), SyscallSucceedsWithValue(kPageSize)); EXPECT_EQ(memcmp(rbuf.data(), buf.data(), buf.size()), 0); } @@ -321,18 +322,19 @@ TEST(SpliceTest, FromPipeOffset) { // Open the input file. const TempPath out_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); - const FileDescriptor outf = + const FileDescriptor out_fd = ASSERT_NO_ERRNO_AND_VALUE(Open(out_file.path(), O_RDWR)); // Splice to the output file. loff_t out_offset = kPageSize / 2; - EXPECT_THAT(splice(rfd.get(), nullptr, outf.get(), &out_offset, kPageSize, 0), - SyscallSucceedsWithValue(kPageSize)); + EXPECT_THAT( + splice(rfd.get(), nullptr, out_fd.get(), &out_offset, kPageSize, 0), + SyscallSucceedsWithValue(kPageSize)); // Content should reflect the splice. We write to a specific offset in the // file, so the internals should now be allocated sparsely. std::vector<char> rbuf(kPageSize); - ASSERT_THAT(read(outf.get(), rbuf.data(), rbuf.size()), + ASSERT_THAT(read(out_fd.get(), rbuf.data(), rbuf.size()), SyscallSucceedsWithValue(kPageSize)); std::vector<char> zbuf(kPageSize / 2); memset(zbuf.data(), 0, zbuf.size()); @@ -404,8 +406,6 @@ TEST(SpliceTest, Blocking) { } TEST(TeeTest, Blocking) { - SKIP_IF(IsRunningOnGvisor()); - // Create two new pipes. int first[2], second[2]; ASSERT_THAT(pipe(first), SyscallSucceeds()); @@ -440,6 +440,49 @@ TEST(TeeTest, Blocking) { EXPECT_EQ(memcmp(rbuf.data(), buf.data(), kPageSize), 0); } +TEST(TeeTest, BlockingWrite) { + // Create two new pipes. + int first[2], second[2]; + ASSERT_THAT(pipe(first), SyscallSucceeds()); + const FileDescriptor rfd1(first[0]); + const FileDescriptor wfd1(first[1]); + ASSERT_THAT(pipe(second), SyscallSucceeds()); + const FileDescriptor rfd2(second[0]); + const FileDescriptor wfd2(second[1]); + + // Make some data available to be read. + std::vector<char> buf1(kPageSize); + RandomizeBuffer(buf1.data(), buf1.size()); + ASSERT_THAT(write(wfd1.get(), buf1.data(), buf1.size()), + SyscallSucceedsWithValue(kPageSize)); + + // Fill up the write pipe's buffer. + int pipe_size = -1; + ASSERT_THAT(pipe_size = fcntl(wfd2.get(), F_GETPIPE_SZ), SyscallSucceeds()); + std::vector<char> buf2(pipe_size); + ASSERT_THAT(write(wfd2.get(), buf2.data(), buf2.size()), + SyscallSucceedsWithValue(pipe_size)); + + ScopedThread t([&]() { + absl::SleepFor(absl::Milliseconds(100)); + ASSERT_THAT(read(rfd2.get(), buf2.data(), buf2.size()), + SyscallSucceedsWithValue(pipe_size)); + }); + + // Attempt a tee immediately; it should block. + EXPECT_THAT(tee(rfd1.get(), wfd2.get(), kPageSize, 0), + SyscallSucceedsWithValue(kPageSize)); + + // Thread should be joinable. + t.Join(); + + // Content should reflect the tee. + std::vector<char> rbuf(kPageSize); + ASSERT_THAT(read(rfd2.get(), rbuf.data(), rbuf.size()), + SyscallSucceedsWithValue(kPageSize)); + EXPECT_EQ(memcmp(rbuf.data(), buf1.data(), kPageSize), 0); +} + TEST(SpliceTest, NonBlocking) { // Create two new pipes. int first[2], second[2]; @@ -457,8 +500,6 @@ TEST(SpliceTest, NonBlocking) { } TEST(TeeTest, NonBlocking) { - SKIP_IF(IsRunningOnGvisor()); - // Create two new pipes. int first[2], second[2]; ASSERT_THAT(pipe(first), SyscallSucceeds()); @@ -473,6 +514,79 @@ TEST(TeeTest, NonBlocking) { SyscallFailsWithErrno(EAGAIN)); } +TEST(TeeTest, MultiPage) { + // Create two new pipes. + int first[2], second[2]; + ASSERT_THAT(pipe(first), SyscallSucceeds()); + const FileDescriptor rfd1(first[0]); + const FileDescriptor wfd1(first[1]); + ASSERT_THAT(pipe(second), SyscallSucceeds()); + const FileDescriptor rfd2(second[0]); + const FileDescriptor wfd2(second[1]); + + // Make some data available to be read. + std::vector<char> wbuf(8 * kPageSize); + RandomizeBuffer(wbuf.data(), wbuf.size()); + ASSERT_THAT(write(wfd1.get(), wbuf.data(), wbuf.size()), + SyscallSucceedsWithValue(wbuf.size())); + + // Attempt a tee immediately; it should complete. + EXPECT_THAT(tee(rfd1.get(), wfd2.get(), wbuf.size(), 0), + SyscallSucceedsWithValue(wbuf.size())); + + // Content should reflect the tee. + std::vector<char> rbuf(wbuf.size()); + ASSERT_THAT(read(rfd2.get(), rbuf.data(), rbuf.size()), + SyscallSucceedsWithValue(rbuf.size())); + EXPECT_EQ(memcmp(rbuf.data(), wbuf.data(), rbuf.size()), 0); + ASSERT_THAT(read(rfd1.get(), rbuf.data(), rbuf.size()), + SyscallSucceedsWithValue(rbuf.size())); + EXPECT_EQ(memcmp(rbuf.data(), wbuf.data(), rbuf.size()), 0); +} + +TEST(SpliceTest, FromPipeMaxFileSize) { + // Create a new pipe. + int fds[2]; + ASSERT_THAT(pipe(fds), SyscallSucceeds()); + const FileDescriptor rfd(fds[0]); + const FileDescriptor wfd(fds[1]); + + // Fill with some random data. + std::vector<char> buf(kPageSize); + RandomizeBuffer(buf.data(), buf.size()); + ASSERT_THAT(write(wfd.get(), buf.data(), buf.size()), + SyscallSucceedsWithValue(kPageSize)); + + // Open the input file. + const TempPath out_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); + const FileDescriptor out_fd = + ASSERT_NO_ERRNO_AND_VALUE(Open(out_file.path(), O_RDWR)); + + EXPECT_THAT(ftruncate(out_fd.get(), 13 << 20), SyscallSucceeds()); + EXPECT_THAT(lseek(out_fd.get(), 0, SEEK_END), + SyscallSucceedsWithValue(13 << 20)); + + // Set our file size limit. + sigset_t set; + sigemptyset(&set); + sigaddset(&set, SIGXFSZ); + TEST_PCHECK(sigprocmask(SIG_BLOCK, &set, nullptr) == 0); + rlimit rlim = {}; + rlim.rlim_cur = rlim.rlim_max = (13 << 20); + EXPECT_THAT(setrlimit(RLIMIT_FSIZE, &rlim), SyscallSucceeds()); + + // Splice to the output file. + EXPECT_THAT( + splice(rfd.get(), nullptr, out_fd.get(), nullptr, 3 * kPageSize, 0), + SyscallFailsWithErrno(EFBIG)); + + // Contents should be equal. + std::vector<char> rbuf(kPageSize); + ASSERT_THAT(read(rfd.get(), rbuf.data(), rbuf.size()), + SyscallSucceedsWithValue(kPageSize)); + EXPECT_EQ(memcmp(rbuf.data(), buf.data(), buf.size()), 0); +} + } // namespace } // namespace testing diff --git a/test/syscalls/linux/sticky.cc b/test/syscalls/linux/sticky.cc index 59fb5dfe6..7e73325bf 100644 --- a/test/syscalls/linux/sticky.cc +++ b/test/syscalls/linux/sticky.cc @@ -17,9 +17,11 @@ #include <sys/prctl.h> #include <sys/types.h> #include <unistd.h> + #include <vector> #include "gtest/gtest.h" +#include "absl/flags/flag.h" #include "test/util/capability_util.h" #include "test/util/file_descriptor.h" #include "test/util/fs_util.h" @@ -27,8 +29,8 @@ #include "test/util/test_util.h" #include "test/util/thread_util.h" -DEFINE_int32(scratch_uid, 65534, "first scratch UID"); -DEFINE_int32(scratch_gid, 65534, "first scratch GID"); +ABSL_FLAG(int32_t, scratch_uid, 65534, "first scratch UID"); +ABSL_FLAG(int32_t, scratch_gid, 65534, "first scratch GID"); namespace gvisor { namespace testing { @@ -52,10 +54,12 @@ TEST(StickyTest, StickyBitPermDenied) { } // Change EUID and EGID. - EXPECT_THAT(syscall(SYS_setresgid, -1, FLAGS_scratch_gid, -1), - SyscallSucceeds()); - EXPECT_THAT(syscall(SYS_setresuid, -1, FLAGS_scratch_uid, -1), - SyscallSucceeds()); + EXPECT_THAT( + syscall(SYS_setresgid, -1, absl::GetFlag(FLAGS_scratch_gid), -1), + SyscallSucceeds()); + EXPECT_THAT( + syscall(SYS_setresuid, -1, absl::GetFlag(FLAGS_scratch_uid), -1), + SyscallSucceeds()); EXPECT_THAT(rmdir(path.c_str()), SyscallFailsWithErrno(EPERM)); }); @@ -78,8 +82,9 @@ TEST(StickyTest, StickyBitSameUID) { } // Change EGID. - EXPECT_THAT(syscall(SYS_setresgid, -1, FLAGS_scratch_gid, -1), - SyscallSucceeds()); + EXPECT_THAT( + syscall(SYS_setresgid, -1, absl::GetFlag(FLAGS_scratch_gid), -1), + SyscallSucceeds()); // We still have the same EUID. EXPECT_THAT(rmdir(path.c_str()), SyscallSucceeds()); @@ -101,10 +106,12 @@ TEST(StickyTest, StickyBitCapFOWNER) { EXPECT_THAT(prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0), SyscallSucceeds()); // Change EUID and EGID. - EXPECT_THAT(syscall(SYS_setresgid, -1, FLAGS_scratch_gid, -1), - SyscallSucceeds()); - EXPECT_THAT(syscall(SYS_setresuid, -1, FLAGS_scratch_uid, -1), - SyscallSucceeds()); + EXPECT_THAT( + syscall(SYS_setresgid, -1, absl::GetFlag(FLAGS_scratch_gid), -1), + SyscallSucceeds()); + EXPECT_THAT( + syscall(SYS_setresuid, -1, absl::GetFlag(FLAGS_scratch_uid), -1), + SyscallSucceeds()); EXPECT_NO_ERRNO(SetCapability(CAP_FOWNER, true)); EXPECT_THAT(rmdir(path.c_str()), SyscallSucceeds()); diff --git a/test/syscalls/linux/tcp_socket.cc b/test/syscalls/linux/tcp_socket.cc index 8f4d3f386..bfa031bce 100644 --- a/test/syscalls/linux/tcp_socket.cc +++ b/test/syscalls/linux/tcp_socket.cc @@ -579,7 +579,7 @@ TEST_P(TcpSocketTest, TcpInq) { if (size == sizeof(buf)) { break; } - usleep(10000); + absl::SleepFor(absl::Milliseconds(10)); } struct msghdr msg = {}; @@ -610,6 +610,25 @@ TEST_P(TcpSocketTest, TcpInq) { } } +TEST_P(TcpSocketTest, Tiocinq) { + char buf[1024]; + size_t size = sizeof(buf); + ASSERT_THAT(RetryEINTR(write)(s_, buf, size), SyscallSucceedsWithValue(size)); + + uint32_t seed = time(nullptr); + const size_t max_chunk = size / 10; + while (size > 0) { + size_t chunk = (rand_r(&seed) % max_chunk) + 1; + ssize_t read = RetryEINTR(recvfrom)(t_, buf, chunk, 0, nullptr, nullptr); + ASSERT_THAT(read, SyscallSucceeds()); + size -= read; + + int inq = 0; + ASSERT_THAT(ioctl(t_, TIOCINQ, &inq), SyscallSucceeds()); + ASSERT_EQ(inq, size); + } +} + TEST_P(TcpSocketTest, TcpSCMPriority) { char buf[1024]; ASSERT_THAT(RetryEINTR(write)(s_, buf, sizeof(buf)), diff --git a/test/syscalls/linux/timers.cc b/test/syscalls/linux/timers.cc index fd42e81e1..3db18d7ac 100644 --- a/test/syscalls/linux/timers.cc +++ b/test/syscalls/linux/timers.cc @@ -23,6 +23,7 @@ #include <atomic> #include "gtest/gtest.h" +#include "absl/flags/flag.h" #include "absl/time/clock.h" #include "absl/time/time.h" #include "test/util/cleanup.h" @@ -33,8 +34,8 @@ #include "test/util/test_util.h" #include "test/util/thread_util.h" -DEFINE_bool(timers_test_sleep, false, - "If true, sleep forever instead of running tests."); +ABSL_FLAG(bool, timers_test_sleep, false, + "If true, sleep forever instead of running tests."); using ::testing::_; using ::testing::AnyOf; @@ -635,7 +636,7 @@ TEST(IntervalTimerTest, IgnoredSignalCountsAsOverrun) { int main(int argc, char** argv) { gvisor::testing::TestInit(&argc, &argv); - if (FLAGS_timers_test_sleep) { + if (absl::GetFlag(FLAGS_timers_test_sleep)) { while (true) { absl::SleepFor(absl::Seconds(10)); } diff --git a/test/syscalls/linux/uidgid.cc b/test/syscalls/linux/uidgid.cc index bf1ca8679..6218fbce1 100644 --- a/test/syscalls/linux/uidgid.cc +++ b/test/syscalls/linux/uidgid.cc @@ -18,17 +18,19 @@ #include <unistd.h> #include "gtest/gtest.h" +#include "absl/flags/flag.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" #include "test/util/capability_util.h" #include "test/util/posix_error.h" #include "test/util/test_util.h" #include "test/util/thread_util.h" +#include "test/util/uid_util.h" -DEFINE_int32(scratch_uid1, 65534, "first scratch UID"); -DEFINE_int32(scratch_uid2, 65533, "second scratch UID"); -DEFINE_int32(scratch_gid1, 65534, "first scratch GID"); -DEFINE_int32(scratch_gid2, 65533, "second scratch GID"); +ABSL_FLAG(int32_t, scratch_uid1, 65534, "first scratch UID"); +ABSL_FLAG(int32_t, scratch_uid2, 65533, "second scratch UID"); +ABSL_FLAG(int32_t, scratch_gid1, 65534, "first scratch GID"); +ABSL_FLAG(int32_t, scratch_gid2, 65533, "second scratch GID"); using ::testing::UnorderedElementsAreArray; @@ -67,30 +69,6 @@ TEST(UidGidTest, Getgroups) { // here; see the setgroups test below. } -// If the caller's real/effective/saved user/group IDs are all 0, IsRoot returns -// true. Otherwise IsRoot logs an explanatory message and returns false. -PosixErrorOr<bool> IsRoot() { - uid_t ruid, euid, suid; - int rc = getresuid(&ruid, &euid, &suid); - MaybeSave(); - if (rc < 0) { - return PosixError(errno, "getresuid"); - } - if (ruid != 0 || euid != 0 || suid != 0) { - return false; - } - gid_t rgid, egid, sgid; - rc = getresgid(&rgid, &egid, &sgid); - MaybeSave(); - if (rc < 0) { - return PosixError(errno, "getresgid"); - } - if (rgid != 0 || egid != 0 || sgid != 0) { - return false; - } - return true; -} - // Checks that the calling process' real/effective/saved user IDs are // ruid/euid/suid respectively. PosixError CheckUIDs(uid_t ruid, uid_t euid, uid_t suid) { @@ -146,7 +124,7 @@ TEST(UidGidRootTest, Setuid) { // real UID. EXPECT_THAT(syscall(SYS_setuid, -1), SyscallFailsWithErrno(EINVAL)); - const uid_t uid = FLAGS_scratch_uid1; + const uid_t uid = absl::GetFlag(FLAGS_scratch_uid1); EXPECT_THAT(syscall(SYS_setuid, uid), SyscallSucceeds()); // "If the effective UID of the caller is root (more precisely: if the // caller has the CAP_SETUID capability), the real UID and saved set-user-ID @@ -160,7 +138,7 @@ TEST(UidGidRootTest, Setgid) { EXPECT_THAT(setgid(-1), SyscallFailsWithErrno(EINVAL)); - const gid_t gid = FLAGS_scratch_gid1; + const gid_t gid = absl::GetFlag(FLAGS_scratch_gid1); ASSERT_THAT(setgid(gid), SyscallSucceeds()); EXPECT_NO_ERRNO(CheckGIDs(gid, gid, gid)); } @@ -168,7 +146,7 @@ TEST(UidGidRootTest, Setgid) { TEST(UidGidRootTest, SetgidNotFromThreadGroupLeader) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(IsRoot())); - const gid_t gid = FLAGS_scratch_gid1; + const gid_t gid = absl::GetFlag(FLAGS_scratch_gid1); // NOTE(b/64676707): Do setgid in a separate thread so that we can test if // info.si_pid is set correctly. ScopedThread([gid] { ASSERT_THAT(setgid(gid), SyscallSucceeds()); }); @@ -189,8 +167,8 @@ TEST(UidGidRootTest, Setreuid) { // cannot be opened by the `uid` set below after the test. After calling // setuid(non-zero-UID), there is no way to get root privileges back. ScopedThread([&] { - const uid_t ruid = FLAGS_scratch_uid1; - const uid_t euid = FLAGS_scratch_uid2; + const uid_t ruid = absl::GetFlag(FLAGS_scratch_uid1); + const uid_t euid = absl::GetFlag(FLAGS_scratch_uid2); // Use syscall instead of glibc setuid wrapper because we want this setuid // call to only apply to this task. posix threads, however, require that all @@ -211,8 +189,8 @@ TEST(UidGidRootTest, Setregid) { EXPECT_THAT(setregid(-1, -1), SyscallSucceeds()); EXPECT_NO_ERRNO(CheckGIDs(0, 0, 0)); - const gid_t rgid = FLAGS_scratch_gid1; - const gid_t egid = FLAGS_scratch_gid2; + const gid_t rgid = absl::GetFlag(FLAGS_scratch_gid1); + const gid_t egid = absl::GetFlag(FLAGS_scratch_gid2); ASSERT_THAT(setregid(rgid, egid), SyscallSucceeds()); EXPECT_NO_ERRNO(CheckGIDs(rgid, egid, egid)); } diff --git a/test/syscalls/linux/uname.cc b/test/syscalls/linux/uname.cc index 0a5d91017..d8824b171 100644 --- a/test/syscalls/linux/uname.cc +++ b/test/syscalls/linux/uname.cc @@ -41,6 +41,19 @@ TEST(UnameTest, Sanity) { TEST(UnameTest, SetNames) { SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + char hostname[65]; + ASSERT_THAT(sethostname("0123456789", 3), SyscallSucceeds()); + EXPECT_THAT(gethostname(hostname, sizeof(hostname)), SyscallSucceeds()); + EXPECT_EQ(absl::string_view(hostname), "012"); + + ASSERT_THAT(sethostname("0123456789\0xxx", 11), SyscallSucceeds()); + EXPECT_THAT(gethostname(hostname, sizeof(hostname)), SyscallSucceeds()); + EXPECT_EQ(absl::string_view(hostname), "0123456789"); + + ASSERT_THAT(sethostname("0123456789\0xxx", 12), SyscallSucceeds()); + EXPECT_THAT(gethostname(hostname, sizeof(hostname)), SyscallSucceeds()); + EXPECT_EQ(absl::string_view(hostname), "0123456789"); + constexpr char kHostname[] = "wubbalubba"; ASSERT_THAT(sethostname(kHostname, sizeof(kHostname)), SyscallSucceeds()); @@ -54,7 +67,6 @@ TEST(UnameTest, SetNames) { EXPECT_EQ(absl::string_view(buf.domainname), kDomainname); // These should just be glibc wrappers that also call uname(2). - char hostname[65]; EXPECT_THAT(gethostname(hostname, sizeof(hostname)), SyscallSucceeds()); EXPECT_EQ(absl::string_view(hostname), kHostname); diff --git a/test/syscalls/linux/unlink.cc b/test/syscalls/linux/unlink.cc index b6f65e027..2040375c9 100644 --- a/test/syscalls/linux/unlink.cc +++ b/test/syscalls/linux/unlink.cc @@ -123,6 +123,8 @@ TEST(UnlinkTest, AtBad) { SyscallSucceeds()); EXPECT_THAT(unlinkat(dirfd, "UnlinkAtFile", AT_REMOVEDIR), SyscallFailsWithErrno(ENOTDIR)); + EXPECT_THAT(unlinkat(dirfd, "UnlinkAtFile/", 0), + SyscallFailsWithErrno(ENOTDIR)); ASSERT_THAT(close(fd), SyscallSucceeds()); EXPECT_THAT(unlinkat(dirfd, "UnlinkAtFile", 0), SyscallSucceeds()); diff --git a/test/syscalls/linux/vfork.cc b/test/syscalls/linux/vfork.cc index f67b06f37..0aaba482d 100644 --- a/test/syscalls/linux/vfork.cc +++ b/test/syscalls/linux/vfork.cc @@ -22,14 +22,15 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include "absl/flags/flag.h" #include "absl/time/time.h" #include "test/util/logging.h" #include "test/util/multiprocess_util.h" #include "test/util/test_util.h" #include "test/util/time_util.h" -DEFINE_bool(vfork_test_child, false, - "If true, run the VforkTest child workload."); +ABSL_FLAG(bool, vfork_test_child, false, + "If true, run the VforkTest child workload."); namespace gvisor { namespace testing { @@ -186,7 +187,7 @@ int RunChild() { int main(int argc, char** argv) { gvisor::testing::TestInit(&argc, &argv); - if (FLAGS_vfork_test_child) { + if (absl::GetFlag(FLAGS_vfork_test_child)) { return gvisor::testing::RunChild(); } diff --git a/test/syscalls/syscall_test_runner.go b/test/syscalls/syscall_test_runner.go index 32408f021..c1e9ce22c 100644 --- a/test/syscalls/syscall_test_runner.go +++ b/test/syscalls/syscall_test_runner.go @@ -20,12 +20,10 @@ import ( "flag" "fmt" "io/ioutil" - "math" "os" "os/exec" "os/signal" "path/filepath" - "strconv" "strings" "syscall" "testing" @@ -35,7 +33,7 @@ import ( "golang.org/x/sys/unix" "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/runsc/specutils" - "gvisor.dev/gvisor/runsc/test/testutil" + "gvisor.dev/gvisor/runsc/testutil" "gvisor.dev/gvisor/test/syscalls/gtest" ) @@ -358,32 +356,14 @@ func main() { fatalf("ParseTestCases(%q) failed: %v", testBin, err) } - // If sharding, then get the subset of tests to run based on the shard index. - if indexStr, totalStr := os.Getenv("TEST_SHARD_INDEX"), os.Getenv("TEST_TOTAL_SHARDS"); indexStr != "" && totalStr != "" { - // Parse index and total to ints. - index, err := strconv.Atoi(indexStr) - if err != nil { - fatalf("invalid TEST_SHARD_INDEX %q: %v", indexStr, err) - } - total, err := strconv.Atoi(totalStr) - if err != nil { - fatalf("invalid TEST_TOTAL_SHARDS %q: %v", totalStr, err) - } - // Calculate subslice of tests to run. - shardSize := int(math.Ceil(float64(len(testCases)) / float64(total))) - begin := index * shardSize - // Set end as begin of next subslice. - end := ((index + 1) * shardSize) - if begin > len(testCases) { - // Nothing to run. - return - } - if end > len(testCases) { - end = len(testCases) - } - testCases = testCases[begin:end] + // Get subset of tests corresponding to shard. + begin, end, err := testutil.TestBoundsForShard(len(testCases)) + if err != nil { + fatalf("TestsForShard() failed: %v", err) } + testCases = testCases[begin:end] + // Run the tests. var tests []testing.InternalTest for _, tc := range testCases { // Capture tc. diff --git a/test/util/BUILD b/test/util/BUILD index c124cef34..5d2a9cc2c 100644 --- a/test/util/BUILD +++ b/test/util/BUILD @@ -1,3 +1,6 @@ +load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test") +load("//test/syscalls:build_defs.bzl", "select_for_linux") + package( default_visibility = ["//:sandbox"], licenses = ["notice"], @@ -139,7 +142,11 @@ cc_library( cc_library( name = "save_util", testonly = 1, - srcs = ["save_util.cc"], + srcs = ["save_util.cc"] + + select_for_linux( + ["save_util_linux.cc"], + ["save_util_other.cc"], + ), hdrs = ["save_util.h"], ) @@ -232,8 +239,9 @@ cc_library( ":logging", ":posix_error", ":save_util", - "@com_github_gflags_gflags//:gflags", "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/flags:parse", "@com_google_absl//absl/strings", "@com_google_absl//absl/strings:str_format", "@com_google_absl//absl/time", @@ -316,3 +324,14 @@ cc_library( ":test_util", ], ) + +cc_library( + name = "uid_util", + testonly = 1, + srcs = ["uid_util.cc"], + hdrs = ["uid_util.h"], + deps = [ + ":posix_error", + ":save_util", + ], +) diff --git a/test/util/fs_util.cc b/test/util/fs_util.cc index ae49725a0..f7d231b14 100644 --- a/test/util/fs_util.cc +++ b/test/util/fs_util.cc @@ -105,6 +105,15 @@ PosixErrorOr<struct stat> Stat(absl::string_view path) { return stat_buf; } +PosixErrorOr<struct stat> Fstat(int fd) { + struct stat stat_buf; + int res = fstat(fd, &stat_buf); + if (res < 0) { + return PosixError(errno, absl::StrCat("fstat ", fd)); + } + return stat_buf; +} + PosixErrorOr<bool> Exists(absl::string_view path) { struct stat stat_buf; int res = stat(std::string(path).c_str(), &stat_buf); diff --git a/test/util/fs_util.h b/test/util/fs_util.h index 3969f8309..e5b555891 100644 --- a/test/util/fs_util.h +++ b/test/util/fs_util.h @@ -35,6 +35,9 @@ PosixErrorOr<bool> Exists(absl::string_view path); // Returns a stat structure for the given path or an error. PosixErrorOr<struct stat> Stat(absl::string_view path); +// Returns a stat struct for the given fd. +PosixErrorOr<struct stat> Fstat(int fd); + // Deletes the file or directory at path or returns an error. PosixError Delete(absl::string_view path); diff --git a/test/util/memory_util.h b/test/util/memory_util.h index 190c469b5..e189b73e8 100644 --- a/test/util/memory_util.h +++ b/test/util/memory_util.h @@ -118,6 +118,18 @@ inline PosixErrorOr<Mapping> MmapAnon(size_t length, int prot, int flags) { return Mmap(nullptr, length, prot, flags | MAP_ANONYMOUS, -1, 0); } +// Wrapper for mremap that returns a PosixErrorOr<>, since the return type of +// void* isn't directly compatible with SyscallSucceeds. +inline PosixErrorOr<void*> Mremap(void* old_address, size_t old_size, + size_t new_size, int flags, + void* new_address) { + void* rv = mremap(old_address, old_size, new_size, flags, new_address); + if (rv == MAP_FAILED) { + return PosixError(errno, "mremap failed"); + } + return rv; +} + // Returns true if the page containing addr is mapped. inline bool IsMapped(uintptr_t addr) { int const rv = msync(reinterpret_cast<void*>(addr & ~(kPageSize - 1)), diff --git a/test/util/proc_util.cc b/test/util/proc_util.cc index 75b24da37..34d636ba9 100644 --- a/test/util/proc_util.cc +++ b/test/util/proc_util.cc @@ -88,7 +88,7 @@ PosixErrorOr<std::vector<ProcMapsEntry>> ParseProcMaps( std::vector<ProcMapsEntry> entries; auto lines = absl::StrSplit(contents, '\n', absl::SkipEmpty()); for (const auto& l : lines) { - std::cout << "line: " << l; + std::cout << "line: " << l << std::endl; ASSIGN_OR_RETURN_ERRNO(auto entry, ParseProcMapsLine(l)); entries.push_back(entry); } diff --git a/test/util/save_util.cc b/test/util/save_util.cc index 05f52b80d..384d626f0 100644 --- a/test/util/save_util.cc +++ b/test/util/save_util.cc @@ -16,8 +16,8 @@ #include <stddef.h> #include <stdlib.h> -#include <sys/syscall.h> #include <unistd.h> + #include <atomic> #include <cerrno> @@ -61,13 +61,11 @@ void DisableSave::reset() { } } -void MaybeSave() { - if (CooperativeSaveEnabled() && !save_disable.load()) { - int orig_errno = errno; - syscall(SYS_create_module, nullptr, 0); - errno = orig_errno; - } +namespace internal { +bool ShouldSave() { + return CooperativeSaveEnabled() && (save_disable.load() == 0); } +} // namespace internal } // namespace testing } // namespace gvisor diff --git a/test/util/save_util.h b/test/util/save_util.h index 90460701e..bddad6120 100644 --- a/test/util/save_util.h +++ b/test/util/save_util.h @@ -41,6 +41,11 @@ class DisableSave { // // errno is guaranteed to be preserved. void MaybeSave(); + +namespace internal { +bool ShouldSave(); +} // namespace internal + } // namespace testing } // namespace gvisor diff --git a/test/runtimes/runtimes.go b/test/util/save_util_linux.cc index 2568e07fe..7a0f14342 100644 --- a/test/runtimes/runtimes.go +++ b/test/util/save_util_linux.cc @@ -12,9 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package runtimes provides language tests for runsc runtimes. -// Each test calls docker commands to start up a container for each supported runtime, -// and tests that its respective language tests are behaving as expected, like -// connecting to a port or looking at the output. The container is killed and deleted -// at the end. -package runtimes +#include <errno.h> +#include <sys/syscall.h> +#include <unistd.h> + +#include "test/util/save_util.h" + +namespace gvisor { +namespace testing { + +void MaybeSave() { + if (internal::ShouldSave()) { + int orig_errno = errno; + syscall(SYS_create_module, nullptr, 0); + errno = orig_errno; + } +} + +} // namespace testing +} // namespace gvisor diff --git a/test/util/save_util_other.cc b/test/util/save_util_other.cc new file mode 100644 index 000000000..1aca663b7 --- /dev/null +++ b/test/util/save_util_other.cc @@ -0,0 +1,23 @@ +// Copyright 2019 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. + +namespace gvisor { +namespace testing { + +void MaybeSave() { + // Saving is never available in a non-linux environment. +} + +} // namespace testing +} // namespace gvisor diff --git a/test/util/test_util.cc b/test/util/test_util.cc index e42bba04a..ba0dcf7d0 100644 --- a/test/util/test_util.cc +++ b/test/util/test_util.cc @@ -28,6 +28,8 @@ #include <vector> #include "absl/base/attributes.h" +#include "absl/flags/flag.h" // IWYU pragma: keep +#include "absl/flags/parse.h" // IWYU pragma: keep #include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_split.h" @@ -224,7 +226,7 @@ bool Equivalent(uint64_t current, uint64_t target, double tolerance) { void TestInit(int* argc, char*** argv) { ::testing::InitGoogleTest(argc, *argv); - ::gflags::ParseCommandLineFlags(argc, argv, true); + ::absl::ParseCommandLine(*argc, *argv); // Always mask SIGPIPE as it's common and tests aren't expected to handle it. struct sigaction sa = {}; diff --git a/test/util/test_util.h b/test/util/test_util.h index cdbe8bfd1..b9d2dc2ba 100644 --- a/test/util/test_util.h +++ b/test/util/test_util.h @@ -185,7 +185,6 @@ #include <utility> #include <vector> -#include <gflags/gflags.h> #include "gmock/gmock.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" diff --git a/test/util/thread_util.h b/test/util/thread_util.h index 860e77531..923c4fe10 100644 --- a/test/util/thread_util.h +++ b/test/util/thread_util.h @@ -16,7 +16,9 @@ #define GVISOR_TEST_UTIL_THREAD_UTIL_H_ #include <pthread.h> +#ifdef __linux__ #include <sys/syscall.h> +#endif #include <unistd.h> #include <functional> @@ -66,13 +68,13 @@ class ScopedThread { private: void CreateThread() { - TEST_PCHECK_MSG( - pthread_create(&pt_, /* attr = */ nullptr, - +[](void* arg) -> void* { - return static_cast<ScopedThread*>(arg)->f_(); - }, - this) == 0, - "thread creation failed"); + TEST_PCHECK_MSG(pthread_create( + &pt_, /* attr = */ nullptr, + +[](void* arg) -> void* { + return static_cast<ScopedThread*>(arg)->f_(); + }, + this) == 0, + "thread creation failed"); } std::function<void*()> f_; @@ -81,7 +83,9 @@ class ScopedThread { void* retval_ = nullptr; }; +#ifdef __linux__ inline pid_t gettid() { return syscall(SYS_gettid); } +#endif } // namespace testing } // namespace gvisor diff --git a/test/util/uid_util.cc b/test/util/uid_util.cc new file mode 100644 index 000000000..b131b4b99 --- /dev/null +++ b/test/util/uid_util.cc @@ -0,0 +1,44 @@ +// 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. + +#include "test/util/posix_error.h" +#include "test/util/save_util.h" + +namespace gvisor { +namespace testing { + +PosixErrorOr<bool> IsRoot() { + uid_t ruid, euid, suid; + int rc = getresuid(&ruid, &euid, &suid); + MaybeSave(); + if (rc < 0) { + return PosixError(errno, "getresuid"); + } + if (ruid != 0 || euid != 0 || suid != 0) { + return false; + } + gid_t rgid, egid, sgid; + rc = getresgid(&rgid, &egid, &sgid); + MaybeSave(); + if (rc < 0) { + return PosixError(errno, "getresgid"); + } + if (rgid != 0 || egid != 0 || sgid != 0) { + return false; + } + return true; +} + +} // namespace testing +} // namespace gvisor diff --git a/test/util/uid_util.h b/test/util/uid_util.h new file mode 100644 index 000000000..2cd387fb0 --- /dev/null +++ b/test/util/uid_util.h @@ -0,0 +1,29 @@ +// 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. + +#ifndef GVISOR_TEST_SYSCALLS_UID_UTIL_H_ +#define GVISOR_TEST_SYSCALLS_UID_UTIL_H_ + +#include "test/util/posix_error.h" + +namespace gvisor { +namespace testing { + +// Returns true if the caller's real/effective/saved user/group IDs are all 0. +PosixErrorOr<bool> IsRoot(); + +} // namespace testing +} // namespace gvisor + +#endif // GVISOR_TEST_SYSCALLS_UID_UTIL_H_ |