summaryrefslogtreecommitdiffhomepage
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/README.md4
-rw-r--r--test/benchmarks/base/size_test.go1
-rw-r--r--test/benchmarks/base/startup_test.go3
-rw-r--r--test/benchmarks/network/nginx_test.go47
-rw-r--r--test/benchmarks/network/static_server.go1
-rw-r--r--test/benchmarks/tcp/tcp_proxy.go4
-rw-r--r--test/e2e/integration_test.go44
-rw-r--r--test/fuse/BUILD66
-rw-r--r--test/fuse/README.md207
-rw-r--r--test/fuse/linux/BUILD198
-rw-r--r--test/fuse/linux/create_test.cc128
-rw-r--r--test/fuse/linux/fuse_base.cc447
-rw-r--r--test/fuse/linux/fuse_base.h240
-rw-r--r--test/fuse/linux/fuse_fd_util.cc61
-rw-r--r--test/fuse/linux/fuse_fd_util.h48
-rw-r--r--test/fuse/linux/mkdir_test.cc88
-rw-r--r--test/fuse/linux/mknod_test.cc107
-rw-r--r--test/fuse/linux/open_test.cc128
-rw-r--r--test/fuse/linux/read_test.cc390
-rw-r--r--test/fuse/linux/readdir_test.cc193
-rw-r--r--test/fuse/linux/readlink_test.cc85
-rw-r--r--test/fuse/linux/release_test.cc74
-rw-r--r--test/fuse/linux/rmdir_test.cc100
-rw-r--r--test/fuse/linux/setstat_test.cc338
-rw-r--r--test/fuse/linux/stat_test.cc226
-rw-r--r--test/fuse/linux/symlink_test.cc88
-rw-r--r--test/fuse/linux/unlink_test.cc107
-rw-r--r--test/fuse/linux/write_test.cc303
-rw-r--r--test/image/image_test.go64
-rw-r--r--test/iptables/README.md2
-rw-r--r--test/iptables/iptables_test.go9
-rw-r--r--test/packetimpact/runner/dut.go19
-rw-r--r--test/packetimpact/testbench/dut.go16
-rw-r--r--test/packetimpact/testbench/rawsockets.go2
-rw-r--r--test/packetimpact/testbench/testbench.go10
-rw-r--r--test/packetimpact/tests/BUILD30
-rw-r--r--test/packetimpact/tests/tcp_linger_test.go270
-rw-r--r--test/packetimpact/tests/tcp_queue_send_in_syn_sent_test.go133
-rw-r--r--test/packetimpact/tests/tcp_rcv_buf_space_test.go80
-rw-r--r--test/root/root.go2
-rw-r--r--test/runner/defs.bzl18
-rw-r--r--test/runtimes/BUILD10
-rw-r--r--test/runtimes/README.md62
-rw-r--r--test/runtimes/exclude/go1.12.csv (renamed from test/runtimes/exclude_go1.12.csv)0
-rw-r--r--test/runtimes/exclude/java11.csv (renamed from test/runtimes/exclude_java11.csv)2
-rw-r--r--test/runtimes/exclude/nodejs12.4.0.csv (renamed from test/runtimes/exclude_nodejs12.4.0.csv)3
-rw-r--r--test/runtimes/exclude/php7.3.6.csv (renamed from test/runtimes/exclude_php7.3.6.csv)1
-rw-r--r--test/runtimes/exclude/python3.7.3.csv (renamed from test/runtimes/exclude_python3.7.3.csv)0
-rw-r--r--test/runtimes/proctor/BUILD24
-rw-r--r--test/runtimes/proctor/lib/BUILD24
-rw-r--r--test/runtimes/proctor/lib/go.go (renamed from test/runtimes/proctor/go.go)4
-rw-r--r--test/runtimes/proctor/lib/java.go (renamed from test/runtimes/proctor/java.go)2
-rw-r--r--test/runtimes/proctor/lib/lib.go (renamed from test/runtimes/proctor/proctor.go)78
-rw-r--r--test/runtimes/proctor/lib/lib_test.go (renamed from test/runtimes/proctor/proctor_test.go)6
-rw-r--r--test/runtimes/proctor/lib/nodejs.go (renamed from test/runtimes/proctor/nodejs.go)4
-rw-r--r--test/runtimes/proctor/lib/php.go (renamed from test/runtimes/proctor/php.go)4
-rw-r--r--test/runtimes/proctor/lib/python.go (renamed from test/runtimes/proctor/python.go)2
-rw-r--r--test/runtimes/proctor/main.go85
-rw-r--r--test/runtimes/runner/BUILD15
-rw-r--r--test/runtimes/runner/lib/BUILD22
-rw-r--r--test/runtimes/runner/lib/exclude_test.go (renamed from test/runtimes/runner/exclude_test.go)6
-rw-r--r--test/runtimes/runner/lib/lib.go185
-rw-r--r--test/runtimes/runner/main.go169
-rw-r--r--test/syscalls/BUILD6
-rw-r--r--test/syscalls/linux/BUILD23
-rw-r--r--test/syscalls/linux/fallocate.cc6
-rw-r--r--test/syscalls/linux/inotify.cc126
-rw-r--r--test/syscalls/linux/ip6tables.cc30
-rw-r--r--test/syscalls/linux/iptables.cc26
-rw-r--r--test/syscalls/linux/kcov.cc140
-rw-r--r--test/syscalls/linux/membarrier.cc268
-rw-r--r--test/syscalls/linux/mknod.cc9
-rw-r--r--test/syscalls/linux/packet_socket_raw.cc21
-rw-r--r--test/syscalls/linux/proc.cc41
-rw-r--r--test/syscalls/linux/proc_net.cc41
-rw-r--r--test/syscalls/linux/raw_socket_icmp.cc22
-rw-r--r--test/syscalls/linux/rename.cc3
-rw-r--r--test/syscalls/linux/socket_inet_loopback.cc23
-rw-r--r--test/syscalls/linux/socket_ip_tcp_generic.cc119
-rw-r--r--test/syscalls/linux/socket_ip_udp_generic.cc22
-rw-r--r--test/syscalls/linux/socket_ipv4_udp_unbound.cc151
-rw-r--r--test/syscalls/linux/socket_ipv4_udp_unbound_netlink.cc153
-rw-r--r--test/syscalls/linux/socket_netlink_route_util.cc87
-rw-r--r--test/syscalls/linux/socket_netlink_route_util.h4
-rw-r--r--test/syscalls/linux/socket_test_util.cc14
-rw-r--r--test/syscalls/linux/socket_test_util.h4
-rw-r--r--test/syscalls/linux/socket_unix_stream.cc18
-rw-r--r--test/syscalls/linux/stat.cc18
-rw-r--r--test/syscalls/linux/tcp_socket.cc30
-rw-r--r--test/syscalls/linux/truncate.cc20
-rw-r--r--test/syscalls/linux/udp_socket.cc30
-rw-r--r--test/syscalls/linux/vdso_clock_gettime.cc69
-rw-r--r--test/util/BUILD11
-rw-r--r--test/util/fuse_util.cc63
-rw-r--r--test/util/fuse_util.h75
95 files changed, 5814 insertions, 948 deletions
diff --git a/test/README.md b/test/README.md
index 02bbf42ff..15b0f4c33 100644
--- a/test/README.md
+++ b/test/README.md
@@ -24,11 +24,11 @@ also used to run these tests in `kokoro`.
To run image and integration tests, run:
-`./scripts/docker_tests.sh`
+`make docker-tests`
To run root tests, run:
-`./scripts/root_tests.sh`
+`make root-tests`
There are a few other interesting variations for image and integration tests:
diff --git a/test/benchmarks/base/size_test.go b/test/benchmarks/base/size_test.go
index 3c1364faf..7d3877459 100644
--- a/test/benchmarks/base/size_test.go
+++ b/test/benchmarks/base/size_test.go
@@ -105,6 +105,7 @@ func BenchmarkSizeNginx(b *testing.B) {
machine: machine,
port: port,
runOpts: runOpts,
+ cmd: []string{"nginx", "-c", "/etc/nginx/nginx_gofer.conf"},
})
defer cleanUpContainers(ctx, servers)
diff --git a/test/benchmarks/base/startup_test.go b/test/benchmarks/base/startup_test.go
index 4628a0a41..c36a544db 100644
--- a/test/benchmarks/base/startup_test.go
+++ b/test/benchmarks/base/startup_test.go
@@ -64,6 +64,7 @@ func BenchmarkStartupNginx(b *testing.B) {
machine: machine,
runOpts: runOpts,
port: 80,
+ cmd: []string{"nginx", "-c", "/etc/nginx/nginx_gofer.conf"},
})
}
@@ -123,8 +124,6 @@ func redisInstance(ctx context.Context, b *testing.B, machine harness.Machine) (
// runServerWorkload runs a server workload defined by 'runOpts' and 'cmd'.
// 'clientMachine' is used to connect to the server on 'serverMachine'.
func runServerWorkload(ctx context.Context, b *testing.B, args serverArgs) {
- b.Helper()
-
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := func() error {
diff --git a/test/benchmarks/network/nginx_test.go b/test/benchmarks/network/nginx_test.go
index 036fd666f..9ec70369b 100644
--- a/test/benchmarks/network/nginx_test.go
+++ b/test/benchmarks/network/nginx_test.go
@@ -36,50 +36,63 @@ var nginxDocs = map[string]string{
func BenchmarkNginxConcurrency(b *testing.B) {
concurrency := []int{1, 25, 100, 1000}
for _, c := range concurrency {
- b.Run(fmt.Sprintf("%d", c), func(b *testing.B) {
- hey := &tools.Hey{
- Requests: c * b.N,
- Concurrency: c,
- Doc: nginxDocs["10kb"], // see Dockerfile '//images/benchmarks/nginx' and httpd_test.
+ for _, tmpfs := range []bool{true, false} {
+ fs := "Gofer"
+ if tmpfs {
+ fs = "Tmpfs"
}
- runNginx(b, hey, false /* reverse */)
- })
+ name := fmt.Sprintf("%d_%s", c, fs)
+ b.Run(name, func(b *testing.B) {
+ hey := &tools.Hey{
+ Requests: c * b.N,
+ Concurrency: c,
+ Doc: nginxDocs["10kb"], // see Dockerfile '//images/benchmarks/nginx' and httpd_test.
+ }
+ runNginx(b, hey, false /* reverse */, tmpfs /* tmpfs */)
+ })
+ }
+
}
}
// BenchmarkNginxDocSize iterates over different sized payloads, testing how
// well the runtime handles sending different payload sizes.
func BenchmarkNginxDocSize(b *testing.B) {
- benchmarkHttpdDocSize(b, false /* reverse */)
+ benchmarkNginxDocSize(b, false /* reverse */, true /* tmpfs */)
+ benchmarkNginxDocSize(b, false /* reverse */, false /* tmpfs */)
}
// BenchmarkReverseNginxDocSize iterates over different sized payloads, testing
// how well the runtime handles receiving different payload sizes.
func BenchmarkReverseNginxDocSize(b *testing.B) {
- benchmarkHttpdDocSize(b, true /* reverse */)
+ benchmarkNginxDocSize(b, true /* reverse */, true /* tmpfs */)
}
// benchmarkNginxDocSize iterates through all doc sizes, running subbenchmarks
// for each size.
-func benchmarkNginxDocSize(b *testing.B, reverse bool) {
- b.Helper()
+func benchmarkNginxDocSize(b *testing.B, reverse, tmpfs bool) {
for name, filename := range nginxDocs {
concurrency := []int{1, 25, 50, 100, 1000}
for _, c := range concurrency {
- b.Run(fmt.Sprintf("%s_%d", name, c), func(b *testing.B) {
+ fs := "Gofer"
+ if tmpfs {
+ fs = "Tmpfs"
+ }
+ benchName := fmt.Sprintf("%s_%d_%s", name, c, fs)
+ b.Run(benchName, func(b *testing.B) {
hey := &tools.Hey{
Requests: c * b.N,
Concurrency: c,
Doc: filename,
}
- runNginx(b, hey, reverse)
+ runNginx(b, hey, reverse, tmpfs)
})
}
}
}
// runNginx configures the static serving methods to run httpd.
-func runNginx(b *testing.B, hey *tools.Hey, reverse bool) {
+func runNginx(b *testing.B, hey *tools.Hey, reverse, tmpfs bool) {
// nginx runs on port 80.
port := 80
nginxRunOpts := dockerutil.RunOpts{
@@ -87,7 +100,11 @@ func runNginx(b *testing.B, hey *tools.Hey, reverse bool) {
Ports: []int{port},
}
+ nginxCmd := []string{"nginx", "-c", "/etc/nginx/nginx_gofer.conf"}
+ if tmpfs {
+ nginxCmd = []string{"sh", "-c", "mkdir -p /tmp/html && cp -a /local/* /tmp/html && nginx -c /etc/nginx/nginx.conf"}
+ }
+
// Command copies nginxDocs to tmpfs serving directory and runs nginx.
- nginxCmd := []string{"sh", "-c", "mkdir -p /tmp/html && cp -a /local/* /tmp/html && nginx"}
runStaticServer(b, nginxRunOpts, nginxCmd, port, hey, reverse)
}
diff --git a/test/benchmarks/network/static_server.go b/test/benchmarks/network/static_server.go
index 3ef62a71f..e747a1395 100644
--- a/test/benchmarks/network/static_server.go
+++ b/test/benchmarks/network/static_server.go
@@ -25,7 +25,6 @@ import (
// runStaticServer runs static serving workloads (httpd, nginx).
func runStaticServer(b *testing.B, serverOpts dockerutil.RunOpts, serverCmd []string, port int, hey *tools.Hey, reverse bool) {
- b.Helper()
ctx := context.Background()
// Get two machines: a client and server.
diff --git a/test/benchmarks/tcp/tcp_proxy.go b/test/benchmarks/tcp/tcp_proxy.go
index 6cabfb451..5afe10f69 100644
--- a/test/benchmarks/tcp/tcp_proxy.go
+++ b/test/benchmarks/tcp/tcp_proxy.go
@@ -174,8 +174,8 @@ func newNetstackImpl(mode string) (impl, error) {
}
// Create a new network stack.
- netProtos := []stack.NetworkProtocol{ipv4.NewProtocol(), arp.NewProtocol()}
- transProtos := []stack.TransportProtocol{tcp.NewProtocol(), udp.NewProtocol()}
+ netProtos := []stack.NetworkProtocolFactory{ipv4.NewProtocol, arp.NewProtocol}
+ transProtos := []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol}
s := stack.New(stack.Options{
NetworkProtocols: netProtos,
TransportProtocols: transProtos,
diff --git a/test/e2e/integration_test.go b/test/e2e/integration_test.go
index 809244bab..8425abecb 100644
--- a/test/e2e/integration_test.go
+++ b/test/e2e/integration_test.go
@@ -64,9 +64,10 @@ func TestLifeCycle(t *testing.T) {
defer d.CleanUp(ctx)
// Start the container.
+ port := 80
if err := d.Create(ctx, dockerutil.RunOpts{
Image: "basic/nginx",
- Ports: []int{80},
+ Ports: []int{port},
}); err != nil {
t.Fatalf("docker create failed: %v", err)
}
@@ -74,16 +75,15 @@ func TestLifeCycle(t *testing.T) {
t.Fatalf("docker start failed: %v", err)
}
- // Test that container is working.
- port, err := d.FindPort(ctx, 80)
+ ip, err := d.FindIP(ctx, false)
if err != nil {
- t.Fatalf("docker.FindPort(80) failed: %v", err)
+ t.Fatalf("docker.FindIP failed: %v", err)
}
- if err := testutil.WaitForHTTP(port, defaultWait); err != nil {
+ if err := testutil.WaitForHTTP(ip.String(), port, defaultWait); err != nil {
t.Fatalf("WaitForHTTP() timeout: %v", err)
}
client := http.Client{Timeout: defaultWait}
- if err := httpRequestSucceeds(client, "localhost", port); err != nil {
+ if err := httpRequestSucceeds(client, ip.String(), port); err != nil {
t.Errorf("http request failed: %v", err)
}
@@ -105,27 +105,28 @@ func TestPauseResume(t *testing.T) {
defer d.CleanUp(ctx)
// Start the container.
+ port := 8080
if err := d.Spawn(ctx, dockerutil.RunOpts{
Image: "basic/python",
- Ports: []int{8080}, // See Dockerfile.
+ Ports: []int{port}, // See Dockerfile.
}); err != nil {
t.Fatalf("docker run failed: %v", err)
}
- // Find where port 8080 is mapped to.
- port, err := d.FindPort(ctx, 8080)
+ // Find container IP address.
+ ip, err := d.FindIP(ctx, false)
if err != nil {
- t.Fatalf("docker.FindPort(8080) failed: %v", err)
+ t.Fatalf("docker.FindIP failed: %v", err)
}
// Wait until it's up and running.
- if err := testutil.WaitForHTTP(port, defaultWait); err != nil {
+ if err := testutil.WaitForHTTP(ip.String(), port, defaultWait); err != nil {
t.Fatalf("WaitForHTTP() timeout: %v", err)
}
// Check that container is working.
client := http.Client{Timeout: defaultWait}
- if err := httpRequestSucceeds(client, "localhost", port); err != nil {
+ if err := httpRequestSucceeds(client, ip.String(), port); err != nil {
t.Error("http request failed:", err)
}
@@ -135,7 +136,7 @@ func TestPauseResume(t *testing.T) {
// Check if container is paused.
client = http.Client{Timeout: 10 * time.Millisecond} // Don't wait a minute.
- switch _, err := client.Get(fmt.Sprintf("http://localhost:%d", port)); v := err.(type) {
+ switch _, err := client.Get(fmt.Sprintf("http://%s:%d", ip.String(), port)); v := err.(type) {
case nil:
t.Errorf("http req expected to fail but it succeeded")
case net.Error:
@@ -151,13 +152,13 @@ func TestPauseResume(t *testing.T) {
}
// Wait until it's up and running.
- if err := testutil.WaitForHTTP(port, defaultWait); err != nil {
+ if err := testutil.WaitForHTTP(ip.String(), port, defaultWait); err != nil {
t.Fatalf("WaitForHTTP() timeout: %v", err)
}
// Check if container is working again.
client = http.Client{Timeout: defaultWait}
- if err := httpRequestSucceeds(client, "localhost", port); err != nil {
+ if err := httpRequestSucceeds(client, ip.String(), port); err != nil {
t.Error("http request failed:", err)
}
}
@@ -179,9 +180,10 @@ func TestCheckpointRestore(t *testing.T) {
defer d.CleanUp(ctx)
// Start the container.
+ port := 8080
if err := d.Spawn(ctx, dockerutil.RunOpts{
Image: "basic/python",
- Ports: []int{8080}, // See Dockerfile.
+ Ports: []int{port}, // See Dockerfile.
}); err != nil {
t.Fatalf("docker run failed: %v", err)
}
@@ -199,20 +201,20 @@ func TestCheckpointRestore(t *testing.T) {
t.Fatalf("docker restore failed: %v", err)
}
- // Find where port 8080 is mapped to.
- port, err := d.FindPort(ctx, 8080)
+ // Find container IP address.
+ ip, err := d.FindIP(ctx, false)
if err != nil {
- t.Fatalf("docker.FindPort(8080) failed: %v", err)
+ t.Fatalf("docker.FindIP failed: %v", err)
}
// Wait until it's up and running.
- if err := testutil.WaitForHTTP(port, defaultWait); err != nil {
+ if err := testutil.WaitForHTTP(ip.String(), port, defaultWait); err != nil {
t.Fatalf("WaitForHTTP() timeout: %v", err)
}
// Check if container is working again.
client := http.Client{Timeout: defaultWait}
- if err := httpRequestSucceeds(client, "localhost", port); err != nil {
+ if err := httpRequestSucceeds(client, ip.String(), port); err != nil {
t.Error("http request failed:", err)
}
}
diff --git a/test/fuse/BUILD b/test/fuse/BUILD
index 56157c96b..8e31fdd41 100644
--- a/test/fuse/BUILD
+++ b/test/fuse/BUILD
@@ -5,5 +5,69 @@ package(licenses = ["notice"])
syscall_test(
fuse = "True",
test = "//test/fuse/linux:stat_test",
- vfs2 = "True",
+)
+
+syscall_test(
+ fuse = "True",
+ test = "//test/fuse/linux:open_test",
+)
+
+syscall_test(
+ fuse = "True",
+ test = "//test/fuse/linux:release_test",
+)
+
+syscall_test(
+ fuse = "True",
+ test = "//test/fuse/linux:mknod_test",
+)
+
+syscall_test(
+ fuse = "True",
+ test = "//test/fuse/linux:symlink_test",
+)
+
+syscall_test(
+ fuse = "True",
+ test = "//test/fuse/linux:readlink_test",
+)
+
+syscall_test(
+ fuse = "True",
+ test = "//test/fuse/linux:mkdir_test",
+)
+
+syscall_test(
+ fuse = "True",
+ test = "//test/fuse/linux:read_test",
+)
+
+syscall_test(
+ fuse = "True",
+ test = "//test/fuse/linux:write_test",
+)
+
+syscall_test(
+ fuse = "True",
+ test = "//test/fuse/linux:rmdir_test",
+)
+
+syscall_test(
+ fuse = "True",
+ test = "//test/fuse/linux:readdir_test",
+)
+
+syscall_test(
+ fuse = "True",
+ test = "//test/fuse/linux:create_test",
+)
+
+syscall_test(
+ fuse = "True",
+ test = "//test/fuse/linux:unlink_test",
+)
+
+syscall_test(
+ fuse = "True",
+ test = "//test/fuse/linux:setstat_test",
)
diff --git a/test/fuse/README.md b/test/fuse/README.md
index 734c3a4e3..65add57e2 100644
--- a/test/fuse/README.md
+++ b/test/fuse/README.md
@@ -1,65 +1,114 @@
# gVisor FUSE Test Suite
-This is an integration test suite for fuse(4) filesystem. It runs under both
-gVisor and Linux, and ensures compatibility between the two. This test suite is
-based on system calls test.
+This is an integration test suite for fuse(4) filesystem. It runs under gVisor
+sandbox container with VFS2 and FUSE function enabled.
-This document describes the framework of fuse integration test and the
-guidelines that should be followed when adding new fuse tests.
+This document describes the framework of FUSE integration test, how to use it,
+and the guidelines that should be followed when adding new testing features.
## Integration Test Framework
-Please refer to the figure below. `>` is entering the function, `<` is leaving
-the function, and `=` indicates sequentially entering and leaving.
+By inheriting the `FuseTest` class defined in `linux/fuse_base.h`, every test
+fixture can run in an environment with `mount_point_` mounted by a fake FUSE
+server. It creates a `socketpair(2)` to send and receive control commands and
+data between the client and the server. Because the FUSE server runs in the
+background thread, gTest cannot catch its assertion failure immediately. Thus,
+`TearDown()` function sends command to the FUSE server to check if all gTest
+assertion in the server are successful and all requests and preset responses are
+consumed.
+
+## Communication Diagram
+
+Diagram below describes how a testing thread communicates with the FUSE server
+to achieve integration test.
+
+For the following diagram, `>` means entering the function, `<` is leaving the
+function, and `=` indicates sequentially entering and leaving. Not necessarily
+follow exactly the below diagram due to the nature of a multi-threaded system,
+however, it is still helpful to know when the client waits for the server to
+complete a command and when the server awaits the next instruction.
```
- | Client (Test Main Process) | Server (FUSE Daemon)
- | |
- | >TEST_F() |
- | >SetUp() |
- | =MountFuse() |
- | >SetUpFuseServer() |
- | [create communication pipes] |
- | =fork() | =fork()
- | >WaitCompleted() |
- | [wait for MarkDone()] |
- | | =ConsumeFuseInit()
- | | =MarkDone()
- | <WaitCompleted() |
- | <SetUpFuseServer() |
- | <SetUp() |
- | >SetExpected() |
- | [construct expected reaction] |
- | | >FuseLoop()
- | | >ReceiveExpected()
- | | [wait data from pipe]
- | [write data to pipe] |
- | [wait for MarkDone()] |
- | | [save data to memory]
- | | =MarkDone()
- | <SetExpected() |
- | | <ReceiveExpected()
- | | >read()
- | | [wait for fs operation]
- | >[Do fs operation] |
- | [wait for fs response] |
- | | <read()
- | | =CompareRequest()
- | | =write() [write fs response]
- | <[Do fs operation] |
- | =[Test fs operation result] |
- | =[wait for MarkDone()] |
- | | =MarkDone()
- | >TearDown() |
- | =UnmountFuse() |
- | <TearDown() |
- | <TEST_F() |
+| Client (Testing Thread) | Server (FUSE Server Thread)
+| |
+| >TEST_F() |
+| >SetUp() |
+| =MountFuse() |
+| >SetUpFuseServer() |
+| [create communication socket]|
+| =fork() | =fork()
+| [wait server complete] |
+| | =ServerConsumeFuseInit()
+| | =ServerCompleteWith()
+| <SetUpFuseServer() |
+| <SetUp() |
+| [testing main] |
+| | >ServerFuseLoop()
+| | [poll on socket and fd]
+| >SetServerResponse() |
+| [write data to socket] |
+| [wait server complete] |
+| | [socket event occurs]
+| | >ServerHandleCommand()
+| | >ServerReceiveResponse()
+| | [read data from socket]
+| | [save data to memory]
+| | <ServerReceiveResponse()
+| | =ServerCompleteWith()
+| <SetServerResponse() |
+| | <ServerHandleCommand()
+| >[Do fs operation] |
+| [wait for fs response] |
+| | [fd event occurs]
+| | >ServerProcessFuseRequest()
+| | =[read fs request]
+| | =[save fs request to memory]
+| | =[write fs response]
+| <[Do fs operation] |
+| | <ServerProcessFuseRequest()
+| |
+| =[Test fs operation result] |
+| |
+| >GetServerActualRequest() |
+| [write data to socket] |
+| [wait data from server] |
+| | [socket event occurs]
+| | >ServerHandleCommand()
+| | >ServerSendReceivedRequest()
+| | [write data to socket]
+| [read data from socket] |
+| [wait server complete] |
+| | <ServerSendReceivedRequest()
+| | =ServerCompleteWith()
+| <GetServerActualRequest() |
+| | <ServerHandleCommand()
+| |
+| =[Test actual request] |
+| |
+| >TearDown() |
+| ... |
+| >GetServerNumUnsentResponses() |
+| [write data to socket] |
+| [wait server complete] |
+| | [socket event arrive]
+| | >ServerHandleCommand()
+| | >ServerSendData()
+| | [write data to socket]
+| | <ServerSendData()
+| | =ServerCompleteWith()
+| [read data from socket] |
+| [test if all succeeded] |
+| <GetServerNumUnsentResponses() |
+| | <ServerHandleCommand()
+| =UnmountFuse() |
+| <TearDown() |
+| <TEST_F() |
```
## Running the tests
-Based on syscall tests, fuse tests can run in different environments. To enable
-fuse testing environment, the test targets should be appended with `_fuse`.
+Based on syscall tests, FUSE tests generate targets only with vfs2 and fuse
+enabled. The corresponding targets end in `_fuse`.
For example, to run fuse test in `stat_test.cc`:
@@ -77,17 +126,16 @@ $ bazel test --test_tag_filters=fuse //test/fuse/...
1. Add test targets in `BUILD` and `linux/BUILD`.
2. Inherit your test from `FuseTest` base class. It allows you to:
- - Run a fake FUSE server in background during each test setup.
- - Create pipes for communication and provide utility functions.
- - Stop FUSE server after test completes.
-3. Customize your comparison function for request assessment in FUSE server.
-4. Add the mapping of the size of structs if you are working on new FUSE
- opcode.
- - Please update `FuseTest::GetPayloadSize()` for each new FUSE opcode.
-5. Build the expected request-response pair of your FUSE operation.
-6. Call `SetExpected()` function to inject the expected reaction.
-7. Check the response and/or errors.
-8. Finally call `WaitCompleted()` to ensure the FUSE server acts correctly.
+ - Fork a fake FUSE server in background during each test setup.
+ - Create a pair of sockets for communication and provide utility
+ functions.
+ - Stop FUSE server and check if error occurs in it after test completes.
+3. Build the expected opcode-response pairs of your FUSE operation.
+4. Call `SetServerResponse()` to preset the next expected opcode and response.
+5. Do real filesystem operations (FUSE is mounted at `mount_point_`).
+6. Check FUSE response and/or errors.
+7. Retrieve FUSE request by `GetServerActualRequest()`.
+8. Check if the request is as expected.
A few customized matchers used in syscalls test are encouraged to test the
outcome of filesystem operations. Such as:
@@ -101,3 +149,40 @@ SyscallFailsWithErrno(...)
Please refer to [test/syscalls/README.md](../syscalls/README.md) for further
details.
+
+## Writing a new FuseTestCmd
+
+A `FuseTestCmd` is a control protocol used in the communication between the
+testing thread and the FUSE server. Such commands are sent from the testing
+thread to the FUSE server to set up, control, or inspect the behavior of the
+FUSE server in response to a sequence of FUSE requests.
+
+The lifecycle of a command contains following steps:
+
+1. The testing thread sends a `FuseTestCmd` via socket and waits for
+ completion.
+2. The FUSE server receives the command and does corresponding action.
+3. (Optional) The testing thread reads data from socket.
+4. The FUSE server sends a success indicator via socket after processing.
+5. The testing thread gets the success signal and continues testing.
+
+The success indicator, i.e. `WaitServerComplete()`, is crucial at the end of
+each `FuseTestCmd` sent from the testing thread. Because we don't want to begin
+filesystem operation if the requests have not been completely set up. Also, to
+test FUSE interactions in a sequential manner, concurrent requests are not
+supported now.
+
+To add a new `FuseTestCmd`, one must comply with following format:
+
+1. Add a new `FuseTestCmd` enum class item defined in `linux/fuse_base.h`
+2. Add a `SetServerXXX()` or `GetServerXXX()` public function in `FuseTest`.
+ This is how the testing thread will call to send control message. Define how
+ many bytes you want to send along with the command and what you will expect
+ to receive. Finally it should block and wait for a success indicator from
+ the FUSE server.
+3. Add a handler logic in the switch condition of `ServerHandleCommand()`. Use
+ `ServerSendData()` or declare a new private function such as
+ `ServerReceiveXXX()` or `ServerSendXXX()`. It is mandatory to set it private
+ since only the FUSE server (forked from `FuseTest` base class) can call it.
+ This is the server part of the specific `FuseTestCmd` and the format of the
+ data should be consistent with what the client expects in the previous step.
diff --git a/test/fuse/linux/BUILD b/test/fuse/linux/BUILD
index 4871bb531..7673252ec 100644
--- a/test/fuse/linux/BUILD
+++ b/test/fuse/linux/BUILD
@@ -11,7 +11,134 @@ cc_binary(
srcs = ["stat_test.cc"],
deps = [
gtest,
+ ":fuse_fd_util",
+ "//test/util:cleanup",
+ "//test/util:fs_util",
+ "//test/util:fuse_util",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ ],
+)
+
+cc_binary(
+ name = "open_test",
+ testonly = 1,
+ srcs = ["open_test.cc"],
+ deps = [
+ gtest,
":fuse_base",
+ "//test/util:fuse_util",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ ],
+)
+
+cc_binary(
+ name = "release_test",
+ testonly = 1,
+ srcs = ["release_test.cc"],
+ deps = [
+ gtest,
+ ":fuse_base",
+ "//test/util:fuse_util",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ ],
+)
+
+cc_binary(
+ name = "mknod_test",
+ testonly = 1,
+ srcs = ["mknod_test.cc"],
+ deps = [
+ gtest,
+ ":fuse_base",
+ "//test/util:fuse_util",
+ "//test/util:temp_umask",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ ],
+)
+
+cc_binary(
+ name = "symlink_test",
+ testonly = 1,
+ srcs = ["symlink_test.cc"],
+ deps = [
+ gtest,
+ ":fuse_base",
+ "//test/util:fuse_util",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ ],
+)
+
+cc_binary(
+ name = "readlink_test",
+ testonly = 1,
+ srcs = ["readlink_test.cc"],
+ deps = [
+ gtest,
+ ":fuse_base",
+ "//test/util:fuse_util",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ ],
+)
+
+cc_binary(
+ name = "mkdir_test",
+ testonly = 1,
+ srcs = ["mkdir_test.cc"],
+ deps = [
+ gtest,
+ ":fuse_base",
+ "//test/util:fuse_util",
+ "//test/util:temp_umask",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ ],
+)
+
+cc_binary(
+ name = "setstat_test",
+ testonly = 1,
+ srcs = ["setstat_test.cc"],
+ deps = [
+ gtest,
+ ":fuse_fd_util",
+ "//test/util:cleanup",
+ "//test/util:fs_util",
+ "//test/util:fuse_util",
+ "//test/util:temp_umask",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ ],
+)
+
+cc_binary(
+ name = "rmdir_test",
+ testonly = 1,
+ srcs = ["rmdir_test.cc"],
+ deps = [
+ gtest,
+ ":fuse_base",
+ "//test/util:fs_util",
+ "//test/util:fuse_util",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ ],
+)
+
+cc_binary(
+ name = "readdir_test",
+ testonly = 1,
+ srcs = ["readdir_test.cc"],
+ deps = [
+ gtest,
+ ":fuse_base",
+ "//test/util:fs_util",
+ "//test/util:fuse_util",
"//test/util:test_main",
"//test/util:test_util",
],
@@ -24,9 +151,80 @@ cc_library(
hdrs = ["fuse_base.h"],
deps = [
gtest,
+ "//test/util:fuse_util",
"//test/util:posix_error",
"//test/util:temp_path",
"//test/util:test_util",
"@com_google_absl//absl/strings:str_format",
],
)
+
+cc_library(
+ name = "fuse_fd_util",
+ testonly = 1,
+ srcs = ["fuse_fd_util.cc"],
+ hdrs = ["fuse_fd_util.h"],
+ deps = [
+ gtest,
+ ":fuse_base",
+ "//test/util:cleanup",
+ "//test/util:file_descriptor",
+ "//test/util:fuse_util",
+ "//test/util:posix_error",
+ ],
+)
+
+cc_binary(
+ name = "read_test",
+ testonly = 1,
+ srcs = ["read_test.cc"],
+ deps = [
+ gtest,
+ ":fuse_base",
+ "//test/util:fuse_util",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ ],
+)
+
+cc_binary(
+ name = "write_test",
+ testonly = 1,
+ srcs = ["write_test.cc"],
+ deps = [
+ gtest,
+ ":fuse_base",
+ "//test/util:fuse_util",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ ],
+)
+
+cc_binary(
+ name = "create_test",
+ testonly = 1,
+ srcs = ["create_test.cc"],
+ deps = [
+ gtest,
+ ":fuse_base",
+ "//test/util:fs_util",
+ "//test/util:fuse_util",
+ "//test/util:temp_umask",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ ],
+)
+
+cc_binary(
+ name = "unlink_test",
+ testonly = 1,
+ srcs = ["unlink_test.cc"],
+ deps = [
+ gtest,
+ ":fuse_base",
+ "//test/util:fuse_util",
+ "//test/util:temp_umask",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ ],
+)
diff --git a/test/fuse/linux/create_test.cc b/test/fuse/linux/create_test.cc
new file mode 100644
index 000000000..9a0219a58
--- /dev/null
+++ b/test/fuse/linux/create_test.cc
@@ -0,0 +1,128 @@
+// Copyright 2020 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 <linux/fuse.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <string>
+
+#include "gtest/gtest.h"
+#include "test/fuse/linux/fuse_base.h"
+#include "test/util/fs_util.h"
+#include "test/util/fuse_util.h"
+#include "test/util/temp_umask.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+class CreateTest : public FuseTest {
+ protected:
+ const std::string test_file_name_ = "test_file";
+ const mode_t mode = S_IFREG | S_IRWXU | S_IRWXG | S_IRWXO;
+};
+
+TEST_F(CreateTest, CreateFile) {
+ const std::string test_file_path =
+ JoinPath(mount_point_.path().c_str(), test_file_name_);
+
+ // Ensure the file doesn't exist.
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header),
+ .error = -ENOENT,
+ };
+ auto iov_out = FuseGenerateIovecs(out_header);
+ SetServerResponse(FUSE_LOOKUP, iov_out);
+
+ // creat(2) is equal to open(2) with open_flags O_CREAT | O_WRONLY | O_TRUNC.
+ const mode_t new_mask = S_IWGRP | S_IWOTH;
+ const int open_flags = O_CREAT | O_WRONLY | O_TRUNC;
+ out_header.error = 0;
+ out_header.len = sizeof(struct fuse_out_header) +
+ sizeof(struct fuse_entry_out) + sizeof(struct fuse_open_out);
+ struct fuse_entry_out entry_payload = DefaultEntryOut(mode & ~new_mask, 2);
+ struct fuse_open_out out_payload = {
+ .fh = 1,
+ .open_flags = open_flags,
+ };
+ iov_out = FuseGenerateIovecs(out_header, entry_payload, out_payload);
+ SetServerResponse(FUSE_CREATE, iov_out);
+
+ // kernfs generates a successive FUSE_OPEN after the file is created. Linux's
+ // fuse kernel module will not send this FUSE_OPEN after creat(2).
+ out_header.len =
+ sizeof(struct fuse_out_header) + sizeof(struct fuse_open_out);
+ iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_OPEN, iov_out);
+
+ int fd;
+ TempUmask mask(new_mask);
+ EXPECT_THAT(fd = creat(test_file_path.c_str(), mode), SyscallSucceeds());
+ EXPECT_THAT(fcntl(fd, F_GETFL),
+ SyscallSucceedsWithValue(open_flags & O_ACCMODE));
+
+ struct fuse_in_header in_header;
+ struct fuse_create_in in_payload;
+ std::vector<char> name(test_file_name_.size() + 1);
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload, name);
+
+ // Skip the request of FUSE_LOOKUP.
+ SkipServerActualRequest();
+
+ // Get the first FUSE_CREATE.
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_header.len, sizeof(in_header) + sizeof(in_payload) +
+ test_file_name_.size() + 1);
+ EXPECT_EQ(in_header.opcode, FUSE_CREATE);
+ EXPECT_EQ(in_payload.flags, open_flags);
+ EXPECT_EQ(in_payload.mode, mode & ~new_mask);
+ EXPECT_EQ(in_payload.umask, new_mask);
+ EXPECT_EQ(std::string(name.data()), test_file_name_);
+
+ // Get the successive FUSE_OPEN.
+ struct fuse_open_in in_payload_open;
+ iov_in = FuseGenerateIovecs(in_header, in_payload_open);
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_header.len, sizeof(in_header) + sizeof(in_payload_open));
+ EXPECT_EQ(in_header.opcode, FUSE_OPEN);
+ EXPECT_EQ(in_payload_open.flags, open_flags & O_ACCMODE);
+
+ EXPECT_THAT(close(fd), SyscallSucceeds());
+ // Skip the FUSE_RELEASE.
+ SkipServerActualRequest();
+}
+
+TEST_F(CreateTest, CreateFileAlreadyExists) {
+ const std::string test_file_path =
+ JoinPath(mount_point_.path().c_str(), test_file_name_);
+
+ const int open_flags = O_CREAT | O_EXCL;
+
+ SetServerInodeLookup(test_file_name_);
+
+ EXPECT_THAT(open(test_file_path.c_str(), mode, open_flags),
+ SyscallFailsWithErrno(EEXIST));
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/fuse/linux/fuse_base.cc b/test/fuse/linux/fuse_base.cc
index 9c3124472..5b45804e1 100644
--- a/test/fuse/linux/fuse_base.cc
+++ b/test/fuse/linux/fuse_base.cc
@@ -16,17 +16,17 @@
#include <fcntl.h>
#include <linux/fuse.h>
-#include <string.h>
+#include <poll.h>
#include <sys/mount.h>
+#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
-#include <iostream>
-
#include "gtest/gtest.h"
#include "absl/strings/str_format.h"
+#include "test/util/fuse_util.h"
#include "test/util/posix_error.h"
#include "test/util/temp_path.h"
#include "test/util/test_util.h"
@@ -39,49 +39,123 @@ void FuseTest::SetUp() {
SetUpFuseServer();
}
-void FuseTest::TearDown() { UnmountFuse(); }
-
-// Since CompareRequest is running in background thread, gTest assertions and
-// expectations won't directly reflect the test result. However, the FUSE
-// background server still connects to the same standard I/O as testing main
-// thread. So EXPECT_XX can still be used to show different results. To
-// ensure failed testing result is observable, return false and the result
-// will be sent to test main thread via pipe.
-bool FuseTest::CompareRequest(void* expected_mem, size_t expected_len,
- void* real_mem, size_t real_len) {
- if (expected_len != real_len) return false;
- return memcmp(expected_mem, real_mem, expected_len) == 0;
+void FuseTest::TearDown() {
+ EXPECT_EQ(GetServerNumUnconsumedRequests(), 0);
+ EXPECT_EQ(GetServerNumUnsentResponses(), 0);
+ UnmountFuse();
}
-// SetExpected is called by the testing main thread to set expected request-
-// response pair of a single FUSE operation.
-void FuseTest::SetExpected(struct iovec* iov_in, int iov_in_cnt,
- struct iovec* iov_out, int iov_out_cnt) {
- EXPECT_THAT(RetryEINTR(writev)(set_expected_[1], iov_in, iov_in_cnt),
- SyscallSucceedsWithValue(::testing::Gt(0)));
- WaitCompleted();
+// Sends 3 parts of data to the FUSE server:
+// 1. The `kSetResponse` command
+// 2. The expected opcode
+// 3. The fake FUSE response
+// Then waits for the FUSE server to notify its completion.
+void FuseTest::SetServerResponse(uint32_t opcode,
+ std::vector<struct iovec>& iovecs) {
+ uint32_t cmd = static_cast<uint32_t>(FuseTestCmd::kSetResponse);
+ EXPECT_THAT(RetryEINTR(write)(sock_[0], &cmd, sizeof(cmd)),
+ SyscallSucceedsWithValue(sizeof(cmd)));
+
+ EXPECT_THAT(RetryEINTR(write)(sock_[0], &opcode, sizeof(opcode)),
+ SyscallSucceedsWithValue(sizeof(opcode)));
- EXPECT_THAT(RetryEINTR(writev)(set_expected_[1], iov_out, iov_out_cnt),
- SyscallSucceedsWithValue(::testing::Gt(0)));
- WaitCompleted();
+ EXPECT_THAT(RetryEINTR(writev)(sock_[0], iovecs.data(), iovecs.size()),
+ SyscallSucceeds());
+
+ WaitServerComplete();
}
-// WaitCompleted waits for the FUSE server to finish its job and check if it
+// Waits for the FUSE server to finish its blocking job and check if it
// completes without errors.
-void FuseTest::WaitCompleted() {
- char success;
- EXPECT_THAT(RetryEINTR(read)(done_[0], &success, sizeof(success)),
- SyscallSucceedsWithValue(1));
+void FuseTest::WaitServerComplete() {
+ uint32_t success;
+ EXPECT_THAT(RetryEINTR(read)(sock_[0], &success, sizeof(success)),
+ SyscallSucceedsWithValue(sizeof(success)));
+ ASSERT_EQ(success, 1);
+}
+
+// Sends the `kGetRequest` command to the FUSE server, then reads the next
+// request into iovec struct. The order of calling this function should be
+// the same as the one of SetServerResponse().
+void FuseTest::GetServerActualRequest(std::vector<struct iovec>& iovecs) {
+ uint32_t cmd = static_cast<uint32_t>(FuseTestCmd::kGetRequest);
+ EXPECT_THAT(RetryEINTR(write)(sock_[0], &cmd, sizeof(cmd)),
+ SyscallSucceedsWithValue(sizeof(cmd)));
+
+ EXPECT_THAT(RetryEINTR(readv)(sock_[0], iovecs.data(), iovecs.size()),
+ SyscallSucceeds());
+
+ WaitServerComplete();
+}
+
+// Sends a FuseTestCmd command to the FUSE server, reads from the socket, and
+// returns the corresponding data.
+uint32_t FuseTest::GetServerData(uint32_t cmd) {
+ uint32_t data;
+ EXPECT_THAT(RetryEINTR(write)(sock_[0], &cmd, sizeof(cmd)),
+ SyscallSucceedsWithValue(sizeof(cmd)));
+
+ EXPECT_THAT(RetryEINTR(read)(sock_[0], &data, sizeof(data)),
+ SyscallSucceedsWithValue(sizeof(data)));
+
+ WaitServerComplete();
+ return data;
+}
+
+uint32_t FuseTest::GetServerNumUnconsumedRequests() {
+ return GetServerData(
+ static_cast<uint32_t>(FuseTestCmd::kGetNumUnconsumedRequests));
+}
+
+uint32_t FuseTest::GetServerNumUnsentResponses() {
+ return GetServerData(
+ static_cast<uint32_t>(FuseTestCmd::kGetNumUnsentResponses));
+}
+
+uint32_t FuseTest::GetServerTotalReceivedBytes() {
+ return GetServerData(
+ static_cast<uint32_t>(FuseTestCmd::kGetTotalReceivedBytes));
+}
+
+// Sends the `kSkipRequest` command to the FUSE server, which would skip
+// current stored request data.
+void FuseTest::SkipServerActualRequest() {
+ uint32_t cmd = static_cast<uint32_t>(FuseTestCmd::kSkipRequest);
+ EXPECT_THAT(RetryEINTR(write)(sock_[0], &cmd, sizeof(cmd)),
+ SyscallSucceedsWithValue(sizeof(cmd)));
+
+ WaitServerComplete();
}
-void FuseTest::MountFuse() {
+// Sends the `kSetInodeLookup` command, expected mode, and the path of the
+// inode to create under the mount point.
+void FuseTest::SetServerInodeLookup(const std::string& path, mode_t mode,
+ uint64_t size) {
+ uint32_t cmd = static_cast<uint32_t>(FuseTestCmd::kSetInodeLookup);
+ EXPECT_THAT(RetryEINTR(write)(sock_[0], &cmd, sizeof(cmd)),
+ SyscallSucceedsWithValue(sizeof(cmd)));
+
+ EXPECT_THAT(RetryEINTR(write)(sock_[0], &mode, sizeof(mode)),
+ SyscallSucceedsWithValue(sizeof(mode)));
+
+ EXPECT_THAT(RetryEINTR(write)(sock_[0], &size, sizeof(size)),
+ SyscallSucceedsWithValue(sizeof(size)));
+
+ // Pad 1 byte for null-terminate c-string.
+ EXPECT_THAT(RetryEINTR(write)(sock_[0], path.c_str(), path.size() + 1),
+ SyscallSucceedsWithValue(path.size() + 1));
+
+ WaitServerComplete();
+}
+
+void FuseTest::MountFuse(const char* mountOpts) {
EXPECT_THAT(dev_fd_ = open("/dev/fuse", O_RDWR), SyscallSucceeds());
- std::string mount_opts = absl::StrFormat("fd=%d,%s", dev_fd_, kMountOpts);
+ std::string mount_opts = absl::StrFormat("fd=%d,%s", dev_fd_, mountOpts);
mount_point_ = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
EXPECT_THAT(mount("fuse", mount_point_.path().c_str(), "fuse",
MS_NODEV | MS_NOSUID, mount_opts.c_str()),
- SyscallSucceedsWithValue(0));
+ SyscallSucceeds());
}
void FuseTest::UnmountFuse() {
@@ -89,13 +163,13 @@ void FuseTest::UnmountFuse() {
// TODO(gvisor.dev/issue/3330): ensure the process is terminated successfully.
}
-// ConsumeFuseInit consumes the first FUSE request and returns the
-// corresponding PosixError.
-PosixError FuseTest::ConsumeFuseInit() {
+// Consumes the first FUSE request and returns the corresponding PosixError.
+PosixError FuseTest::ServerConsumeFuseInit(
+ const struct fuse_init_out* out_payload) {
+ std::vector<char> buf(FUSE_MIN_READ_BUFFER);
RETURN_ERROR_IF_SYSCALL_FAIL(
- RetryEINTR(read)(dev_fd_, buf_.data(), buf_.size()));
+ RetryEINTR(read)(dev_fd_, buf.data(), buf.size()));
- struct iovec iov_out[2];
struct fuse_out_header out_header = {
.len = sizeof(struct fuse_out_header) + sizeof(struct fuse_init_out),
.error = 0,
@@ -103,72 +177,74 @@ PosixError FuseTest::ConsumeFuseInit() {
};
// Returns a fake fuse_init_out with 7.0 version to avoid ECONNREFUSED
// error in the initialization of FUSE connection.
- struct fuse_init_out out_payload = {
- .major = 7,
- };
- iov_out[0].iov_len = sizeof(out_header);
- iov_out[0].iov_base = &out_header;
- iov_out[1].iov_len = sizeof(out_payload);
- iov_out[1].iov_base = &out_payload;
+ auto iov_out = FuseGenerateIovecs(
+ out_header, *const_cast<struct fuse_init_out*>(out_payload));
- RETURN_ERROR_IF_SYSCALL_FAIL(RetryEINTR(writev)(dev_fd_, iov_out, 2));
+ RETURN_ERROR_IF_SYSCALL_FAIL(
+ RetryEINTR(writev)(dev_fd_, iov_out.data(), iov_out.size()));
return NoError();
}
-// ReceiveExpected reads 1 pair of expected fuse request-response `iovec`s
-// from pipe and save them into member variables of this testing instance.
-void FuseTest::ReceiveExpected() {
- // Set expected fuse_in request.
- EXPECT_THAT(len_in_ = RetryEINTR(read)(set_expected_[0], mem_in_.data(),
- mem_in_.size()),
- SyscallSucceedsWithValue(::testing::Gt(0)));
- MarkDone(len_in_ > 0);
+// Reads 1 expected opcode and a fake response from socket and save them into
+// the serial buffer of this testing instance.
+void FuseTest::ServerReceiveResponse() {
+ ssize_t len;
+ uint32_t opcode;
+ std::vector<char> buf(FUSE_MIN_READ_BUFFER);
+ EXPECT_THAT(RetryEINTR(read)(sock_[1], &opcode, sizeof(opcode)),
+ SyscallSucceedsWithValue(sizeof(opcode)));
- // Set expected fuse_out response.
- EXPECT_THAT(len_out_ = RetryEINTR(read)(set_expected_[0], mem_out_.data(),
- mem_out_.size()),
- SyscallSucceedsWithValue(::testing::Gt(0)));
- MarkDone(len_out_ > 0);
+ EXPECT_THAT(len = RetryEINTR(read)(sock_[1], buf.data(), buf.size()),
+ SyscallSucceeds());
+
+ responses_.AddMemBlock(opcode, buf.data(), len);
}
-// MarkDone writes 1 byte of success indicator through pipe.
-void FuseTest::MarkDone(bool success) {
- char data = success ? 1 : 0;
- EXPECT_THAT(RetryEINTR(write)(done_[1], &data, sizeof(data)),
- SyscallSucceedsWithValue(1));
+// Writes 1 byte of success indicator through socket.
+void FuseTest::ServerCompleteWith(bool success) {
+ uint32_t data = success ? 1 : 0;
+ ServerSendData(data);
}
-// FuseLoop is the implementation of the fake FUSE server. Read from /dev/fuse,
-// compare the request by CompareRequest (use derived function if specified),
-// and write the expected response to /dev/fuse.
-void FuseTest::FuseLoop() {
- bool success = true;
- ssize_t len = 0;
+// ServerFuseLoop is the implementation of the fake FUSE server. Monitors 2
+// file descriptors: /dev/fuse and sock_[1]. Events from /dev/fuse are FUSE
+// requests and events from sock_[1] are FUSE testing commands, leading by
+// a FuseTestCmd data to indicate the command.
+void FuseTest::ServerFuseLoop() {
+ const int nfds = 2;
+ struct pollfd fds[nfds] = {
+ {
+ .fd = dev_fd_,
+ .events = POLL_IN | POLLHUP | POLLERR | POLLNVAL,
+ },
+ {
+ .fd = sock_[1],
+ .events = POLL_IN | POLLHUP | POLLERR | POLLNVAL,
+ },
+ };
+
while (true) {
- ReceiveExpected();
+ ASSERT_THAT(poll(fds, nfds, -1), SyscallSucceeds());
- EXPECT_THAT(len = RetryEINTR(read)(dev_fd_, buf_.data(), buf_.size()),
- SyscallSucceedsWithValue(len_in_));
- if (len != len_in_) success = false;
+ for (int fd_idx = 0; fd_idx < nfds; ++fd_idx) {
+ if (fds[fd_idx].revents == 0) continue;
- if (!CompareRequest(buf_.data(), len_in_, mem_in_.data(), len_in_)) {
- std::cerr << "the FUSE request is not expected" << std::endl;
- success = false;
+ ASSERT_EQ(fds[fd_idx].revents, POLL_IN);
+ if (fds[fd_idx].fd == sock_[1]) {
+ ServerHandleCommand();
+ } else if (fds[fd_idx].fd == dev_fd_) {
+ ServerProcessFuseRequest();
+ }
}
-
- EXPECT_THAT(len = RetryEINTR(write)(dev_fd_, mem_out_.data(), len_out_),
- SyscallSucceedsWithValue(len_out_));
- if (len != len_out_) success = false;
- MarkDone(success);
}
}
-// SetUpFuseServer creates 2 pipes. First is for testing client to send the
-// expected request-response pair, and the other acts as a checkpoint for the
-// FUSE server to notify the client that it can proceed.
-void FuseTest::SetUpFuseServer() {
- ASSERT_THAT(pipe(set_expected_), SyscallSucceedsWithValue(0));
- ASSERT_THAT(pipe(done_), SyscallSucceedsWithValue(0));
+// SetUpFuseServer creates 1 socketpair and fork the process. The parent thread
+// becomes testing thread and the child thread becomes the FUSE server running
+// in background. These 2 threads are connected via socketpair. sock_[0] is
+// opened in testing thread and sock_[1] is opened in the FUSE server.
+void FuseTest::SetUpFuseServer(const struct fuse_init_out* payload) {
+ ASSERT_THAT(socketpair(AF_UNIX, SOCK_STREAM, 0, sock_), SyscallSucceeds());
switch (fork()) {
case -1:
@@ -177,31 +253,194 @@ void FuseTest::SetUpFuseServer() {
case 0:
break;
default:
- ASSERT_THAT(close(set_expected_[0]), SyscallSucceedsWithValue(0));
- ASSERT_THAT(close(done_[1]), SyscallSucceedsWithValue(0));
- WaitCompleted();
+ ASSERT_THAT(close(sock_[1]), SyscallSucceeds());
+ WaitServerComplete();
return;
}
- ASSERT_THAT(close(set_expected_[1]), SyscallSucceedsWithValue(0));
- ASSERT_THAT(close(done_[0]), SyscallSucceedsWithValue(0));
-
- MarkDone(ConsumeFuseInit().ok());
-
- FuseLoop();
+ // Begin child thread, i.e. the FUSE server.
+ ASSERT_THAT(close(sock_[0]), SyscallSucceeds());
+ ServerCompleteWith(ServerConsumeFuseInit(payload).ok());
+ ServerFuseLoop();
_exit(0);
}
-// GetPayloadSize is a helper function to get the number of bytes of a
-// specific FUSE operation struct.
-size_t FuseTest::GetPayloadSize(uint32_t opcode, bool in) {
- switch (opcode) {
- case FUSE_INIT:
- return in ? sizeof(struct fuse_init_in) : sizeof(struct fuse_init_out);
+void FuseTest::ServerSendData(uint32_t data) {
+ EXPECT_THAT(RetryEINTR(write)(sock_[1], &data, sizeof(data)),
+ SyscallSucceedsWithValue(sizeof(data)));
+}
+
+// Reads FuseTestCmd sent from testing thread and routes to correct handler.
+// Since each command should be a blocking operation, a `ServerCompleteWith()`
+// is required after the switch keyword.
+void FuseTest::ServerHandleCommand() {
+ uint32_t cmd;
+ EXPECT_THAT(RetryEINTR(read)(sock_[1], &cmd, sizeof(cmd)),
+ SyscallSucceedsWithValue(sizeof(cmd)));
+
+ switch (static_cast<FuseTestCmd>(cmd)) {
+ case FuseTestCmd::kSetResponse:
+ ServerReceiveResponse();
+ break;
+ case FuseTestCmd::kSetInodeLookup:
+ ServerReceiveInodeLookup();
+ break;
+ case FuseTestCmd::kGetRequest:
+ ServerSendReceivedRequest();
+ break;
+ case FuseTestCmd::kGetTotalReceivedBytes:
+ ServerSendData(static_cast<uint32_t>(requests_.UsedBytes()));
+ break;
+ case FuseTestCmd::kGetNumUnconsumedRequests:
+ ServerSendData(static_cast<uint32_t>(requests_.RemainingBlocks()));
+ break;
+ case FuseTestCmd::kGetNumUnsentResponses:
+ ServerSendData(static_cast<uint32_t>(responses_.RemainingBlocks()));
+ break;
+ case FuseTestCmd::kSkipRequest:
+ ServerSkipReceivedRequest();
+ break;
default:
+ FAIL() << "Unknown FuseTestCmd " << cmd;
break;
}
- return 0;
+
+ ServerCompleteWith(!HasFailure());
+}
+
+// Reads the expected file mode and the path of one file. Crafts a basic
+// `fuse_entry_out` memory block and inserts into a map for future use.
+// The FUSE server will always return this response if a FUSE_LOOKUP
+// request with this specific path comes in.
+void FuseTest::ServerReceiveInodeLookup() {
+ mode_t mode;
+ uint64_t size;
+ std::vector<char> buf(FUSE_MIN_READ_BUFFER);
+
+ EXPECT_THAT(RetryEINTR(read)(sock_[1], &mode, sizeof(mode)),
+ SyscallSucceedsWithValue(sizeof(mode)));
+
+ EXPECT_THAT(RetryEINTR(read)(sock_[1], &size, sizeof(size)),
+ SyscallSucceedsWithValue(sizeof(size)));
+
+ EXPECT_THAT(RetryEINTR(read)(sock_[1], buf.data(), buf.size()),
+ SyscallSucceeds());
+
+ std::string path(buf.data());
+
+ uint32_t out_len =
+ sizeof(struct fuse_out_header) + sizeof(struct fuse_entry_out);
+ struct fuse_out_header out_header = {
+ .len = out_len,
+ .error = 0,
+ };
+ struct fuse_entry_out out_payload = DefaultEntryOut(mode, nodeid_);
+ // Since this is only used in test, nodeid_ is simply increased by 1 to
+ // comply with the unqiueness of different path.
+ ++nodeid_;
+
+ // Set the size.
+ out_payload.attr.size = size;
+
+ memcpy(buf.data(), &out_header, sizeof(out_header));
+ memcpy(buf.data() + sizeof(out_header), &out_payload, sizeof(out_payload));
+ lookups_.AddMemBlock(FUSE_LOOKUP, buf.data(), out_len);
+ lookup_map_[path] = lookups_.Next();
+}
+
+// Sends the received request pointed by current cursor and advances cursor.
+void FuseTest::ServerSendReceivedRequest() {
+ if (requests_.End()) {
+ FAIL() << "No more received request.";
+ return;
+ }
+ auto mem_block = requests_.Next();
+ EXPECT_THAT(
+ RetryEINTR(write)(sock_[1], requests_.DataAtOffset(mem_block.offset),
+ mem_block.len),
+ SyscallSucceedsWithValue(mem_block.len));
+}
+
+// Skip the request pointed by current cursor.
+void FuseTest::ServerSkipReceivedRequest() {
+ if (requests_.End()) {
+ FAIL() << "No more received request.";
+ return;
+ }
+ requests_.Next();
+}
+
+// Handles FUSE request. Reads request from /dev/fuse, checks if it has the
+// same opcode as expected, and responds with the saved fake FUSE response.
+// The FUSE request is copied to the serial buffer and can be retrieved one-
+// by-one by calling GetServerActualRequest from testing thread.
+void FuseTest::ServerProcessFuseRequest() {
+ ssize_t len;
+ std::vector<char> buf(FUSE_MIN_READ_BUFFER);
+
+ // Read FUSE request.
+ EXPECT_THAT(len = RetryEINTR(read)(dev_fd_, buf.data(), buf.size()),
+ SyscallSucceeds());
+ fuse_in_header* in_header = reinterpret_cast<fuse_in_header*>(buf.data());
+
+ // Check if this is a preset FUSE_LOOKUP path.
+ if (in_header->opcode == FUSE_LOOKUP) {
+ std::string path(buf.data() + sizeof(struct fuse_in_header));
+ auto it = lookup_map_.find(path);
+ if (it != lookup_map_.end()) {
+ // Matches a preset path. Reply with fake data and skip saving the
+ // request.
+ ServerRespondFuseSuccess(lookups_, it->second, in_header->unique);
+ return;
+ }
+ }
+
+ requests_.AddMemBlock(in_header->opcode, buf.data(), len);
+
+ if (in_header->opcode == FUSE_RELEASE || in_header->opcode == FUSE_RELEASEDIR)
+ return;
+ // Check if there is a corresponding response.
+ if (responses_.End()) {
+ GTEST_NONFATAL_FAILURE_("No more FUSE response is expected");
+ ServerRespondFuseError(in_header->unique);
+ return;
+ }
+ auto mem_block = responses_.Next();
+ if (in_header->opcode != mem_block.opcode) {
+ std::string message = absl::StrFormat("Expect opcode %d but got %d",
+ mem_block.opcode, in_header->opcode);
+ GTEST_NONFATAL_FAILURE_(message.c_str());
+ // We won't get correct response if opcode is not expected. Send error
+ // response here to avoid wrong parsing by VFS.
+ ServerRespondFuseError(in_header->unique);
+ return;
+ }
+
+ // Write FUSE response.
+ ServerRespondFuseSuccess(responses_, mem_block, in_header->unique);
+}
+
+void FuseTest::ServerRespondFuseSuccess(FuseMemBuffer& mem_buf,
+ const FuseMemBlock& block,
+ uint64_t unique) {
+ fuse_out_header* out_header =
+ reinterpret_cast<fuse_out_header*>(mem_buf.DataAtOffset(block.offset));
+
+ // Patch `unique` in fuse_out_header to avoid EINVAL caused by responding
+ // with an unknown `unique`.
+ out_header->unique = unique;
+ EXPECT_THAT(RetryEINTR(write)(dev_fd_, out_header, block.len),
+ SyscallSucceedsWithValue(block.len));
+}
+
+void FuseTest::ServerRespondFuseError(uint64_t unique) {
+ fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header),
+ .error = ENOSYS,
+ .unique = unique,
+ };
+ EXPECT_THAT(RetryEINTR(write)(dev_fd_, &out_header, sizeof(out_header)),
+ SyscallSucceedsWithValue(sizeof(out_header)));
}
} // namespace testing
diff --git a/test/fuse/linux/fuse_base.h b/test/fuse/linux/fuse_base.h
index 3a2f255a9..6ad296ca2 100644
--- a/test/fuse/linux/fuse_base.h
+++ b/test/fuse/linux/fuse_base.h
@@ -16,8 +16,12 @@
#define GVISOR_TEST_FUSE_FUSE_BASE_H_
#include <linux/fuse.h>
+#include <string.h>
+#include <sys/stat.h>
#include <sys/uio.h>
+#include <iostream>
+#include <unordered_map>
#include <vector>
#include "gtest/gtest.h"
@@ -29,68 +33,216 @@ namespace testing {
constexpr char kMountOpts[] = "rootmode=755,user_id=0,group_id=0";
-class FuseTest : public ::testing::Test {
+constexpr struct fuse_init_out kDefaultFUSEInitOutPayload = {.major = 7};
+
+// Internal commands used to communicate between testing thread and the FUSE
+// server. See test/fuse/README.md for further detail.
+enum class FuseTestCmd {
+ kSetResponse = 0,
+ kSetInodeLookup,
+ kGetRequest,
+ kGetNumUnconsumedRequests,
+ kGetNumUnsentResponses,
+ kGetTotalReceivedBytes,
+ kSkipRequest,
+};
+
+// Holds the information of a memory block in a serial buffer.
+struct FuseMemBlock {
+ uint32_t opcode;
+ size_t offset;
+ size_t len;
+};
+
+// A wrapper of a simple serial buffer that can be used with read(2) and
+// write(2). Contains a cursor to indicate accessing. This class is not thread-
+// safe and can only be used in single-thread version.
+class FuseMemBuffer {
public:
- FuseTest() {
- buf_.resize(FUSE_MIN_READ_BUFFER);
- mem_in_.resize(FUSE_MIN_READ_BUFFER);
- mem_out_.resize(FUSE_MIN_READ_BUFFER);
+ FuseMemBuffer() : cursor_(0) {
+ // To read from /dev/fuse, a buffer needs at least FUSE_MIN_READ_BUFFER
+ // bytes to avoid EINVAL. FuseMemBuffer holds memory that can accommodate
+ // a sequence of FUSE request/response, so it is initiated with double
+ // minimal requirement.
+ mem_.resize(FUSE_MIN_READ_BUFFER * 2);
}
- void SetUp() override;
- void TearDown() override;
- // CompareRequest is used by the FUSE server and should be implemented to
- // compare different FUSE operations. It compares the actual FUSE input
- // request with the expected one set by `SetExpected()`.
- virtual bool CompareRequest(void* expected_mem, size_t expected_len,
- void* real_mem, size_t real_len);
+ // Returns whether there is no memory block.
+ bool Empty() { return blocks_.empty(); }
+
+ // Returns if there is no more remaining memory blocks.
+ bool End() { return cursor_ == blocks_.size(); }
+
+ // Returns how many bytes that have been received.
+ size_t UsedBytes() {
+ return Empty() ? 0 : blocks_.back().offset + blocks_.back().len;
+ }
+
+ // Returns the available bytes remains in the serial buffer.
+ size_t AvailBytes() { return mem_.size() - UsedBytes(); }
+
+ // Appends a memory block information that starts at the tail of the serial
+ // buffer. /dev/fuse requires at least FUSE_MIN_READ_BUFFER bytes to read, or
+ // it will issue EINVAL. If it is not enough, just double the buffer length.
+ void AddMemBlock(uint32_t opcode, void* data, size_t len) {
+ if (AvailBytes() < FUSE_MIN_READ_BUFFER) {
+ mem_.resize(mem_.size() << 1);
+ }
+ size_t offset = UsedBytes();
+ memcpy(mem_.data() + offset, data, len);
+ blocks_.push_back(FuseMemBlock{opcode, offset, len});
+ }
+
+ // Returns the memory address at a specific offset. Used with read(2) or
+ // write(2).
+ char* DataAtOffset(size_t offset) { return mem_.data() + offset; }
+
+ // Returns current memory block pointed by the cursor and increase by 1.
+ FuseMemBlock Next() {
+ if (End()) {
+ std::cerr << "Buffer is already exhausted." << std::endl;
+ return FuseMemBlock{};
+ }
+ return blocks_[cursor_++];
+ }
+
+ // Returns the number of the blocks that has not been requested.
+ size_t RemainingBlocks() { return blocks_.size() - cursor_; }
+
+ private:
+ size_t cursor_;
+ std::vector<FuseMemBlock> blocks_;
+ std::vector<char> mem_;
+};
- // SetExpected is called by the testing main thread. Writes a request-
- // response pair into FUSE server's member variables via pipe.
- void SetExpected(struct iovec* iov_in, int iov_in_cnt, struct iovec* iov_out,
- int iov_out_cnt);
+// FuseTest base class is useful in FUSE integration test. Inherit this class
+// to automatically set up a fake FUSE server and use the member functions
+// to manipulate with it. Refer to test/fuse/README.md for detailed explanation.
+class FuseTest : public ::testing::Test {
+ public:
+ // nodeid_ is the ID of a fake inode. We starts from 2 since 1 is occupied by
+ // the mount point.
+ FuseTest() : nodeid_(2) {}
+ void SetUp() override;
+ void TearDown() override;
- // WaitCompleted waits for FUSE server to complete its processing. It
- // complains if the FUSE server responds failure during tests.
- void WaitCompleted();
+ // Called by the testing thread to set up a fake response for an expected
+ // opcode via socket. This can be used multiple times to define a sequence of
+ // expected FUSE reactions.
+ void SetServerResponse(uint32_t opcode, std::vector<struct iovec>& iovecs);
+
+ // Called by the testing thread to install a fake path under the mount point.
+ // e.g. a file under /mnt/dir/file and moint point is /mnt, then it will look
+ // up "dir/file" in this case.
+ //
+ // It sets a fixed response to the FUSE_LOOKUP requests issued with this
+ // path, pretending there is an inode and avoid ENOENT when testing. If mode
+ // is not given, it creates a regular file with mode 0600.
+ void SetServerInodeLookup(const std::string& path,
+ mode_t mode = S_IFREG | S_IRUSR | S_IWUSR,
+ uint64_t size = 512);
+
+ // Called by the testing thread to ask the FUSE server for its next received
+ // FUSE request. Be sure to use the corresponding struct of iovec to receive
+ // data from server.
+ void GetServerActualRequest(std::vector<struct iovec>& iovecs);
+
+ // Called by the testing thread to query the number of unconsumed requests in
+ // the requests_ serial buffer of the FUSE server. TearDown() ensures all
+ // FUSE requests received by the FUSE server were consumed by the testing
+ // thread.
+ uint32_t GetServerNumUnconsumedRequests();
+
+ // Called by the testing thread to query the number of unsent responses in
+ // the responses_ serial buffer of the FUSE server. TearDown() ensures all
+ // preset FUSE responses were sent out by the FUSE server.
+ uint32_t GetServerNumUnsentResponses();
+
+ // Called by the testing thread to ask the FUSE server for its total received
+ // bytes from /dev/fuse.
+ uint32_t GetServerTotalReceivedBytes();
+
+ // Called by the testing thread to ask the FUSE server to skip stored
+ // request data.
+ void SkipServerActualRequest();
protected:
TempPath mount_point_;
- private:
- void MountFuse();
+ // Opens /dev/fuse and inherit the file descriptor for the FUSE server.
+ void MountFuse(const char* mountOpts = kMountOpts);
+
+ // Creates a socketpair for communication and forks FUSE server.
+ void SetUpFuseServer(
+ const struct fuse_init_out* payload = &kDefaultFUSEInitOutPayload);
+
+ // Unmounts the mountpoint of the FUSE server.
void UnmountFuse();
- // ConsumeFuseInit is only used during FUSE server setup.
- PosixError ConsumeFuseInit();
+ private:
+ // Sends a FuseTestCmd and gets a uint32_t data from the FUSE server.
+ inline uint32_t GetServerData(uint32_t cmd);
+
+ // Waits for FUSE server to complete its processing. Complains if the FUSE
+ // server responds any failure during tests.
+ void WaitServerComplete();
- // ReceiveExpected is the FUSE server side's corresponding code of
- // `SetExpected()`. Save the request-response pair into its memory.
- void ReceiveExpected();
+ // The FUSE server stays here and waits next command or FUSE request until it
+ // is terminated.
+ void ServerFuseLoop();
- // MarkDone is used by the FUSE server to tell testing main if it's OK to
- // proceed next command.
- void MarkDone(bool success);
+ // Used by the FUSE server to tell testing thread if it is OK to proceed next
+ // command. Will be issued after processing each FuseTestCmd.
+ void ServerCompleteWith(bool success);
- // FuseLoop is where the FUSE server stay until it is terminated.
- void FuseLoop();
+ // Consumes the first FUSE request when mounting FUSE. Replies with a
+ // response with empty payload.
+ PosixError ServerConsumeFuseInit(const struct fuse_init_out* payload);
- // SetUpFuseServer creates 2 pipes for communication and forks FUSE server.
- void SetUpFuseServer();
+ // A command switch that dispatch different FuseTestCmd to its handler.
+ void ServerHandleCommand();
- // GetPayloadSize is a helper function to get the number of bytes of a
- // specific FUSE operation struct.
- size_t GetPayloadSize(uint32_t opcode, bool in);
+ // The FUSE server side's corresponding code of `SetServerResponse()`.
+ // Handles `kSetResponse` command. Saves the fake response into its output
+ // memory queue.
+ void ServerReceiveResponse();
+
+ // The FUSE server side's corresponding code of `SetServerInodeLookup()`.
+ // Handles `kSetInodeLookup` command. Receives an expected file mode and
+ // file path under the mount point.
+ void ServerReceiveInodeLookup();
+
+ // The FUSE server side's corresponding code of `GetServerActualRequest()`.
+ // Handles `kGetRequest` command. Sends the next received request pointed by
+ // the cursor.
+ void ServerSendReceivedRequest();
+
+ // Sends a uint32_t data via socket.
+ inline void ServerSendData(uint32_t data);
+
+ // The FUSE server side's corresponding code of `SkipServerActualRequest()`.
+ // Handles `kSkipRequest` command. Skip the request pointed by current cursor.
+ void ServerSkipReceivedRequest();
+
+ // Handles FUSE request sent to /dev/fuse by its saved responses.
+ void ServerProcessFuseRequest();
+
+ // Responds to FUSE request with a saved data.
+ void ServerRespondFuseSuccess(FuseMemBuffer& mem_buf,
+ const FuseMemBlock& block, uint64_t unique);
+
+ // Responds an error header to /dev/fuse when bad thing happens.
+ void ServerRespondFuseError(uint64_t unique);
int dev_fd_;
- int set_expected_[2];
- int done_[2];
-
- std::vector<char> buf_;
- std::vector<char> mem_in_;
- std::vector<char> mem_out_;
- ssize_t len_in_;
- ssize_t len_out_;
+ int sock_[2];
+
+ uint64_t nodeid_;
+ std::unordered_map<std::string, FuseMemBlock> lookup_map_;
+
+ FuseMemBuffer requests_;
+ FuseMemBuffer responses_;
+ FuseMemBuffer lookups_;
};
} // namespace testing
diff --git a/test/fuse/linux/fuse_fd_util.cc b/test/fuse/linux/fuse_fd_util.cc
new file mode 100644
index 000000000..30d1157bb
--- /dev/null
+++ b/test/fuse/linux/fuse_fd_util.cc
@@ -0,0 +1,61 @@
+// Copyright 2020 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/fuse/linux/fuse_fd_util.h"
+
+#include <fcntl.h>
+#include <linux/fuse.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/uio.h>
+
+#include <string>
+#include <vector>
+
+#include "test/util/cleanup.h"
+#include "test/util/file_descriptor.h"
+#include "test/util/fuse_util.h"
+#include "test/util/posix_error.h"
+
+namespace gvisor {
+namespace testing {
+
+PosixErrorOr<FileDescriptor> FuseFdTest::OpenPath(const std::string &path,
+ uint32_t flags, uint64_t fh) {
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_open_out),
+ };
+ struct fuse_open_out out_payload = {
+ .fh = fh,
+ .open_flags = flags,
+ };
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_OPEN, iov_out);
+
+ auto res = Open(path.c_str(), flags);
+ if (res.ok()) {
+ SkipServerActualRequest();
+ }
+ return res;
+}
+
+Cleanup FuseFdTest::CloseFD(FileDescriptor &fd) {
+ return Cleanup([&] {
+ close(fd.release());
+ SkipServerActualRequest();
+ });
+}
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/fuse/linux/fuse_fd_util.h b/test/fuse/linux/fuse_fd_util.h
new file mode 100644
index 000000000..066185c94
--- /dev/null
+++ b/test/fuse/linux/fuse_fd_util.h
@@ -0,0 +1,48 @@
+// Copyright 2020 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_FUSE_FUSE_FD_UTIL_H_
+#define GVISOR_TEST_FUSE_FUSE_FD_UTIL_H_
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <string>
+
+#include "test/fuse/linux/fuse_base.h"
+#include "test/util/cleanup.h"
+#include "test/util/file_descriptor.h"
+#include "test/util/posix_error.h"
+
+namespace gvisor {
+namespace testing {
+
+class FuseFdTest : public FuseTest {
+ public:
+ // Sets the FUSE server to respond to a FUSE_OPEN with corresponding flags and
+ // fh. Then does a real file system open on the absolute path to get an fd.
+ PosixErrorOr<FileDescriptor> OpenPath(const std::string &path,
+ uint32_t flags = O_RDONLY,
+ uint64_t fh = 1);
+
+ // Returns a cleanup object that closes the fd when it is destroyed. After
+ // the close is done, tells the FUSE server to skip this FUSE_RELEASE.
+ Cleanup CloseFD(FileDescriptor &fd);
+};
+
+} // namespace testing
+} // namespace gvisor
+
+#endif // GVISOR_TEST_FUSE_FUSE_FD_UTIL_H_
diff --git a/test/fuse/linux/mkdir_test.cc b/test/fuse/linux/mkdir_test.cc
new file mode 100644
index 000000000..9647cb93f
--- /dev/null
+++ b/test/fuse/linux/mkdir_test.cc
@@ -0,0 +1,88 @@
+// Copyright 2020 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 <linux/fuse.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "test/fuse/linux/fuse_base.h"
+#include "test/util/fuse_util.h"
+#include "test/util/temp_umask.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+class MkdirTest : public FuseTest {
+ protected:
+ const std::string test_dir_ = "test_dir";
+ const mode_t perms_ = S_IRWXU | S_IRWXG | S_IRWXO;
+};
+
+TEST_F(MkdirTest, CreateDir) {
+ const std::string test_dir_path_ =
+ JoinPath(mount_point_.path().c_str(), test_dir_);
+ const mode_t new_umask = 0077;
+
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_entry_out),
+ };
+ struct fuse_entry_out out_payload = DefaultEntryOut(S_IFDIR | perms_, 5);
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_MKDIR, iov_out);
+ TempUmask mask(new_umask);
+ ASSERT_THAT(mkdir(test_dir_path_.c_str(), 0777), SyscallSucceeds());
+
+ struct fuse_in_header in_header;
+ struct fuse_mkdir_in in_payload;
+ std::vector<char> actual_dir(test_dir_.length() + 1);
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload, actual_dir);
+ GetServerActualRequest(iov_in);
+
+ EXPECT_EQ(in_header.len,
+ sizeof(in_header) + sizeof(in_payload) + test_dir_.length() + 1);
+ EXPECT_EQ(in_header.opcode, FUSE_MKDIR);
+ EXPECT_EQ(in_payload.mode & 0777, perms_ & ~new_umask);
+ EXPECT_EQ(in_payload.umask, new_umask);
+ EXPECT_EQ(std::string(actual_dir.data()), test_dir_);
+}
+
+TEST_F(MkdirTest, FileTypeError) {
+ const std::string test_dir_path_ =
+ JoinPath(mount_point_.path().c_str(), test_dir_);
+
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_entry_out),
+ };
+ struct fuse_entry_out out_payload = DefaultEntryOut(S_IFREG | perms_, 5);
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_MKDIR, iov_out);
+ ASSERT_THAT(mkdir(test_dir_path_.c_str(), 0777), SyscallFailsWithErrno(EIO));
+ SkipServerActualRequest();
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/fuse/linux/mknod_test.cc b/test/fuse/linux/mknod_test.cc
new file mode 100644
index 000000000..74c74d76b
--- /dev/null
+++ b/test/fuse/linux/mknod_test.cc
@@ -0,0 +1,107 @@
+// Copyright 2020 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 <linux/fuse.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "test/fuse/linux/fuse_base.h"
+#include "test/util/fuse_util.h"
+#include "test/util/temp_umask.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+class MknodTest : public FuseTest {
+ protected:
+ const std::string test_file_ = "test_file";
+ const mode_t perms_ = S_IRWXU | S_IRWXG | S_IRWXO;
+};
+
+TEST_F(MknodTest, RegularFile) {
+ const std::string test_file_path =
+ JoinPath(mount_point_.path().c_str(), test_file_);
+ const mode_t new_umask = 0077;
+
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_entry_out),
+ };
+ struct fuse_entry_out out_payload = DefaultEntryOut(S_IFREG | perms_, 5);
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_MKNOD, iov_out);
+ TempUmask mask(new_umask);
+ ASSERT_THAT(mknod(test_file_path.c_str(), perms_, 0), SyscallSucceeds());
+
+ struct fuse_in_header in_header;
+ struct fuse_mknod_in in_payload;
+ std::vector<char> actual_file(test_file_.length() + 1);
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload, actual_file);
+ GetServerActualRequest(iov_in);
+
+ EXPECT_EQ(in_header.len,
+ sizeof(in_header) + sizeof(in_payload) + test_file_.length() + 1);
+ EXPECT_EQ(in_header.opcode, FUSE_MKNOD);
+ EXPECT_EQ(in_payload.mode & 0777, perms_ & ~new_umask);
+ EXPECT_EQ(in_payload.umask, new_umask);
+ EXPECT_EQ(in_payload.rdev, 0);
+ EXPECT_EQ(std::string(actual_file.data()), test_file_);
+}
+
+TEST_F(MknodTest, FileTypeError) {
+ const std::string test_file_path =
+ JoinPath(mount_point_.path().c_str(), test_file_);
+
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_entry_out),
+ };
+ // server return directory instead of regular file should cause an error.
+ struct fuse_entry_out out_payload = DefaultEntryOut(S_IFDIR | perms_, 5);
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_MKNOD, iov_out);
+ ASSERT_THAT(mknod(test_file_path.c_str(), perms_, 0),
+ SyscallFailsWithErrno(EIO));
+ SkipServerActualRequest();
+}
+
+TEST_F(MknodTest, NodeIDError) {
+ const std::string test_file_path =
+ JoinPath(mount_point_.path().c_str(), test_file_);
+
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_entry_out),
+ };
+ struct fuse_entry_out out_payload =
+ DefaultEntryOut(S_IFREG | perms_, FUSE_ROOT_ID);
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_MKNOD, iov_out);
+ ASSERT_THAT(mknod(test_file_path.c_str(), perms_, 0),
+ SyscallFailsWithErrno(EIO));
+ SkipServerActualRequest();
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/fuse/linux/open_test.cc b/test/fuse/linux/open_test.cc
new file mode 100644
index 000000000..4b0c4a805
--- /dev/null
+++ b/test/fuse/linux/open_test.cc
@@ -0,0 +1,128 @@
+// Copyright 2020 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 <linux/fuse.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <string>
+
+#include "gtest/gtest.h"
+#include "test/fuse/linux/fuse_base.h"
+#include "test/util/fuse_util.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+class OpenTest : public FuseTest {
+ // OpenTest doesn't care the release request when close a fd,
+ // so doesn't check leftover requests when tearing down.
+ void TearDown() { UnmountFuse(); }
+
+ protected:
+ const std::string test_file_ = "test_file";
+ const mode_t regular_file_ = S_IFREG | S_IRWXU | S_IRWXG | S_IRWXO;
+
+ struct fuse_open_out out_payload_ = {
+ .fh = 1,
+ .open_flags = O_RDWR,
+ };
+};
+
+TEST_F(OpenTest, RegularFile) {
+ const std::string test_file_path =
+ JoinPath(mount_point_.path().c_str(), test_file_);
+ SetServerInodeLookup(test_file_, regular_file_);
+
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_open_out),
+ };
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload_);
+ SetServerResponse(FUSE_OPEN, iov_out);
+ FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(test_file_path.c_str(), O_RDWR));
+
+ struct fuse_in_header in_header;
+ struct fuse_open_in in_payload;
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload);
+ GetServerActualRequest(iov_in);
+
+ EXPECT_EQ(in_header.len, sizeof(in_header) + sizeof(in_payload));
+ EXPECT_EQ(in_header.opcode, FUSE_OPEN);
+ EXPECT_EQ(in_payload.flags, O_RDWR);
+ EXPECT_THAT(fcntl(fd.get(), F_GETFL), SyscallSucceedsWithValue(O_RDWR));
+}
+
+TEST_F(OpenTest, SetNoOpen) {
+ const std::string test_file_path =
+ JoinPath(mount_point_.path().c_str(), test_file_);
+ SetServerInodeLookup(test_file_, regular_file_);
+
+ // ENOSYS indicates open is not implemented.
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_open_out),
+ .error = -ENOSYS,
+ };
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload_);
+ SetServerResponse(FUSE_OPEN, iov_out);
+ ASSERT_NO_ERRNO_AND_VALUE(Open(test_file_path.c_str(), O_RDWR));
+ SkipServerActualRequest();
+
+ // check open doesn't send new request.
+ uint32_t recieved_before = GetServerTotalReceivedBytes();
+ ASSERT_NO_ERRNO_AND_VALUE(Open(test_file_path.c_str(), O_RDWR));
+ EXPECT_EQ(GetServerTotalReceivedBytes(), recieved_before);
+}
+
+TEST_F(OpenTest, OpenFail) {
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_open_out),
+ .error = -ENOENT,
+ };
+
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload_);
+ SetServerResponse(FUSE_OPENDIR, iov_out);
+ ASSERT_THAT(open(mount_point_.path().c_str(), O_RDWR),
+ SyscallFailsWithErrno(ENOENT));
+
+ struct fuse_in_header in_header;
+ struct fuse_open_in in_payload;
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload);
+ GetServerActualRequest(iov_in);
+
+ EXPECT_EQ(in_header.len, sizeof(in_header) + sizeof(in_payload));
+ EXPECT_EQ(in_header.opcode, FUSE_OPENDIR);
+ EXPECT_EQ(in_payload.flags, O_RDWR);
+}
+
+TEST_F(OpenTest, DirectoryFlagOnRegularFile) {
+ const std::string test_file_path =
+ JoinPath(mount_point_.path().c_str(), test_file_);
+
+ SetServerInodeLookup(test_file_, regular_file_);
+ ASSERT_THAT(open(test_file_path.c_str(), O_RDWR | O_DIRECTORY),
+ SyscallFailsWithErrno(ENOTDIR));
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/fuse/linux/read_test.cc b/test/fuse/linux/read_test.cc
new file mode 100644
index 000000000..88fc299d8
--- /dev/null
+++ b/test/fuse/linux/read_test.cc
@@ -0,0 +1,390 @@
+// Copyright 2020 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 <linux/fuse.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "test/fuse/linux/fuse_base.h"
+#include "test/util/fuse_util.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+class ReadTest : public FuseTest {
+ void SetUp() override {
+ FuseTest::SetUp();
+ test_file_path_ = JoinPath(mount_point_.path().c_str(), test_file_);
+ }
+
+ // TearDown overrides the parent's function
+ // to skip checking the unconsumed release request at the end.
+ void TearDown() override { UnmountFuse(); }
+
+ protected:
+ const std::string test_file_ = "test_file";
+ const mode_t test_file_mode_ = S_IFREG | S_IRWXU | S_IRWXG | S_IRWXO;
+ const uint64_t test_fh_ = 1;
+ const uint32_t open_flag_ = O_RDWR;
+
+ std::string test_file_path_;
+
+ PosixErrorOr<FileDescriptor> OpenTestFile(const std::string &path,
+ uint64_t size = 512) {
+ SetServerInodeLookup(test_file_, test_file_mode_, size);
+
+ struct fuse_out_header out_header_open = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_open_out),
+ };
+ struct fuse_open_out out_payload_open = {
+ .fh = test_fh_,
+ .open_flags = open_flag_,
+ };
+ auto iov_out_open = FuseGenerateIovecs(out_header_open, out_payload_open);
+ SetServerResponse(FUSE_OPEN, iov_out_open);
+
+ auto res = Open(path.c_str(), open_flag_);
+ if (res.ok()) {
+ SkipServerActualRequest();
+ }
+ return res;
+ }
+};
+
+class ReadTestSmallMaxRead : public ReadTest {
+ void SetUp() override {
+ MountFuse(mountOpts);
+ SetUpFuseServer();
+ test_file_path_ = JoinPath(mount_point_.path().c_str(), test_file_);
+ }
+
+ protected:
+ constexpr static char mountOpts[] =
+ "rootmode=755,user_id=0,group_id=0,max_read=4096";
+ // 4096 is hard-coded as the max_read in mount options.
+ const int size_fragment = 4096;
+};
+
+TEST_F(ReadTest, ReadWhole) {
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_));
+
+ // Prepare for the read.
+ const int n_read = 5;
+ std::vector<char> data(n_read);
+ RandomizeBuffer(data.data(), data.size());
+ struct fuse_out_header out_header_read = {
+ .len =
+ static_cast<uint32_t>(sizeof(struct fuse_out_header) + data.size()),
+ };
+ auto iov_out_read = FuseGenerateIovecs(out_header_read, data);
+ SetServerResponse(FUSE_READ, iov_out_read);
+
+ // Read the whole "file".
+ std::vector<char> buf(n_read);
+ EXPECT_THAT(read(fd.get(), buf.data(), n_read),
+ SyscallSucceedsWithValue(n_read));
+
+ // Check the read request.
+ struct fuse_in_header in_header_read;
+ struct fuse_read_in in_payload_read;
+ auto iov_in = FuseGenerateIovecs(in_header_read, in_payload_read);
+ GetServerActualRequest(iov_in);
+
+ EXPECT_EQ(in_payload_read.fh, test_fh_);
+ EXPECT_EQ(in_header_read.len,
+ sizeof(in_header_read) + sizeof(in_payload_read));
+ EXPECT_EQ(in_header_read.opcode, FUSE_READ);
+ EXPECT_EQ(in_payload_read.offset, 0);
+ EXPECT_EQ(buf, data);
+}
+
+TEST_F(ReadTest, ReadPartial) {
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_));
+
+ // Prepare for the read.
+ const int n_data = 10;
+ std::vector<char> data(n_data);
+ RandomizeBuffer(data.data(), data.size());
+ // Note: due to read ahead, current read implementation will treat any
+ // response that is longer than requested as correct (i.e. not reach the EOF).
+ // Therefore, the test below should make sure the size to read does not exceed
+ // n_data.
+ struct fuse_out_header out_header_read = {
+ .len =
+ static_cast<uint32_t>(sizeof(struct fuse_out_header) + data.size()),
+ };
+ auto iov_out_read = FuseGenerateIovecs(out_header_read, data);
+ struct fuse_in_header in_header_read;
+ struct fuse_read_in in_payload_read;
+ auto iov_in = FuseGenerateIovecs(in_header_read, in_payload_read);
+
+ std::vector<char> buf(n_data);
+
+ // Read 1 bytes.
+ SetServerResponse(FUSE_READ, iov_out_read);
+ EXPECT_THAT(read(fd.get(), buf.data(), 1), SyscallSucceedsWithValue(1));
+
+ // Check the 1-byte read request.
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_payload_read.fh, test_fh_);
+ EXPECT_EQ(in_header_read.len,
+ sizeof(in_header_read) + sizeof(in_payload_read));
+ EXPECT_EQ(in_header_read.opcode, FUSE_READ);
+ EXPECT_EQ(in_payload_read.offset, 0);
+
+ // Read 3 bytes.
+ SetServerResponse(FUSE_READ, iov_out_read);
+ EXPECT_THAT(read(fd.get(), buf.data(), 3), SyscallSucceedsWithValue(3));
+
+ // Check the 3-byte read request.
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_payload_read.fh, test_fh_);
+ EXPECT_EQ(in_payload_read.offset, 1);
+
+ // Read 5 bytes.
+ SetServerResponse(FUSE_READ, iov_out_read);
+ EXPECT_THAT(read(fd.get(), buf.data(), 5), SyscallSucceedsWithValue(5));
+
+ // Check the 5-byte read request.
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_payload_read.fh, test_fh_);
+ EXPECT_EQ(in_payload_read.offset, 4);
+}
+
+TEST_F(ReadTest, PRead) {
+ const int file_size = 512;
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_, file_size));
+
+ // Prepare for the read.
+ const int n_read = 5;
+ std::vector<char> data(n_read);
+ RandomizeBuffer(data.data(), data.size());
+ struct fuse_out_header out_header_read = {
+ .len =
+ static_cast<uint32_t>(sizeof(struct fuse_out_header) + data.size()),
+ };
+ auto iov_out_read = FuseGenerateIovecs(out_header_read, data);
+ SetServerResponse(FUSE_READ, iov_out_read);
+
+ // Read some bytes.
+ std::vector<char> buf(n_read);
+ const int offset_read = file_size >> 1;
+ EXPECT_THAT(pread(fd.get(), buf.data(), n_read, offset_read),
+ SyscallSucceedsWithValue(n_read));
+
+ // Check the read request.
+ struct fuse_in_header in_header_read;
+ struct fuse_read_in in_payload_read;
+ auto iov_in = FuseGenerateIovecs(in_header_read, in_payload_read);
+ GetServerActualRequest(iov_in);
+
+ EXPECT_EQ(in_payload_read.fh, test_fh_);
+ EXPECT_EQ(in_header_read.len,
+ sizeof(in_header_read) + sizeof(in_payload_read));
+ EXPECT_EQ(in_header_read.opcode, FUSE_READ);
+ EXPECT_EQ(in_payload_read.offset, offset_read);
+ EXPECT_EQ(buf, data);
+}
+
+TEST_F(ReadTest, ReadZero) {
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_));
+
+ // Issue the read.
+ std::vector<char> buf;
+ EXPECT_THAT(read(fd.get(), buf.data(), 0), SyscallSucceedsWithValue(0));
+}
+
+TEST_F(ReadTest, ReadShort) {
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_));
+
+ // Prepare for the short read.
+ const int n_read = 5;
+ std::vector<char> data(n_read >> 1);
+ RandomizeBuffer(data.data(), data.size());
+ struct fuse_out_header out_header_read = {
+ .len =
+ static_cast<uint32_t>(sizeof(struct fuse_out_header) + data.size()),
+ };
+ auto iov_out_read = FuseGenerateIovecs(out_header_read, data);
+ SetServerResponse(FUSE_READ, iov_out_read);
+
+ // Read the whole "file".
+ std::vector<char> buf(n_read);
+ EXPECT_THAT(read(fd.get(), buf.data(), n_read),
+ SyscallSucceedsWithValue(data.size()));
+
+ // Check the read request.
+ struct fuse_in_header in_header_read;
+ struct fuse_read_in in_payload_read;
+ auto iov_in = FuseGenerateIovecs(in_header_read, in_payload_read);
+ GetServerActualRequest(iov_in);
+
+ EXPECT_EQ(in_payload_read.fh, test_fh_);
+ EXPECT_EQ(in_header_read.len,
+ sizeof(in_header_read) + sizeof(in_payload_read));
+ EXPECT_EQ(in_header_read.opcode, FUSE_READ);
+ EXPECT_EQ(in_payload_read.offset, 0);
+ std::vector<char> short_buf(buf.begin(), buf.begin() + data.size());
+ EXPECT_EQ(short_buf, data);
+}
+
+TEST_F(ReadTest, ReadShortEOF) {
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_));
+
+ // Prepare for the short read.
+ struct fuse_out_header out_header_read = {
+ .len = static_cast<uint32_t>(sizeof(struct fuse_out_header)),
+ };
+ auto iov_out_read = FuseGenerateIovecs(out_header_read);
+ SetServerResponse(FUSE_READ, iov_out_read);
+
+ // Read the whole "file".
+ const int n_read = 10;
+ std::vector<char> buf(n_read);
+ EXPECT_THAT(read(fd.get(), buf.data(), n_read), SyscallSucceedsWithValue(0));
+
+ // Check the read request.
+ struct fuse_in_header in_header_read;
+ struct fuse_read_in in_payload_read;
+ auto iov_in = FuseGenerateIovecs(in_header_read, in_payload_read);
+ GetServerActualRequest(iov_in);
+
+ EXPECT_EQ(in_payload_read.fh, test_fh_);
+ EXPECT_EQ(in_header_read.len,
+ sizeof(in_header_read) + sizeof(in_payload_read));
+ EXPECT_EQ(in_header_read.opcode, FUSE_READ);
+ EXPECT_EQ(in_payload_read.offset, 0);
+}
+
+TEST_F(ReadTestSmallMaxRead, ReadSmallMaxRead) {
+ const int n_fragment = 10;
+ const int n_read = size_fragment * n_fragment;
+
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_, n_read));
+
+ // Prepare for the read.
+ std::vector<char> data(size_fragment);
+ RandomizeBuffer(data.data(), data.size());
+ struct fuse_out_header out_header_read = {
+ .len =
+ static_cast<uint32_t>(sizeof(struct fuse_out_header) + data.size()),
+ };
+ auto iov_out_read = FuseGenerateIovecs(out_header_read, data);
+
+ for (int i = 0; i < n_fragment; ++i) {
+ SetServerResponse(FUSE_READ, iov_out_read);
+ }
+
+ // Read the whole "file".
+ std::vector<char> buf(n_read);
+ EXPECT_THAT(read(fd.get(), buf.data(), n_read),
+ SyscallSucceedsWithValue(n_read));
+
+ ASSERT_EQ(GetServerNumUnsentResponses(), 0);
+ ASSERT_EQ(GetServerNumUnconsumedRequests(), n_fragment);
+
+ // Check each read segment.
+ struct fuse_in_header in_header_read;
+ struct fuse_read_in in_payload_read;
+ auto iov_in = FuseGenerateIovecs(in_header_read, in_payload_read);
+
+ for (int i = 0; i < n_fragment; ++i) {
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_payload_read.fh, test_fh_);
+ EXPECT_EQ(in_header_read.len,
+ sizeof(in_header_read) + sizeof(in_payload_read));
+ EXPECT_EQ(in_header_read.opcode, FUSE_READ);
+ EXPECT_EQ(in_payload_read.offset, i * size_fragment);
+ EXPECT_EQ(in_payload_read.size, size_fragment);
+
+ auto it = buf.begin() + i * size_fragment;
+ EXPECT_EQ(std::vector<char>(it, it + size_fragment), data);
+ }
+}
+
+TEST_F(ReadTestSmallMaxRead, ReadSmallMaxReadShort) {
+ const int n_fragment = 10;
+ const int n_read = size_fragment * n_fragment;
+
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_, n_read));
+
+ // Prepare for the read.
+ std::vector<char> data(size_fragment);
+ RandomizeBuffer(data.data(), data.size());
+ struct fuse_out_header out_header_read = {
+ .len =
+ static_cast<uint32_t>(sizeof(struct fuse_out_header) + data.size()),
+ };
+ auto iov_out_read = FuseGenerateIovecs(out_header_read, data);
+
+ for (int i = 0; i < n_fragment - 1; ++i) {
+ SetServerResponse(FUSE_READ, iov_out_read);
+ }
+
+ // The last fragment is a short read.
+ std::vector<char> half_data(data.begin(), data.begin() + (data.size() >> 1));
+ struct fuse_out_header out_header_read_short = {
+ .len = static_cast<uint32_t>(sizeof(struct fuse_out_header) +
+ half_data.size()),
+ };
+ auto iov_out_read_short =
+ FuseGenerateIovecs(out_header_read_short, half_data);
+ SetServerResponse(FUSE_READ, iov_out_read_short);
+
+ // Read the whole "file".
+ std::vector<char> buf(n_read);
+ EXPECT_THAT(read(fd.get(), buf.data(), n_read),
+ SyscallSucceedsWithValue(n_read - (data.size() >> 1)));
+
+ ASSERT_EQ(GetServerNumUnsentResponses(), 0);
+ ASSERT_EQ(GetServerNumUnconsumedRequests(), n_fragment);
+
+ // Check each read segment.
+ struct fuse_in_header in_header_read;
+ struct fuse_read_in in_payload_read;
+ auto iov_in = FuseGenerateIovecs(in_header_read, in_payload_read);
+
+ for (int i = 0; i < n_fragment; ++i) {
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_payload_read.fh, test_fh_);
+ EXPECT_EQ(in_header_read.len,
+ sizeof(in_header_read) + sizeof(in_payload_read));
+ EXPECT_EQ(in_header_read.opcode, FUSE_READ);
+ EXPECT_EQ(in_payload_read.offset, i * size_fragment);
+ EXPECT_EQ(in_payload_read.size, size_fragment);
+
+ auto it = buf.begin() + i * size_fragment;
+ if (i != n_fragment - 1) {
+ EXPECT_EQ(std::vector<char>(it, it + data.size()), data);
+ } else {
+ EXPECT_EQ(std::vector<char>(it, it + half_data.size()), half_data);
+ }
+ }
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/fuse/linux/readdir_test.cc b/test/fuse/linux/readdir_test.cc
new file mode 100644
index 000000000..2afb4b062
--- /dev/null
+++ b/test/fuse/linux/readdir_test.cc
@@ -0,0 +1,193 @@
+// Copyright 2020 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 <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/fuse.h>
+#include <linux/unistd.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <string>
+
+#include "gtest/gtest.h"
+#include "test/fuse/linux/fuse_base.h"
+#include "test/util/fuse_util.h"
+#include "test/util/test_util.h"
+
+#define FUSE_NAME_OFFSET offsetof(struct fuse_dirent, name)
+#define FUSE_DIRENT_ALIGN(x) \
+ (((x) + sizeof(uint64_t) - 1) & ~(sizeof(uint64_t) - 1))
+#define FUSE_DIRENT_SIZE(d) FUSE_DIRENT_ALIGN(FUSE_NAME_OFFSET + (d)->namelen)
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+class ReaddirTest : public FuseTest {
+ public:
+ void fill_fuse_dirent(char *buf, const char *name, uint64_t ino) {
+ size_t namelen = strlen(name);
+ size_t entlen = FUSE_NAME_OFFSET + namelen;
+ size_t entlen_padded = FUSE_DIRENT_ALIGN(entlen);
+ struct fuse_dirent *dirent;
+
+ dirent = reinterpret_cast<struct fuse_dirent *>(buf);
+ dirent->ino = ino;
+ dirent->namelen = namelen;
+ memcpy(dirent->name, name, namelen);
+ memset(dirent->name + namelen, 0, entlen_padded - entlen);
+ }
+
+ protected:
+ const std::string test_dir_name_ = "test_dir";
+};
+
+TEST_F(ReaddirTest, SingleEntry) {
+ const std::string test_dir_path =
+ JoinPath(mount_point_.path().c_str(), test_dir_name_);
+
+ const uint64_t ino_dir = 1024;
+ // We need to make sure the test dir is a directory that can be found.
+ mode_t expected_mode =
+ S_IFDIR | S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH;
+ struct fuse_attr dir_attr = {
+ .ino = ino_dir,
+ .size = 512,
+ .blocks = 4,
+ .mode = expected_mode,
+ .blksize = 4096,
+ };
+
+ // We need to make sure the test dir is a directory that can be found.
+ struct fuse_out_header lookup_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_entry_out),
+ };
+ struct fuse_entry_out lookup_payload = {
+ .nodeid = 1,
+ .entry_valid = true,
+ .attr_valid = true,
+ .attr = dir_attr,
+ };
+
+ struct fuse_out_header open_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_open_out),
+ };
+ struct fuse_open_out open_payload = {
+ .fh = 1,
+ };
+ auto iov_out = FuseGenerateIovecs(lookup_header, lookup_payload);
+ SetServerResponse(FUSE_LOOKUP, iov_out);
+
+ iov_out = FuseGenerateIovecs(open_header, open_payload);
+ SetServerResponse(FUSE_OPENDIR, iov_out);
+
+ FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(test_dir_path.c_str(), O_RDONLY));
+
+ // The open command makes two syscalls. Lookup the dir file and open.
+ // We don't need to inspect those headers in this test.
+ SkipServerActualRequest(); // LOOKUP.
+ SkipServerActualRequest(); // OPENDIR.
+
+ // Readdir test code.
+ std::string dot = ".";
+ std::string dot_dot = "..";
+ std::string test_file = "testFile";
+
+ // Figure out how many dirents to send over and allocate them appropriately.
+ // Each dirent has a dynamic name and a static metadata part. The dirent size
+ // is aligned to being a multiple of 8.
+ size_t dot_file_dirent_size =
+ FUSE_DIRENT_ALIGN(dot.length() + FUSE_NAME_OFFSET);
+ size_t dot_dot_file_dirent_size =
+ FUSE_DIRENT_ALIGN(dot_dot.length() + FUSE_NAME_OFFSET);
+ size_t test_file_dirent_size =
+ FUSE_DIRENT_ALIGN(test_file.length() + FUSE_NAME_OFFSET);
+
+ // Create an appropriately sized payload.
+ size_t readdir_payload_size =
+ test_file_dirent_size + dot_file_dirent_size + dot_dot_file_dirent_size;
+ std::vector<char> readdir_payload_vec(readdir_payload_size);
+ char *readdir_payload = readdir_payload_vec.data();
+
+ // Use fake ino for other directories.
+ fill_fuse_dirent(readdir_payload, dot.c_str(), ino_dir - 2);
+ fill_fuse_dirent(readdir_payload + dot_file_dirent_size, dot_dot.c_str(),
+ ino_dir - 1);
+ fill_fuse_dirent(
+ readdir_payload + dot_file_dirent_size + dot_dot_file_dirent_size,
+ test_file.c_str(), ino_dir);
+
+ struct fuse_out_header readdir_header = {
+ .len = uint32_t(sizeof(struct fuse_out_header) + readdir_payload_size),
+ };
+ struct fuse_out_header readdir_header_break = {
+ .len = uint32_t(sizeof(struct fuse_out_header)),
+ };
+
+ iov_out = FuseGenerateIovecs(readdir_header, readdir_payload_vec);
+ SetServerResponse(FUSE_READDIR, iov_out);
+
+ iov_out = FuseGenerateIovecs(readdir_header_break);
+ SetServerResponse(FUSE_READDIR, iov_out);
+
+ std::vector<char> buf(4090, 0);
+ int nread, off = 0, i = 0;
+ EXPECT_THAT(
+ nread = syscall(__NR_getdents64, fd.get(), buf.data(), buf.size()),
+ SyscallSucceeds());
+ for (; off < nread;) {
+ struct dirent64 *ent = (struct dirent64 *)(buf.data() + off);
+ off += ent->d_reclen;
+ switch (i++) {
+ case 0:
+ EXPECT_EQ(std::string(ent->d_name), dot);
+ break;
+ case 1:
+ EXPECT_EQ(std::string(ent->d_name), dot_dot);
+ break;
+ case 2:
+ EXPECT_EQ(std::string(ent->d_name), test_file);
+ break;
+ }
+ }
+
+ EXPECT_THAT(
+ nread = syscall(__NR_getdents64, fd.get(), buf.data(), buf.size()),
+ SyscallSucceedsWithValue(0));
+
+ SkipServerActualRequest(); // READDIR.
+ SkipServerActualRequest(); // READDIR with no data.
+
+ // Clean up.
+ fd.reset(-1);
+
+ struct fuse_in_header in_header;
+ struct fuse_release_in in_payload;
+
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload);
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_header.len, sizeof(in_header) + sizeof(in_payload));
+ EXPECT_EQ(in_header.opcode, FUSE_RELEASEDIR);
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/fuse/linux/readlink_test.cc b/test/fuse/linux/readlink_test.cc
new file mode 100644
index 000000000..2cba8fc23
--- /dev/null
+++ b/test/fuse/linux/readlink_test.cc
@@ -0,0 +1,85 @@
+// Copyright 2020 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 <linux/fuse.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "test/fuse/linux/fuse_base.h"
+#include "test/util/fuse_util.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+class ReadlinkTest : public FuseTest {
+ protected:
+ const std::string test_file_ = "test_file_";
+ const mode_t perms_ = S_IRWXU | S_IRWXG | S_IRWXO;
+};
+
+TEST_F(ReadlinkTest, ReadSymLink) {
+ const std::string symlink_path =
+ JoinPath(mount_point_.path().c_str(), test_file_);
+ SetServerInodeLookup(test_file_, S_IFLNK | perms_);
+
+ struct fuse_out_header out_header = {
+ .len = static_cast<uint32_t>(sizeof(struct fuse_out_header)) +
+ static_cast<uint32_t>(test_file_.length()) + 1,
+ };
+ std::string link = test_file_;
+ auto iov_out = FuseGenerateIovecs(out_header, link);
+ SetServerResponse(FUSE_READLINK, iov_out);
+ const std::string actual_link =
+ ASSERT_NO_ERRNO_AND_VALUE(ReadLink(symlink_path));
+
+ struct fuse_in_header in_header;
+ auto iov_in = FuseGenerateIovecs(in_header);
+ GetServerActualRequest(iov_in);
+
+ EXPECT_EQ(in_header.len, sizeof(in_header));
+ EXPECT_EQ(in_header.opcode, FUSE_READLINK);
+ EXPECT_EQ(0, memcmp(actual_link.c_str(), link.data(), link.size()));
+
+ // next readlink should have link cached, so shouldn't have new request to
+ // server.
+ uint32_t recieved_before = GetServerTotalReceivedBytes();
+ ASSERT_NO_ERRNO(ReadLink(symlink_path));
+ EXPECT_EQ(GetServerTotalReceivedBytes(), recieved_before);
+}
+
+TEST_F(ReadlinkTest, NotSymlink) {
+ const std::string test_file_path =
+ JoinPath(mount_point_.path().c_str(), test_file_);
+ SetServerInodeLookup(test_file_, S_IFREG | perms_);
+
+ std::vector<char> buf(PATH_MAX + 1);
+ ASSERT_THAT(readlink(test_file_path.c_str(), buf.data(), PATH_MAX),
+ SyscallFailsWithErrno(EINVAL));
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/fuse/linux/release_test.cc b/test/fuse/linux/release_test.cc
new file mode 100644
index 000000000..b5adb0870
--- /dev/null
+++ b/test/fuse/linux/release_test.cc
@@ -0,0 +1,74 @@
+// Copyright 2020 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 <linux/fuse.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "test/fuse/linux/fuse_base.h"
+#include "test/util/fuse_util.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+class ReleaseTest : public FuseTest {
+ protected:
+ const std::string test_file_ = "test_file";
+};
+
+TEST_F(ReleaseTest, RegularFile) {
+ const std::string test_file_path =
+ JoinPath(mount_point_.path().c_str(), test_file_);
+ SetServerInodeLookup(test_file_, S_IFREG | S_IRWXU | S_IRWXG | S_IRWXO);
+
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_open_out),
+ };
+ struct fuse_open_out out_payload = {
+ .fh = 1,
+ .open_flags = O_RDWR,
+ };
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_OPEN, iov_out);
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(test_file_path, O_RDWR));
+ SkipServerActualRequest();
+ ASSERT_THAT(close(fd.release()), SyscallSucceeds());
+
+ struct fuse_in_header in_header;
+ struct fuse_release_in in_payload;
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload);
+ GetServerActualRequest(iov_in);
+
+ EXPECT_EQ(in_header.len, sizeof(in_header) + sizeof(in_payload));
+ EXPECT_EQ(in_header.opcode, FUSE_RELEASE);
+ EXPECT_EQ(in_payload.flags, O_RDWR);
+ EXPECT_EQ(in_payload.fh, 1);
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/fuse/linux/rmdir_test.cc b/test/fuse/linux/rmdir_test.cc
new file mode 100644
index 000000000..e3200e446
--- /dev/null
+++ b/test/fuse/linux/rmdir_test.cc
@@ -0,0 +1,100 @@
+// Copyright 2020 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 <linux/fuse.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/types.h>
+#include <sys/uio.h>
+#include <unistd.h>
+
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "test/fuse/linux/fuse_base.h"
+#include "test/util/fs_util.h"
+#include "test/util/fuse_util.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+class RmDirTest : public FuseTest {
+ protected:
+ const std::string test_dir_name_ = "test_dir";
+ const std::string test_subdir_ = "test_subdir";
+ const mode_t test_dir_mode_ = S_IFDIR | S_IRWXU | S_IRWXG | S_IRWXO;
+};
+
+TEST_F(RmDirTest, NormalRmDir) {
+ const std::string test_dir_path_ =
+ JoinPath(mount_point_.path().c_str(), test_dir_name_);
+
+ SetServerInodeLookup(test_dir_name_, test_dir_mode_);
+
+ // RmDir code.
+ struct fuse_out_header rmdir_header = {
+ .len = sizeof(struct fuse_out_header),
+ };
+
+ auto iov_out = FuseGenerateIovecs(rmdir_header);
+ SetServerResponse(FUSE_RMDIR, iov_out);
+
+ ASSERT_THAT(rmdir(test_dir_path_.c_str()), SyscallSucceeds());
+
+ struct fuse_in_header in_header;
+ std::vector<char> actual_dirname(test_dir_name_.length() + 1);
+ auto iov_in = FuseGenerateIovecs(in_header, actual_dirname);
+ GetServerActualRequest(iov_in);
+
+ EXPECT_EQ(in_header.len, sizeof(in_header) + test_dir_name_.length() + 1);
+ EXPECT_EQ(in_header.opcode, FUSE_RMDIR);
+ EXPECT_EQ(std::string(actual_dirname.data()), test_dir_name_);
+}
+
+TEST_F(RmDirTest, NormalRmDirSubdir) {
+ SetServerInodeLookup(test_subdir_, S_IFDIR | S_IRWXU | S_IRWXG | S_IRWXO);
+ const std::string test_dir_path_ =
+ JoinPath(mount_point_.path().c_str(), test_subdir_, test_dir_name_);
+ SetServerInodeLookup(test_dir_name_, test_dir_mode_);
+
+ // RmDir code.
+ struct fuse_out_header rmdir_header = {
+ .len = sizeof(struct fuse_out_header),
+ };
+
+ auto iov_out = FuseGenerateIovecs(rmdir_header);
+ SetServerResponse(FUSE_RMDIR, iov_out);
+
+ ASSERT_THAT(rmdir(test_dir_path_.c_str()), SyscallSucceeds());
+
+ struct fuse_in_header in_header;
+ std::vector<char> actual_dirname(test_dir_name_.length() + 1);
+ auto iov_in = FuseGenerateIovecs(in_header, actual_dirname);
+ GetServerActualRequest(iov_in);
+
+ EXPECT_EQ(in_header.len, sizeof(in_header) + test_dir_name_.length() + 1);
+ EXPECT_EQ(in_header.opcode, FUSE_RMDIR);
+ EXPECT_EQ(std::string(actual_dirname.data()), test_dir_name_);
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/fuse/linux/setstat_test.cc b/test/fuse/linux/setstat_test.cc
new file mode 100644
index 000000000..68301c775
--- /dev/null
+++ b/test/fuse/linux/setstat_test.cc
@@ -0,0 +1,338 @@
+// Copyright 2020 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 <linux/fuse.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/types.h>
+#include <sys/uio.h>
+#include <unistd.h>
+#include <utime.h>
+
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "test/fuse/linux/fuse_fd_util.h"
+#include "test/util/cleanup.h"
+#include "test/util/fs_util.h"
+#include "test/util/fuse_util.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+class SetStatTest : public FuseFdTest {
+ public:
+ void SetUp() override {
+ FuseFdTest::SetUp();
+ test_dir_path_ = JoinPath(mount_point_.path(), test_dir_);
+ test_file_path_ = JoinPath(mount_point_.path(), test_file_);
+ }
+
+ protected:
+ const uint64_t fh = 23;
+ const std::string test_dir_ = "testdir";
+ const std::string test_file_ = "testfile";
+ const mode_t test_dir_mode_ = S_IFDIR | S_IRUSR | S_IWUSR | S_IXUSR;
+ const mode_t test_file_mode_ = S_IFREG | S_IRUSR | S_IWUSR | S_IXUSR;
+
+ std::string test_dir_path_;
+ std::string test_file_path_;
+};
+
+TEST_F(SetStatTest, ChmodDir) {
+ // Set up fixture.
+ SetServerInodeLookup(test_dir_, test_dir_mode_);
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_attr_out),
+ .error = 0,
+ };
+ mode_t set_mode = S_IRGRP | S_IWGRP | S_IXGRP;
+ struct fuse_attr_out out_payload = {
+ .attr = DefaultFuseAttr(set_mode, 2),
+ };
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_SETATTR, iov_out);
+
+ // Make syscall.
+ EXPECT_THAT(chmod(test_dir_path_.c_str(), set_mode), SyscallSucceeds());
+
+ // Check FUSE request.
+ struct fuse_in_header in_header;
+ struct fuse_setattr_in in_payload;
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload);
+
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_header.len, sizeof(in_header) + sizeof(in_payload));
+ EXPECT_EQ(in_header.opcode, FUSE_SETATTR);
+ EXPECT_EQ(in_header.uid, 0);
+ EXPECT_EQ(in_header.gid, 0);
+ EXPECT_EQ(in_payload.valid, FATTR_MODE);
+ EXPECT_EQ(in_payload.mode, S_IFDIR | set_mode);
+}
+
+TEST_F(SetStatTest, ChownDir) {
+ // Set up fixture.
+ SetServerInodeLookup(test_dir_, test_dir_mode_);
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_attr_out),
+ .error = 0,
+ };
+ struct fuse_attr_out out_payload = {
+ .attr = DefaultFuseAttr(test_dir_mode_, 2),
+ };
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_SETATTR, iov_out);
+
+ // Make syscall.
+ EXPECT_THAT(chown(test_dir_path_.c_str(), 1025, 1025), SyscallSucceeds());
+
+ // Check FUSE request.
+ struct fuse_in_header in_header;
+ struct fuse_setattr_in in_payload;
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload);
+
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_header.len, sizeof(in_header) + sizeof(in_payload));
+ EXPECT_EQ(in_header.opcode, FUSE_SETATTR);
+ EXPECT_EQ(in_header.uid, 0);
+ EXPECT_EQ(in_header.gid, 0);
+ EXPECT_EQ(in_payload.valid, FATTR_UID | FATTR_GID);
+ EXPECT_EQ(in_payload.uid, 1025);
+ EXPECT_EQ(in_payload.gid, 1025);
+}
+
+TEST_F(SetStatTest, TruncateFile) {
+ // Set up fixture.
+ SetServerInodeLookup(test_file_, test_file_mode_);
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_attr_out),
+ .error = 0,
+ };
+ struct fuse_attr_out out_payload = {
+ .attr = DefaultFuseAttr(S_IFREG | S_IRUSR | S_IWUSR, 2),
+ };
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_SETATTR, iov_out);
+
+ // Make syscall.
+ EXPECT_THAT(truncate(test_file_path_.c_str(), 321), SyscallSucceeds());
+
+ // Check FUSE request.
+ struct fuse_in_header in_header;
+ struct fuse_setattr_in in_payload;
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload);
+
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_header.len, sizeof(in_header) + sizeof(in_payload));
+ EXPECT_EQ(in_header.opcode, FUSE_SETATTR);
+ EXPECT_EQ(in_header.uid, 0);
+ EXPECT_EQ(in_header.gid, 0);
+ EXPECT_EQ(in_payload.valid, FATTR_SIZE);
+ EXPECT_EQ(in_payload.size, 321);
+}
+
+TEST_F(SetStatTest, UtimeFile) {
+ // Set up fixture.
+ SetServerInodeLookup(test_file_, test_file_mode_);
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_attr_out),
+ .error = 0,
+ };
+ struct fuse_attr_out out_payload = {
+ .attr = DefaultFuseAttr(S_IFREG | S_IRUSR | S_IWUSR, 2),
+ };
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_SETATTR, iov_out);
+
+ // Make syscall.
+ time_t expected_atime = 1597159766, expected_mtime = 1597159765;
+ struct utimbuf times = {
+ .actime = expected_atime,
+ .modtime = expected_mtime,
+ };
+ EXPECT_THAT(utime(test_file_path_.c_str(), &times), SyscallSucceeds());
+
+ // Check FUSE request.
+ struct fuse_in_header in_header;
+ struct fuse_setattr_in in_payload;
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload);
+
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_header.len, sizeof(in_header) + sizeof(in_payload));
+ EXPECT_EQ(in_header.opcode, FUSE_SETATTR);
+ EXPECT_EQ(in_header.uid, 0);
+ EXPECT_EQ(in_header.gid, 0);
+ EXPECT_EQ(in_payload.valid, FATTR_ATIME | FATTR_MTIME);
+ EXPECT_EQ(in_payload.atime, expected_atime);
+ EXPECT_EQ(in_payload.mtime, expected_mtime);
+}
+
+TEST_F(SetStatTest, UtimesFile) {
+ // Set up fixture.
+ SetServerInodeLookup(test_file_, test_file_mode_);
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_attr_out),
+ .error = 0,
+ };
+ struct fuse_attr_out out_payload = {
+ .attr = DefaultFuseAttr(test_file_mode_, 2),
+ };
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_SETATTR, iov_out);
+
+ // Make syscall.
+ struct timeval expected_times[2] = {
+ {
+ .tv_sec = 1597159766,
+ .tv_usec = 234945,
+ },
+ {
+ .tv_sec = 1597159765,
+ .tv_usec = 232341,
+ },
+ };
+ EXPECT_THAT(utimes(test_file_path_.c_str(), expected_times),
+ SyscallSucceeds());
+
+ // Check FUSE request.
+ struct fuse_in_header in_header;
+ struct fuse_setattr_in in_payload;
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload);
+
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_header.len, sizeof(in_header) + sizeof(in_payload));
+ EXPECT_EQ(in_header.opcode, FUSE_SETATTR);
+ EXPECT_EQ(in_header.uid, 0);
+ EXPECT_EQ(in_header.gid, 0);
+ EXPECT_EQ(in_payload.valid, FATTR_ATIME | FATTR_MTIME);
+ EXPECT_EQ(in_payload.atime, expected_times[0].tv_sec);
+ EXPECT_EQ(in_payload.atimensec, expected_times[0].tv_usec * 1000);
+ EXPECT_EQ(in_payload.mtime, expected_times[1].tv_sec);
+ EXPECT_EQ(in_payload.mtimensec, expected_times[1].tv_usec * 1000);
+}
+
+TEST_F(SetStatTest, FtruncateFile) {
+ // Set up fixture.
+ SetServerInodeLookup(test_file_, test_file_mode_);
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenPath(test_file_path_, O_RDWR, fh));
+ auto close_fd = CloseFD(fd);
+
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_attr_out),
+ .error = 0,
+ };
+ struct fuse_attr_out out_payload = {
+ .attr = DefaultFuseAttr(test_file_mode_, 2),
+ };
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_SETATTR, iov_out);
+
+ // Make syscall.
+ EXPECT_THAT(ftruncate(fd.get(), 321), SyscallSucceeds());
+
+ // Check FUSE request.
+ struct fuse_in_header in_header;
+ struct fuse_setattr_in in_payload;
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload);
+
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_header.len, sizeof(in_header) + sizeof(in_payload));
+ EXPECT_EQ(in_header.opcode, FUSE_SETATTR);
+ EXPECT_EQ(in_header.uid, 0);
+ EXPECT_EQ(in_header.gid, 0);
+ EXPECT_EQ(in_payload.valid, FATTR_SIZE | FATTR_FH);
+ EXPECT_EQ(in_payload.fh, fh);
+ EXPECT_EQ(in_payload.size, 321);
+}
+
+TEST_F(SetStatTest, FchmodFile) {
+ // Set up fixture.
+ SetServerInodeLookup(test_file_, test_file_mode_);
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenPath(test_file_path_, O_RDWR, fh));
+ auto close_fd = CloseFD(fd);
+
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_attr_out),
+ .error = 0,
+ };
+ mode_t set_mode = S_IROTH | S_IWOTH | S_IXOTH;
+ struct fuse_attr_out out_payload = {
+ .attr = DefaultFuseAttr(set_mode, 2),
+ };
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_SETATTR, iov_out);
+
+ // Make syscall.
+ EXPECT_THAT(fchmod(fd.get(), set_mode), SyscallSucceeds());
+
+ // Check FUSE request.
+ struct fuse_in_header in_header;
+ struct fuse_setattr_in in_payload;
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload);
+
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_header.len, sizeof(in_header) + sizeof(in_payload));
+ EXPECT_EQ(in_header.opcode, FUSE_SETATTR);
+ EXPECT_EQ(in_header.uid, 0);
+ EXPECT_EQ(in_header.gid, 0);
+ EXPECT_EQ(in_payload.valid, FATTR_MODE | FATTR_FH);
+ EXPECT_EQ(in_payload.fh, fh);
+ EXPECT_EQ(in_payload.mode, S_IFREG | set_mode);
+}
+
+TEST_F(SetStatTest, FchownFile) {
+ // Set up fixture.
+ SetServerInodeLookup(test_file_, test_file_mode_);
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenPath(test_file_path_, O_RDWR, fh));
+ auto close_fd = CloseFD(fd);
+
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_attr_out),
+ .error = 0,
+ };
+ struct fuse_attr_out out_payload = {
+ .attr = DefaultFuseAttr(S_IFREG | S_IRUSR | S_IWUSR | S_IXUSR, 2),
+ };
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_SETATTR, iov_out);
+
+ // Make syscall.
+ EXPECT_THAT(fchown(fd.get(), 1025, 1025), SyscallSucceeds());
+
+ // Check FUSE request.
+ struct fuse_in_header in_header;
+ struct fuse_setattr_in in_payload;
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload);
+
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_header.len, sizeof(in_header) + sizeof(in_payload));
+ EXPECT_EQ(in_header.opcode, FUSE_SETATTR);
+ EXPECT_EQ(in_header.uid, 0);
+ EXPECT_EQ(in_header.gid, 0);
+ EXPECT_EQ(in_payload.valid, FATTR_UID | FATTR_GID | FATTR_FH);
+ EXPECT_EQ(in_payload.fh, fh);
+ EXPECT_EQ(in_payload.uid, 1025);
+ EXPECT_EQ(in_payload.gid, 1025);
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/fuse/linux/stat_test.cc b/test/fuse/linux/stat_test.cc
index 172e09867..6f032cac1 100644
--- a/test/fuse/linux/stat_test.cc
+++ b/test/fuse/linux/stat_test.cc
@@ -18,12 +18,16 @@
#include <sys/stat.h>
#include <sys/statfs.h>
#include <sys/types.h>
+#include <sys/uio.h>
#include <unistd.h>
#include <vector>
#include "gtest/gtest.h"
-#include "test/fuse/linux/fuse_base.h"
+#include "test/fuse/linux/fuse_fd_util.h"
+#include "test/util/cleanup.h"
+#include "test/util/fs_util.h"
+#include "test/util/fuse_util.h"
#include "test/util/test_util.h"
namespace gvisor {
@@ -31,89 +35,45 @@ namespace testing {
namespace {
-class StatTest : public FuseTest {
+class StatTest : public FuseFdTest {
public:
- bool CompareRequest(void* expected_mem, size_t expected_len, void* real_mem,
- size_t real_len) override {
- if (expected_len != real_len) return false;
- struct fuse_in_header* real_header =
- reinterpret_cast<fuse_in_header*>(real_mem);
-
- if (real_header->opcode != FUSE_GETATTR) {
- std::cerr << "expect header opcode " << FUSE_GETATTR << " but got "
- << real_header->opcode << std::endl;
- return false;
- }
- return true;
+ void SetUp() override {
+ FuseFdTest::SetUp();
+ test_file_path_ = JoinPath(mount_point_.path(), test_file_);
}
+ protected:
bool StatsAreEqual(struct stat expected, struct stat actual) {
- // device number will be dynamically allocated by kernel, we cannot know
- // in advance
+ // Device number will be dynamically allocated by kernel, we cannot know in
+ // advance.
actual.st_dev = expected.st_dev;
return memcmp(&expected, &actual, sizeof(struct stat)) == 0;
}
+
+ const std::string test_file_ = "testfile";
+ const mode_t expected_mode = S_IFREG | S_IRUSR | S_IWUSR;
+ const uint64_t fh = 23;
+
+ std::string test_file_path_;
};
TEST_F(StatTest, StatNormal) {
- struct iovec iov_in[2];
- struct iovec iov_out[2];
-
- struct fuse_in_header in_header = {
- .len = sizeof(struct fuse_in_header) + sizeof(struct fuse_getattr_in),
- .opcode = FUSE_GETATTR,
- .unique = 4,
- .nodeid = 1,
- .uid = 0,
- .gid = 0,
- .pid = 4,
- .padding = 0,
- };
- struct fuse_getattr_in in_payload = {0};
- iov_in[0].iov_len = sizeof(in_header);
- iov_in[0].iov_base = &in_header;
- iov_in[1].iov_len = sizeof(in_payload);
- iov_in[1].iov_base = &in_payload;
-
- mode_t expected_mode = S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH;
- struct timespec atime = {.tv_sec = 1595436289, .tv_nsec = 134150844};
- struct timespec mtime = {.tv_sec = 1595436290, .tv_nsec = 134150845};
- struct timespec ctime = {.tv_sec = 1595436291, .tv_nsec = 134150846};
+ // Set up fixture.
+ struct fuse_attr attr = DefaultFuseAttr(expected_mode, 1);
struct fuse_out_header out_header = {
.len = sizeof(struct fuse_out_header) + sizeof(struct fuse_attr_out),
- .error = 0,
- .unique = 4,
- };
- struct fuse_attr attr = {
- .ino = 1,
- .size = 512,
- .blocks = 4,
- .atime = static_cast<uint64_t>(atime.tv_sec),
- .mtime = static_cast<uint64_t>(mtime.tv_sec),
- .ctime = static_cast<uint64_t>(ctime.tv_sec),
- .atimensec = static_cast<uint32_t>(atime.tv_nsec),
- .mtimensec = static_cast<uint32_t>(mtime.tv_nsec),
- .ctimensec = static_cast<uint32_t>(ctime.tv_nsec),
- .mode = expected_mode,
- .nlink = 2,
- .uid = 1234,
- .gid = 4321,
- .rdev = 12,
- .blksize = 4096,
};
struct fuse_attr_out out_payload = {
.attr = attr,
};
- iov_out[0].iov_len = sizeof(out_header);
- iov_out[0].iov_base = &out_header;
- iov_out[1].iov_len = sizeof(out_payload);
- iov_out[1].iov_base = &out_payload;
-
- SetExpected(iov_in, 2, iov_out, 2);
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_GETATTR, iov_out);
+ // Make syscall.
struct stat stat_buf;
EXPECT_THAT(stat(mount_point_.path().c_str(), &stat_buf), SyscallSucceeds());
+ // Check filesystem operation result.
struct stat expected_stat = {
.st_ino = attr.ino,
.st_nlink = attr.nlink,
@@ -124,43 +84,133 @@ TEST_F(StatTest, StatNormal) {
.st_size = static_cast<off_t>(attr.size),
.st_blksize = attr.blksize,
.st_blocks = static_cast<blkcnt_t>(attr.blocks),
- .st_atim = atime,
- .st_mtim = mtime,
- .st_ctim = ctime,
+ .st_atim = (struct timespec){.tv_sec = static_cast<int>(attr.atime),
+ .tv_nsec = attr.atimensec},
+ .st_mtim = (struct timespec){.tv_sec = static_cast<int>(attr.mtime),
+ .tv_nsec = attr.mtimensec},
+ .st_ctim = (struct timespec){.tv_sec = static_cast<int>(attr.ctime),
+ .tv_nsec = attr.ctimensec},
};
EXPECT_TRUE(StatsAreEqual(stat_buf, expected_stat));
- WaitCompleted();
-}
-TEST_F(StatTest, StatNotFound) {
- struct iovec iov_in[2];
- struct iovec iov_out[2];
+ // Check FUSE request.
+ struct fuse_in_header in_header;
+ struct fuse_getattr_in in_payload;
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload);
- struct fuse_in_header in_header = {
- .len = sizeof(struct fuse_in_header) + sizeof(struct fuse_getattr_in),
- .opcode = FUSE_GETATTR,
- .unique = 4,
- };
- struct fuse_getattr_in in_payload = {0};
- iov_in[0].iov_len = sizeof(in_header);
- iov_in[0].iov_base = &in_header;
- iov_in[1].iov_len = sizeof(in_payload);
- iov_in[1].iov_base = &in_payload;
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_header.opcode, FUSE_GETATTR);
+ EXPECT_EQ(in_payload.getattr_flags, 0);
+ EXPECT_EQ(in_payload.fh, 0);
+}
+TEST_F(StatTest, StatNotFound) {
+ // Set up fixture.
struct fuse_out_header out_header = {
.len = sizeof(struct fuse_out_header),
.error = -ENOENT,
- .unique = 4,
};
- iov_out[0].iov_len = sizeof(out_header);
- iov_out[0].iov_base = &out_header;
-
- SetExpected(iov_in, 2, iov_out, 1);
+ auto iov_out = FuseGenerateIovecs(out_header);
+ SetServerResponse(FUSE_GETATTR, iov_out);
+ // Make syscall.
struct stat stat_buf;
EXPECT_THAT(stat(mount_point_.path().c_str(), &stat_buf),
SyscallFailsWithErrno(ENOENT));
- WaitCompleted();
+
+ // Check FUSE request.
+ struct fuse_in_header in_header;
+ struct fuse_getattr_in in_payload;
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload);
+
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_header.opcode, FUSE_GETATTR);
+ EXPECT_EQ(in_payload.getattr_flags, 0);
+ EXPECT_EQ(in_payload.fh, 0);
+}
+
+TEST_F(StatTest, FstatNormal) {
+ // Set up fixture.
+ SetServerInodeLookup(test_file_);
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenPath(test_file_path_, O_RDONLY, fh));
+ auto close_fd = CloseFD(fd);
+
+ struct fuse_attr attr = DefaultFuseAttr(expected_mode, 2);
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_attr_out),
+ };
+ struct fuse_attr_out out_payload = {
+ .attr = attr,
+ };
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_GETATTR, iov_out);
+
+ // Make syscall.
+ struct stat stat_buf;
+ EXPECT_THAT(fstat(fd.get(), &stat_buf), SyscallSucceeds());
+
+ // Check filesystem operation result.
+ struct stat expected_stat = {
+ .st_ino = attr.ino,
+ .st_nlink = attr.nlink,
+ .st_mode = expected_mode,
+ .st_uid = attr.uid,
+ .st_gid = attr.gid,
+ .st_rdev = attr.rdev,
+ .st_size = static_cast<off_t>(attr.size),
+ .st_blksize = attr.blksize,
+ .st_blocks = static_cast<blkcnt_t>(attr.blocks),
+ .st_atim = (struct timespec){.tv_sec = static_cast<int>(attr.atime),
+ .tv_nsec = attr.atimensec},
+ .st_mtim = (struct timespec){.tv_sec = static_cast<int>(attr.mtime),
+ .tv_nsec = attr.mtimensec},
+ .st_ctim = (struct timespec){.tv_sec = static_cast<int>(attr.ctime),
+ .tv_nsec = attr.ctimensec},
+ };
+ EXPECT_TRUE(StatsAreEqual(stat_buf, expected_stat));
+
+ // Check FUSE request.
+ struct fuse_in_header in_header;
+ struct fuse_getattr_in in_payload;
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload);
+
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_header.opcode, FUSE_GETATTR);
+ EXPECT_EQ(in_payload.getattr_flags, 0);
+ EXPECT_EQ(in_payload.fh, 0);
+}
+
+TEST_F(StatTest, StatByFileHandle) {
+ // Set up fixture.
+ SetServerInodeLookup(test_file_, expected_mode, 0);
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenPath(test_file_path_, O_RDONLY, fh));
+ auto close_fd = CloseFD(fd);
+
+ struct fuse_attr attr = DefaultFuseAttr(expected_mode, 2, 0);
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_attr_out),
+ };
+ struct fuse_attr_out out_payload = {
+ .attr = attr,
+ };
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_GETATTR, iov_out);
+
+ // Make syscall.
+ std::vector<char> buf(1);
+ // Since this is an empty file, it won't issue FUSE_READ. But a FUSE_GETATTR
+ // will be issued before read completes.
+ EXPECT_THAT(read(fd.get(), buf.data(), buf.size()), SyscallSucceeds());
+
+ // Check FUSE request.
+ struct fuse_in_header in_header;
+ struct fuse_getattr_in in_payload;
+ auto iov_in = FuseGenerateIovecs(in_header, in_payload);
+
+ GetServerActualRequest(iov_in);
+ EXPECT_EQ(in_header.opcode, FUSE_GETATTR);
+ EXPECT_EQ(in_payload.getattr_flags, FUSE_GETATTR_FH);
+ EXPECT_EQ(in_payload.fh, fh);
}
} // namespace
diff --git a/test/fuse/linux/symlink_test.cc b/test/fuse/linux/symlink_test.cc
new file mode 100644
index 000000000..2c3a52987
--- /dev/null
+++ b/test/fuse/linux/symlink_test.cc
@@ -0,0 +1,88 @@
+// Copyright 2020 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 <linux/fuse.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "test/fuse/linux/fuse_base.h"
+#include "test/util/fuse_util.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+class SymlinkTest : public FuseTest {
+ protected:
+ const std::string target_file_ = "target_file_";
+ const std::string symlink_ = "symlink_";
+ const mode_t perms_ = S_IRWXU | S_IRWXG | S_IRWXO;
+};
+
+TEST_F(SymlinkTest, CreateSymLink) {
+ const std::string symlink_path =
+ JoinPath(mount_point_.path().c_str(), symlink_);
+
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_entry_out),
+ };
+ struct fuse_entry_out out_payload = DefaultEntryOut(S_IFLNK | perms_, 5);
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_SYMLINK, iov_out);
+ ASSERT_THAT(symlink(target_file_.c_str(), symlink_path.c_str()),
+ SyscallSucceeds());
+
+ struct fuse_in_header in_header;
+ std::vector<char> actual_target_file(target_file_.length() + 1);
+ std::vector<char> actual_symlink(symlink_.length() + 1);
+ auto iov_in =
+ FuseGenerateIovecs(in_header, actual_symlink, actual_target_file);
+ GetServerActualRequest(iov_in);
+
+ EXPECT_EQ(in_header.len,
+ sizeof(in_header) + symlink_.length() + target_file_.length() + 2);
+ EXPECT_EQ(in_header.opcode, FUSE_SYMLINK);
+ EXPECT_EQ(std::string(actual_target_file.data()), target_file_);
+ EXPECT_EQ(std::string(actual_symlink.data()), symlink_);
+}
+
+TEST_F(SymlinkTest, FileTypeError) {
+ const std::string symlink_path =
+ JoinPath(mount_point_.path().c_str(), symlink_);
+
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_entry_out),
+ };
+ struct fuse_entry_out out_payload = DefaultEntryOut(S_IFREG | perms_, 5);
+ auto iov_out = FuseGenerateIovecs(out_header, out_payload);
+ SetServerResponse(FUSE_SYMLINK, iov_out);
+ ASSERT_THAT(symlink(target_file_.c_str(), symlink_path.c_str()),
+ SyscallFailsWithErrno(EIO));
+ SkipServerActualRequest();
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/fuse/linux/unlink_test.cc b/test/fuse/linux/unlink_test.cc
new file mode 100644
index 000000000..13efbf7c7
--- /dev/null
+++ b/test/fuse/linux/unlink_test.cc
@@ -0,0 +1,107 @@
+// Copyright 2020 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 <linux/fuse.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "test/fuse/linux/fuse_base.h"
+#include "test/util/fuse_util.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+class UnlinkTest : public FuseTest {
+ protected:
+ const std::string test_file_ = "test_file";
+ const std::string test_subdir_ = "test_subdir";
+};
+
+TEST_F(UnlinkTest, RegularFile) {
+ const std::string test_file_path =
+ JoinPath(mount_point_.path().c_str(), test_file_);
+ SetServerInodeLookup(test_file_, S_IFREG | S_IRWXU | S_IRWXG | S_IRWXO);
+
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header),
+ };
+ auto iov_out = FuseGenerateIovecs(out_header);
+ SetServerResponse(FUSE_UNLINK, iov_out);
+
+ ASSERT_THAT(unlink(test_file_path.c_str()), SyscallSucceeds());
+ struct fuse_in_header in_header;
+ std::vector<char> unlinked_file(test_file_.length() + 1);
+ auto iov_in = FuseGenerateIovecs(in_header, unlinked_file);
+ GetServerActualRequest(iov_in);
+
+ EXPECT_EQ(in_header.len, sizeof(in_header) + test_file_.length() + 1);
+ EXPECT_EQ(in_header.opcode, FUSE_UNLINK);
+ EXPECT_EQ(std::string(unlinked_file.data()), test_file_);
+}
+
+TEST_F(UnlinkTest, RegularFileSubDir) {
+ SetServerInodeLookup(test_subdir_, S_IFDIR | S_IRWXU | S_IRWXG | S_IRWXO);
+ const std::string test_file_path =
+ JoinPath(mount_point_.path().c_str(), test_subdir_, test_file_);
+ SetServerInodeLookup(test_file_, S_IFREG | S_IRWXU | S_IRWXG | S_IRWXO);
+
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header),
+ };
+ auto iov_out = FuseGenerateIovecs(out_header);
+ SetServerResponse(FUSE_UNLINK, iov_out);
+
+ ASSERT_THAT(unlink(test_file_path.c_str()), SyscallSucceeds());
+ struct fuse_in_header in_header;
+ std::vector<char> unlinked_file(test_file_.length() + 1);
+ auto iov_in = FuseGenerateIovecs(in_header, unlinked_file);
+ GetServerActualRequest(iov_in);
+
+ EXPECT_EQ(in_header.len, sizeof(in_header) + test_file_.length() + 1);
+ EXPECT_EQ(in_header.opcode, FUSE_UNLINK);
+ EXPECT_EQ(std::string(unlinked_file.data()), test_file_);
+}
+
+TEST_F(UnlinkTest, NoFile) {
+ const std::string test_file_path =
+ JoinPath(mount_point_.path().c_str(), test_file_);
+ SetServerInodeLookup(test_file_, S_IFREG | S_IRWXU | S_IRWXG | S_IRWXO);
+
+ struct fuse_out_header out_header = {
+ .len = sizeof(struct fuse_out_header),
+ .error = -ENOENT,
+ };
+ auto iov_out = FuseGenerateIovecs(out_header);
+ SetServerResponse(FUSE_UNLINK, iov_out);
+
+ ASSERT_THAT(unlink(test_file_path.c_str()), SyscallFailsWithErrno(ENOENT));
+ SkipServerActualRequest();
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/fuse/linux/write_test.cc b/test/fuse/linux/write_test.cc
new file mode 100644
index 000000000..1a62beb96
--- /dev/null
+++ b/test/fuse/linux/write_test.cc
@@ -0,0 +1,303 @@
+// Copyright 2020 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 <linux/fuse.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "test/fuse/linux/fuse_base.h"
+#include "test/util/fuse_util.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+class WriteTest : public FuseTest {
+ void SetUp() override {
+ FuseTest::SetUp();
+ test_file_path_ = JoinPath(mount_point_.path().c_str(), test_file_);
+ }
+
+ // TearDown overrides the parent's function
+ // to skip checking the unconsumed release request at the end.
+ void TearDown() override { UnmountFuse(); }
+
+ protected:
+ const std::string test_file_ = "test_file";
+ const mode_t test_file_mode_ = S_IFREG | S_IRWXU | S_IRWXG | S_IRWXO;
+ const uint64_t test_fh_ = 1;
+ const uint32_t open_flag_ = O_RDWR;
+
+ std::string test_file_path_;
+
+ PosixErrorOr<FileDescriptor> OpenTestFile(const std::string &path,
+ uint64_t size = 512) {
+ SetServerInodeLookup(test_file_, test_file_mode_, size);
+
+ struct fuse_out_header out_header_open = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_open_out),
+ };
+ struct fuse_open_out out_payload_open = {
+ .fh = test_fh_,
+ .open_flags = open_flag_,
+ };
+ auto iov_out_open = FuseGenerateIovecs(out_header_open, out_payload_open);
+ SetServerResponse(FUSE_OPEN, iov_out_open);
+
+ auto res = Open(path.c_str(), open_flag_);
+ if (res.ok()) {
+ SkipServerActualRequest();
+ }
+ return res;
+ }
+};
+
+class WriteTestSmallMaxWrite : public WriteTest {
+ void SetUp() override {
+ MountFuse();
+ SetUpFuseServer(&fuse_init_payload);
+ test_file_path_ = JoinPath(mount_point_.path().c_str(), test_file_);
+ }
+
+ protected:
+ const static uint32_t max_write_ = 4096;
+ constexpr static struct fuse_init_out fuse_init_payload = {
+ .major = 7,
+ .max_write = max_write_,
+ };
+
+ const uint32_t size_fragment = max_write_;
+};
+
+TEST_F(WriteTest, WriteNormal) {
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_));
+
+ // Prepare for the write.
+ const int n_write = 10;
+ struct fuse_out_header out_header_write = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_write_out),
+ };
+ struct fuse_write_out out_payload_write = {
+ .size = n_write,
+ };
+ auto iov_out_write = FuseGenerateIovecs(out_header_write, out_payload_write);
+ SetServerResponse(FUSE_WRITE, iov_out_write);
+
+ // Issue the write.
+ std::vector<char> buf(n_write);
+ RandomizeBuffer(buf.data(), buf.size());
+ EXPECT_THAT(write(fd.get(), buf.data(), n_write),
+ SyscallSucceedsWithValue(n_write));
+
+ // Check the write request.
+ struct fuse_in_header in_header_write;
+ struct fuse_write_in in_payload_write;
+ std::vector<char> payload_buf(n_write);
+ auto iov_in_write =
+ FuseGenerateIovecs(in_header_write, in_payload_write, payload_buf);
+ GetServerActualRequest(iov_in_write);
+
+ EXPECT_EQ(in_payload_write.fh, test_fh_);
+ EXPECT_EQ(in_header_write.len,
+ sizeof(in_header_write) + sizeof(in_payload_write));
+ EXPECT_EQ(in_header_write.opcode, FUSE_WRITE);
+ EXPECT_EQ(in_payload_write.offset, 0);
+ EXPECT_EQ(in_payload_write.size, n_write);
+ EXPECT_EQ(buf, payload_buf);
+}
+
+TEST_F(WriteTest, WriteShort) {
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_));
+
+ // Prepare for the write.
+ const int n_write = 10, n_written = 5;
+ struct fuse_out_header out_header_write = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_write_out),
+ };
+ struct fuse_write_out out_payload_write = {
+ .size = n_written,
+ };
+ auto iov_out_write = FuseGenerateIovecs(out_header_write, out_payload_write);
+ SetServerResponse(FUSE_WRITE, iov_out_write);
+
+ // Issue the write.
+ std::vector<char> buf(n_write);
+ RandomizeBuffer(buf.data(), buf.size());
+ EXPECT_THAT(write(fd.get(), buf.data(), n_write),
+ SyscallSucceedsWithValue(n_written));
+
+ // Check the write request.
+ struct fuse_in_header in_header_write;
+ struct fuse_write_in in_payload_write;
+ std::vector<char> payload_buf(n_write);
+ auto iov_in_write =
+ FuseGenerateIovecs(in_header_write, in_payload_write, payload_buf);
+ GetServerActualRequest(iov_in_write);
+
+ EXPECT_EQ(in_payload_write.fh, test_fh_);
+ EXPECT_EQ(in_header_write.len,
+ sizeof(in_header_write) + sizeof(in_payload_write));
+ EXPECT_EQ(in_header_write.opcode, FUSE_WRITE);
+ EXPECT_EQ(in_payload_write.offset, 0);
+ EXPECT_EQ(in_payload_write.size, n_write);
+ EXPECT_EQ(buf, payload_buf);
+}
+
+TEST_F(WriteTest, WriteShortZero) {
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_));
+
+ // Prepare for the write.
+ const int n_write = 10;
+ struct fuse_out_header out_header_write = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_write_out),
+ };
+ struct fuse_write_out out_payload_write = {
+ .size = 0,
+ };
+ auto iov_out_write = FuseGenerateIovecs(out_header_write, out_payload_write);
+ SetServerResponse(FUSE_WRITE, iov_out_write);
+
+ // Issue the write.
+ std::vector<char> buf(n_write);
+ RandomizeBuffer(buf.data(), buf.size());
+ EXPECT_THAT(write(fd.get(), buf.data(), n_write), SyscallFailsWithErrno(EIO));
+
+ // Check the write request.
+ struct fuse_in_header in_header_write;
+ struct fuse_write_in in_payload_write;
+ std::vector<char> payload_buf(n_write);
+ auto iov_in_write =
+ FuseGenerateIovecs(in_header_write, in_payload_write, payload_buf);
+ GetServerActualRequest(iov_in_write);
+
+ EXPECT_EQ(in_payload_write.fh, test_fh_);
+ EXPECT_EQ(in_header_write.len,
+ sizeof(in_header_write) + sizeof(in_payload_write));
+ EXPECT_EQ(in_header_write.opcode, FUSE_WRITE);
+ EXPECT_EQ(in_payload_write.offset, 0);
+ EXPECT_EQ(in_payload_write.size, n_write);
+ EXPECT_EQ(buf, payload_buf);
+}
+
+TEST_F(WriteTest, WriteZero) {
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_));
+
+ // Issue the write.
+ std::vector<char> buf(0);
+ EXPECT_THAT(write(fd.get(), buf.data(), 0), SyscallSucceedsWithValue(0));
+}
+
+TEST_F(WriteTest, PWrite) {
+ const int file_size = 512;
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_, file_size));
+
+ // Prepare for the write.
+ const int n_write = 10;
+ struct fuse_out_header out_header_write = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_write_out),
+ };
+ struct fuse_write_out out_payload_write = {
+ .size = n_write,
+ };
+ auto iov_out_write = FuseGenerateIovecs(out_header_write, out_payload_write);
+ SetServerResponse(FUSE_WRITE, iov_out_write);
+
+ // Issue the write.
+ std::vector<char> buf(n_write);
+ RandomizeBuffer(buf.data(), buf.size());
+ const int offset_write = file_size >> 1;
+ EXPECT_THAT(pwrite(fd.get(), buf.data(), n_write, offset_write),
+ SyscallSucceedsWithValue(n_write));
+
+ // Check the write request.
+ struct fuse_in_header in_header_write;
+ struct fuse_write_in in_payload_write;
+ std::vector<char> payload_buf(n_write);
+ auto iov_in_write =
+ FuseGenerateIovecs(in_header_write, in_payload_write, payload_buf);
+ GetServerActualRequest(iov_in_write);
+
+ EXPECT_EQ(in_payload_write.fh, test_fh_);
+ EXPECT_EQ(in_header_write.len,
+ sizeof(in_header_write) + sizeof(in_payload_write));
+ EXPECT_EQ(in_header_write.opcode, FUSE_WRITE);
+ EXPECT_EQ(in_payload_write.offset, offset_write);
+ EXPECT_EQ(in_payload_write.size, n_write);
+ EXPECT_EQ(buf, payload_buf);
+}
+
+TEST_F(WriteTestSmallMaxWrite, WriteSmallMaxWrie) {
+ const int n_fragment = 10;
+ const int n_write = size_fragment * n_fragment;
+
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(OpenTestFile(test_file_path_, n_write));
+
+ // Prepare for the write.
+ struct fuse_out_header out_header_write = {
+ .len = sizeof(struct fuse_out_header) + sizeof(struct fuse_write_out),
+ };
+ struct fuse_write_out out_payload_write = {
+ .size = size_fragment,
+ };
+ auto iov_out_write = FuseGenerateIovecs(out_header_write, out_payload_write);
+
+ for (int i = 0; i < n_fragment; ++i) {
+ SetServerResponse(FUSE_WRITE, iov_out_write);
+ }
+
+ // Issue the write.
+ std::vector<char> buf(n_write);
+ RandomizeBuffer(buf.data(), buf.size());
+ EXPECT_THAT(write(fd.get(), buf.data(), n_write),
+ SyscallSucceedsWithValue(n_write));
+
+ ASSERT_EQ(GetServerNumUnsentResponses(), 0);
+ ASSERT_EQ(GetServerNumUnconsumedRequests(), n_fragment);
+
+ // Check the write request.
+ struct fuse_in_header in_header_write;
+ struct fuse_write_in in_payload_write;
+ std::vector<char> payload_buf(size_fragment);
+ auto iov_in_write =
+ FuseGenerateIovecs(in_header_write, in_payload_write, payload_buf);
+
+ for (int i = 0; i < n_fragment; ++i) {
+ GetServerActualRequest(iov_in_write);
+
+ EXPECT_EQ(in_payload_write.fh, test_fh_);
+ EXPECT_EQ(in_header_write.len,
+ sizeof(in_header_write) + sizeof(in_payload_write));
+ EXPECT_EQ(in_header_write.opcode, FUSE_WRITE);
+ EXPECT_EQ(in_payload_write.offset, i * size_fragment);
+ EXPECT_EQ(in_payload_write.size, size_fragment);
+
+ auto it = buf.begin() + i * size_fragment;
+ EXPECT_EQ(std::vector<char>(it, it + size_fragment), payload_buf);
+ }
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/image/image_test.go b/test/image/image_test.go
index ac6186688..968e62f63 100644
--- a/test/image/image_test.go
+++ b/test/image/image_test.go
@@ -63,8 +63,8 @@ func TestHelloWorld(t *testing.T) {
}
}
-func runHTTPRequest(port int) error {
- url := fmt.Sprintf("http://localhost:%d/not-found", port)
+func runHTTPRequest(ip string, port int) error {
+ url := fmt.Sprintf("http://%s:%d/not-found", ip, port)
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("error reaching http server: %v", err)
@@ -73,7 +73,7 @@ func runHTTPRequest(port int) error {
return fmt.Errorf("Wrong response code, got: %d, want: %d", resp.StatusCode, want)
}
- url = fmt.Sprintf("http://localhost:%d/latin10k.txt", port)
+ url = fmt.Sprintf("http://%s:%d/latin10k.txt", ip, port)
resp, err = http.Get(url)
if err != nil {
return fmt.Errorf("Error reaching http server: %v", err)
@@ -95,13 +95,13 @@ func runHTTPRequest(port int) error {
return nil
}
-func testHTTPServer(t *testing.T, port int) {
+func testHTTPServer(t *testing.T, ip string, port int) {
const requests = 10
ch := make(chan error, requests)
for i := 0; i < requests; i++ {
go func() {
start := time.Now()
- err := runHTTPRequest(port)
+ err := runHTTPRequest(ip, port)
log.Printf("Response time %v: %v", time.Since(start).String(), err)
ch <- err
}()
@@ -110,7 +110,7 @@ func testHTTPServer(t *testing.T, port int) {
for i := 0; i < requests; i++ {
err := <-ch
if err != nil {
- t.Errorf("testHTTPServer(%d) failed: %v", port, err)
+ t.Errorf("testHTTPServer(%s, %d) failed: %v", ip, port, err)
}
}
}
@@ -121,27 +121,28 @@ func TestHttpd(t *testing.T) {
defer d.CleanUp(ctx)
// Start the container.
+ port := 80
opts := dockerutil.RunOpts{
Image: "basic/httpd",
- Ports: []int{80},
+ Ports: []int{port},
}
d.CopyFiles(&opts, "/usr/local/apache2/htdocs", "test/image/latin10k.txt")
if err := d.Spawn(ctx, opts); err != nil {
t.Fatalf("docker run failed: %v", err)
}
- // Find where port 80 is mapped to.
- port, err := d.FindPort(ctx, 80)
+ // Find container IP address.
+ ip, err := d.FindIP(ctx, false)
if err != nil {
- t.Fatalf("FindPort(80) failed: %v", err)
+ t.Fatalf("docker.FindIP failed: %v", err)
}
// Wait until it's up and running.
- if err := testutil.WaitForHTTP(port, defaultWait); err != nil {
+ if err := testutil.WaitForHTTP(ip.String(), port, defaultWait); err != nil {
t.Errorf("WaitForHTTP() timeout: %v", err)
}
- testHTTPServer(t, port)
+ testHTTPServer(t, ip.String(), port)
}
func TestNginx(t *testing.T) {
@@ -150,27 +151,28 @@ func TestNginx(t *testing.T) {
defer d.CleanUp(ctx)
// Start the container.
+ port := 80
opts := dockerutil.RunOpts{
Image: "basic/nginx",
- Ports: []int{80},
+ Ports: []int{port},
}
d.CopyFiles(&opts, "/usr/share/nginx/html", "test/image/latin10k.txt")
if err := d.Spawn(ctx, opts); err != nil {
t.Fatalf("docker run failed: %v", err)
}
- // Find where port 80 is mapped to.
- port, err := d.FindPort(ctx, 80)
+ // Find container IP address.
+ ip, err := d.FindIP(ctx, false)
if err != nil {
- t.Fatalf("FindPort(80) failed: %v", err)
+ t.Fatalf("docker.FindIP failed: %v", err)
}
// Wait until it's up and running.
- if err := testutil.WaitForHTTP(port, defaultWait); err != nil {
+ if err := testutil.WaitForHTTP(ip.String(), port, defaultWait); err != nil {
t.Errorf("WaitForHTTP() timeout: %v", err)
}
- testHTTPServer(t, port)
+ testHTTPServer(t, ip.String(), port)
}
func TestMysql(t *testing.T) {
@@ -218,26 +220,27 @@ func TestTomcat(t *testing.T) {
defer d.CleanUp(ctx)
// Start the server.
+ port := 8080
if err := d.Spawn(ctx, dockerutil.RunOpts{
Image: "basic/tomcat",
- Ports: []int{8080},
+ Ports: []int{port},
}); err != nil {
t.Fatalf("docker run failed: %v", err)
}
- // Find where port 8080 is mapped to.
- port, err := d.FindPort(ctx, 8080)
+ // Find container IP address.
+ ip, err := d.FindIP(ctx, false)
if err != nil {
- t.Fatalf("FindPort(8080) failed: %v", err)
+ t.Fatalf("docker.FindIP failed: %v", err)
}
// Wait until it's up and running.
- if err := testutil.WaitForHTTP(port, defaultWait); err != nil {
+ if err := testutil.WaitForHTTP(ip.String(), port, defaultWait); err != nil {
t.Fatalf("WaitForHTTP() timeout: %v", err)
}
// Ensure that content is being served.
- url := fmt.Sprintf("http://localhost:%d", port)
+ url := fmt.Sprintf("http://%s:%d", ip.String(), port)
resp, err := http.Get(url)
if err != nil {
t.Errorf("Error reaching http server: %v", err)
@@ -253,28 +256,29 @@ func TestRuby(t *testing.T) {
defer d.CleanUp(ctx)
// Execute the ruby workload.
+ port := 8080
opts := dockerutil.RunOpts{
Image: "basic/ruby",
- Ports: []int{8080},
+ Ports: []int{port},
}
d.CopyFiles(&opts, "/src", "test/image/ruby.rb", "test/image/ruby.sh")
if err := d.Spawn(ctx, opts, "/src/ruby.sh"); err != nil {
t.Fatalf("docker run failed: %v", err)
}
- // Find where port 8080 is mapped to.
- port, err := d.FindPort(ctx, 8080)
+ // Find container IP address.
+ ip, err := d.FindIP(ctx, false)
if err != nil {
- t.Fatalf("FindPort(8080) failed: %v", err)
+ t.Fatalf("docker.FindIP failed: %v", err)
}
// Wait until it's up and running, 'gem install' can take some time.
- if err := testutil.WaitForHTTP(port, time.Minute); err != nil {
+ if err := testutil.WaitForHTTP(ip.String(), port, time.Minute); err != nil {
t.Fatalf("WaitForHTTP() timeout: %v", err)
}
// Ensure that content is being served.
- url := fmt.Sprintf("http://localhost:%d", port)
+ url := fmt.Sprintf("http://%s:%d", ip.String(), port)
resp, err := http.Get(url)
if err != nil {
t.Errorf("error reaching http server: %v", err)
diff --git a/test/iptables/README.md b/test/iptables/README.md
index b9f44bd40..28ab195ca 100644
--- a/test/iptables/README.md
+++ b/test/iptables/README.md
@@ -1,6 +1,6 @@
# iptables Tests
-iptables tests are run via `scripts/iptables_test.sh`.
+iptables tests are run via `make iptables-tests`.
iptables requires raw socket support, so you must add the `--net-raw=true` flag
to `/etc/docker/daemon.json` in order to use it.
diff --git a/test/iptables/iptables_test.go b/test/iptables/iptables_test.go
index e2beb30d5..834f7615f 100644
--- a/test/iptables/iptables_test.go
+++ b/test/iptables/iptables_test.go
@@ -72,11 +72,6 @@ func iptablesTest(t *testing.T, test TestCase, ipv6 bool) {
d.CleanUp(context.Background())
}()
- // TODO(gvisor.dev/issue/170): Skipping IPv6 gVisor tests.
- if ipv6 && dockerutil.Runtime() != "runc" {
- t.Skip("gVisor ip6tables not yet implemented")
- }
-
// Create and start the container.
opts := dockerutil.RunOpts{
Image: "iptables",
@@ -314,11 +309,11 @@ func TestInputInvertDestination(t *testing.T) {
singleTest(t, FilterInputInvertDestination{})
}
-func TestOutputDestination(t *testing.T) {
+func TestFilterOutputDestination(t *testing.T) {
singleTest(t, FilterOutputDestination{})
}
-func TestOutputInvertDestination(t *testing.T) {
+func TestFilterOutputInvertDestination(t *testing.T) {
singleTest(t, FilterOutputInvertDestination{})
}
diff --git a/test/packetimpact/runner/dut.go b/test/packetimpact/runner/dut.go
index d4c486f9c..59bb68eb1 100644
--- a/test/packetimpact/runner/dut.go
+++ b/test/packetimpact/runner/dut.go
@@ -69,8 +69,8 @@ func RegisterFlags(fs *flag.FlagSet) {
fs.BoolVar(&native, "native", false, "whether the test should be run natively")
fs.StringVar(&testbenchBinary, "testbench_binary", "", "path to the testbench binary")
fs.BoolVar(&tshark, "tshark", false, "use more verbose tshark in logs instead of tcpdump")
- flag.Var(&extraTestArgs, "extra_test_arg", "extra arguments to pass to the testbench")
- flag.BoolVar(&expectFailure, "expect_failure", false, "expect that the test will fail when run")
+ fs.Var(&extraTestArgs, "extra_test_arg", "extra arguments to pass to the testbench")
+ fs.BoolVar(&expectFailure, "expect_failure", false, "expect that the test will fail when run")
}
// CtrlPort is the port that posix_server listens on.
@@ -172,7 +172,7 @@ func TestWithDUT(ctx context.Context, t *testing.T, mkDevice func(*dockerutil.Co
}
device := mkDevice(dut)
- remoteIPv6, remoteMAC, dutDeviceID, testNetDev := device.Prepare(ctx, t, runOpts, ctrlNet, testNet, containerAddr)
+ remoteIPv6, remoteMAC, dutDeviceID, dutTestNetDev := device.Prepare(ctx, t, runOpts, ctrlNet, testNet, containerAddr)
// Create the Docker container for the testbench.
testbench := dockerutil.MakeNativeContainer(ctx, logger("testbench"))
@@ -181,12 +181,16 @@ func TestWithDUT(ctx context.Context, t *testing.T, mkDevice func(*dockerutil.Co
containerTestbenchBinary := filepath.Join("/packetimpact", tbb)
testbench.CopyFiles(&runOpts, "/packetimpact", filepath.Join("test/packetimpact/tests", tbb))
+ // snifferNetDev is a network device on the test orchestrator that we will
+ // run sniffer (tcpdump or tshark) on and inject traffic to, not to be
+ // confused with the device on the DUT.
+ const snifferNetDev = "eth2"
// Run tcpdump in the test bench unbuffered, without DNS resolution, just on
// the interface with the test packets.
snifferArgs := []string{
"tcpdump",
"-S", "-vvv", "-U", "-n",
- "-i", testNetDev,
+ "-i", snifferNetDev,
"-w", testOutputDir + "/dump.pcap",
}
snifferRegex := "tcpdump: listening.*\n"
@@ -194,7 +198,7 @@ func TestWithDUT(ctx context.Context, t *testing.T, mkDevice func(*dockerutil.Co
// Run tshark in the test bench unbuffered, without DNS resolution, just on
// the interface with the test packets.
snifferArgs = []string{
- "tshark", "-V", "-l", "-n", "-i", testNetDev,
+ "tshark", "-V", "-l", "-n", "-i", snifferNetDev,
"-o", "tcp.check_checksum:TRUE",
"-o", "udp.check_checksum:TRUE",
}
@@ -228,7 +232,7 @@ func TestWithDUT(ctx context.Context, t *testing.T, mkDevice func(*dockerutil.Co
// this, we can install the following iptables rules. The raw socket that
// packetimpact tests use will still be able to see everything.
for _, bin := range []string{"iptables", "ip6tables"} {
- if logs, err := testbench.Exec(ctx, dockerutil.ExecOpts{}, bin, "-A", "INPUT", "-i", testNetDev, "-p", "tcp", "-j", "DROP"); err != nil {
+ if logs, err := testbench.Exec(ctx, dockerutil.ExecOpts{}, bin, "-A", "INPUT", "-i", snifferNetDev, "-p", "tcp", "-j", "DROP"); err != nil {
t.Fatalf("unable to Exec %s on container %s: %s, logs from testbench:\n%s", bin, testbench.Name, err, logs)
}
}
@@ -251,7 +255,8 @@ func TestWithDUT(ctx context.Context, t *testing.T, mkDevice func(*dockerutil.Co
"--remote_ipv6", remoteIPv6.String(),
"--remote_mac", remoteMAC.String(),
"--remote_interface_id", fmt.Sprintf("%d", dutDeviceID),
- "--device", testNetDev,
+ "--local_device", snifferNetDev,
+ "--remote_device", dutTestNetDev,
fmt.Sprintf("--native=%t", native),
)
testbenchLogs, err := testbench.Exec(ctx, dockerutil.ExecOpts{}, testArgs...)
diff --git a/test/packetimpact/testbench/dut.go b/test/packetimpact/testbench/dut.go
index ff269d949..6165ab293 100644
--- a/test/packetimpact/testbench/dut.go
+++ b/test/packetimpact/testbench/dut.go
@@ -16,11 +16,13 @@ package testbench
import (
"context"
+ "encoding/binary"
"flag"
"net"
"strconv"
"syscall"
"testing"
+ "time"
pb "gvisor.dev/gvisor/test/packetimpact/proto/posix_server_go_proto"
@@ -701,6 +703,20 @@ func (dut *DUT) RecvWithErrno(ctx context.Context, t *testing.T, sockfd, len, fl
return resp.GetRet(), resp.GetBuf(), syscall.Errno(resp.GetErrno_())
}
+// SetSockLingerOption sets SO_LINGER socket option on the DUT.
+func (dut *DUT) SetSockLingerOption(t *testing.T, sockfd int32, timeout time.Duration, enable bool) {
+ var linger unix.Linger
+ if enable {
+ linger.Onoff = 1
+ }
+ linger.Linger = int32(timeout / time.Second)
+
+ buf := make([]byte, 8)
+ binary.LittleEndian.PutUint32(buf, uint32(linger.Onoff))
+ binary.LittleEndian.PutUint32(buf[4:], uint32(linger.Linger))
+ dut.SetSockOpt(t, sockfd, unix.SOL_SOCKET, unix.SO_LINGER, buf)
+}
+
// Shutdown calls shutdown on the DUT and causes a fatal test failure if it doesn't
// succeed. If more control over the timeout or error handling is needed, use
// ShutdownWithErrno.
diff --git a/test/packetimpact/testbench/rawsockets.go b/test/packetimpact/testbench/rawsockets.go
index 57e822725..193bb2dc8 100644
--- a/test/packetimpact/testbench/rawsockets.go
+++ b/test/packetimpact/testbench/rawsockets.go
@@ -139,7 +139,7 @@ type Injector struct {
func NewInjector(t *testing.T) (Injector, error) {
t.Helper()
- ifInfo, err := net.InterfaceByName(Device)
+ ifInfo, err := net.InterfaceByName(LocalDevice)
if err != nil {
return Injector{}, err
}
diff --git a/test/packetimpact/testbench/testbench.go b/test/packetimpact/testbench/testbench.go
index e3629e1f3..0073a1361 100644
--- a/test/packetimpact/testbench/testbench.go
+++ b/test/packetimpact/testbench/testbench.go
@@ -29,8 +29,11 @@ import (
var (
// Native indicates that the test is being run natively.
Native = false
- // Device is the local device on the test network.
- Device = ""
+ // LocalDevice is the device that testbench uses to inject traffic.
+ LocalDevice = ""
+ // RemoteDevice is the device name on the DUT, individual tests can
+ // use the name to construct tests.
+ RemoteDevice = ""
// LocalIPv4 is the local IPv4 address on the test network.
LocalIPv4 = ""
@@ -80,7 +83,8 @@ func RegisterFlags(fs *flag.FlagSet) {
fs.StringVar(&RemoteIPv4, "remote_ipv4", RemoteIPv4, "remote IPv4 address for test packets")
fs.StringVar(&RemoteIPv6, "remote_ipv6", RemoteIPv6, "remote IPv6 address for test packets")
fs.StringVar(&RemoteMAC, "remote_mac", RemoteMAC, "remote mac address for test packets")
- fs.StringVar(&Device, "device", Device, "local device for test packets")
+ fs.StringVar(&LocalDevice, "local_device", LocalDevice, "local device to inject traffic")
+ fs.StringVar(&RemoteDevice, "remote_device", RemoteDevice, "remote device on the DUT")
fs.BoolVar(&Native, "native", Native, "whether the test is running natively")
fs.Uint64Var(&RemoteInterfaceID, "remote_interface_id", RemoteInterfaceID, "remote interface ID for test packets")
}
diff --git a/test/packetimpact/tests/BUILD b/test/packetimpact/tests/BUILD
index 6dda05102..94731c64b 100644
--- a/test/packetimpact/tests/BUILD
+++ b/test/packetimpact/tests/BUILD
@@ -272,6 +272,16 @@ packetimpact_go_test(
)
packetimpact_go_test(
+ name = "tcp_queue_send_in_syn_sent",
+ srcs = ["tcp_queue_send_in_syn_sent_test.go"],
+ deps = [
+ "//pkg/tcpip/header",
+ "//test/packetimpact/testbench",
+ "@org_golang_x_sys//unix:go_default_library",
+ ],
+)
+
+packetimpact_go_test(
name = "icmpv6_param_problem",
srcs = ["icmpv6_param_problem_test.go"],
# TODO(b/153485026): Fix netstack then remove the line below.
@@ -320,3 +330,23 @@ packetimpact_go_test(
"@org_golang_x_sys//unix:go_default_library",
],
)
+
+packetimpact_go_test(
+ name = "tcp_linger",
+ srcs = ["tcp_linger_test.go"],
+ deps = [
+ "//pkg/tcpip/header",
+ "//test/packetimpact/testbench",
+ "@org_golang_x_sys//unix:go_default_library",
+ ],
+)
+
+packetimpact_go_test(
+ name = "tcp_rcv_buf_space",
+ srcs = ["tcp_rcv_buf_space_test.go"],
+ deps = [
+ "//pkg/tcpip/header",
+ "//test/packetimpact/testbench",
+ "@org_golang_x_sys//unix:go_default_library",
+ ],
+)
diff --git a/test/packetimpact/tests/tcp_linger_test.go b/test/packetimpact/tests/tcp_linger_test.go
new file mode 100644
index 000000000..b9a0409aa
--- /dev/null
+++ b/test/packetimpact/tests/tcp_linger_test.go
@@ -0,0 +1,270 @@
+// Copyright 2020 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 tcp_linger_test
+
+import (
+ "context"
+ "flag"
+ "syscall"
+ "testing"
+ "time"
+
+ "golang.org/x/sys/unix"
+ "gvisor.dev/gvisor/pkg/tcpip/header"
+ "gvisor.dev/gvisor/test/packetimpact/testbench"
+)
+
+func init() {
+ testbench.RegisterFlags(flag.CommandLine)
+}
+
+func createSocket(t *testing.T, dut testbench.DUT) (int32, int32, testbench.TCPIPv4) {
+ listenFD, remotePort := dut.CreateListener(t, unix.SOCK_STREAM, unix.IPPROTO_TCP, 1)
+ conn := testbench.NewTCPIPv4(t, testbench.TCP{DstPort: &remotePort}, testbench.TCP{SrcPort: &remotePort})
+ conn.Connect(t)
+ acceptFD, _ := dut.Accept(t, listenFD)
+ return acceptFD, listenFD, conn
+}
+
+func closeAll(t *testing.T, dut testbench.DUT, listenFD int32, conn testbench.TCPIPv4) {
+ conn.Close(t)
+ dut.Close(t, listenFD)
+ dut.TearDown()
+}
+
+// lingerDuration is the timeout value used with SO_LINGER socket option.
+const lingerDuration = 3 * time.Second
+
+// TestTCPLingerZeroTimeout tests when SO_LINGER is set with zero timeout. DUT
+// should send RST-ACK when socket is closed.
+func TestTCPLingerZeroTimeout(t *testing.T) {
+ // Create a socket, listen, TCP connect, and accept.
+ dut := testbench.NewDUT(t)
+ acceptFD, listenFD, conn := createSocket(t, dut)
+ defer closeAll(t, dut, listenFD, conn)
+
+ dut.SetSockLingerOption(t, acceptFD, 0, true)
+ dut.Close(t, acceptFD)
+
+ // If the linger timeout is set to zero, the DUT should send a RST.
+ if _, err := conn.Expect(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagRst | header.TCPFlagAck)}, time.Second); err != nil {
+ t.Errorf("expected RST-ACK packet within a second but got none: %s", err)
+ }
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck)})
+}
+
+// TestTCPLingerOff tests when SO_LINGER is not set. DUT should send FIN-ACK
+// when socket is closed.
+func TestTCPLingerOff(t *testing.T) {
+ // Create a socket, listen, TCP connect, and accept.
+ dut := testbench.NewDUT(t)
+ acceptFD, listenFD, conn := createSocket(t, dut)
+ defer closeAll(t, dut, listenFD, conn)
+
+ dut.Close(t, acceptFD)
+
+ // If SO_LINGER is not set, DUT should send a FIN-ACK.
+ if _, err := conn.Expect(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagFin | header.TCPFlagAck)}, time.Second); err != nil {
+ t.Errorf("expected FIN-ACK packet within a second but got none: %s", err)
+ }
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck)})
+}
+
+// TestTCPLingerNonZeroTimeout tests when SO_LINGER is set with non-zero timeout.
+// DUT should close the socket after timeout.
+func TestTCPLingerNonZeroTimeout(t *testing.T) {
+ for _, tt := range []struct {
+ description string
+ lingerOn bool
+ }{
+ {"WithNonZeroLinger", true},
+ {"WithoutLinger", false},
+ } {
+ t.Run(tt.description, func(t *testing.T) {
+ // Create a socket, listen, TCP connect, and accept.
+ dut := testbench.NewDUT(t)
+ acceptFD, listenFD, conn := createSocket(t, dut)
+ defer closeAll(t, dut, listenFD, conn)
+
+ dut.SetSockLingerOption(t, acceptFD, lingerDuration, tt.lingerOn)
+
+ // Increase timeout as Close will take longer time to
+ // return when SO_LINGER is set with non-zero timeout.
+ timeout := lingerDuration + 1*time.Second
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
+ defer cancel()
+ start := time.Now()
+ dut.CloseWithErrno(ctx, t, acceptFD)
+ end := time.Now()
+ diff := end.Sub(start)
+
+ if tt.lingerOn && diff < lingerDuration {
+ t.Errorf("expected close to return after %v seconds, but returned sooner", lingerDuration)
+ } else if !tt.lingerOn && diff > 1*time.Second {
+ t.Errorf("expected close to return within a second, but returned later")
+ }
+
+ if _, err := conn.Expect(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagFin | header.TCPFlagAck)}, time.Second); err != nil {
+ t.Errorf("expected FIN-ACK packet within a second but got none: %s", err)
+ }
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck)})
+ })
+ }
+}
+
+// TestTCPLingerSendNonZeroTimeout tests when SO_LINGER is set with non-zero
+// timeout and send a packet. DUT should close the socket after timeout.
+func TestTCPLingerSendNonZeroTimeout(t *testing.T) {
+ for _, tt := range []struct {
+ description string
+ lingerOn bool
+ }{
+ {"WithSendNonZeroLinger", true},
+ {"WithoutLinger", false},
+ } {
+ t.Run(tt.description, func(t *testing.T) {
+ // Create a socket, listen, TCP connect, and accept.
+ dut := testbench.NewDUT(t)
+ acceptFD, listenFD, conn := createSocket(t, dut)
+ defer closeAll(t, dut, listenFD, conn)
+
+ dut.SetSockLingerOption(t, acceptFD, lingerDuration, tt.lingerOn)
+
+ // Send data.
+ sampleData := []byte("Sample Data")
+ dut.Send(t, acceptFD, sampleData, 0)
+
+ // Increase timeout as Close will take longer time to
+ // return when SO_LINGER is set with non-zero timeout.
+ timeout := lingerDuration + 1*time.Second
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
+ defer cancel()
+ start := time.Now()
+ dut.CloseWithErrno(ctx, t, acceptFD)
+ end := time.Now()
+ diff := end.Sub(start)
+
+ if tt.lingerOn && diff < lingerDuration {
+ t.Errorf("expected close to return after %v seconds, but returned sooner", lingerDuration)
+ } else if !tt.lingerOn && diff > 1*time.Second {
+ t.Errorf("expected close to return within a second, but returned later")
+ }
+
+ samplePayload := &testbench.Payload{Bytes: sampleData}
+ if _, err := conn.ExpectData(t, &testbench.TCP{}, samplePayload, time.Second); err != nil {
+ t.Fatalf("expected a packet with payload %v: %s", samplePayload, err)
+ }
+
+ if _, err := conn.Expect(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagFin | header.TCPFlagAck)}, time.Second); err != nil {
+ t.Errorf("expected FIN-ACK packet within a second but got none: %s", err)
+ }
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck)})
+ })
+ }
+}
+
+// TestTCPLingerShutdownZeroTimeout tests SO_LINGER with shutdown() and zero
+// timeout. DUT should send RST-ACK when socket is closed.
+func TestTCPLingerShutdownZeroTimeout(t *testing.T) {
+ // Create a socket, listen, TCP connect, and accept.
+ dut := testbench.NewDUT(t)
+ acceptFD, listenFD, conn := createSocket(t, dut)
+ defer closeAll(t, dut, listenFD, conn)
+
+ dut.SetSockLingerOption(t, acceptFD, 0, true)
+ dut.Shutdown(t, acceptFD, syscall.SHUT_RDWR)
+ dut.Close(t, acceptFD)
+
+ // Shutdown will send FIN-ACK with read/write option.
+ if _, err := conn.Expect(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagFin | header.TCPFlagAck)}, time.Second); err != nil {
+ t.Errorf("expected FIN-ACK packet within a second but got none: %s", err)
+ }
+
+ // If the linger timeout is set to zero, the DUT should send a RST.
+ if _, err := conn.Expect(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagRst | header.TCPFlagAck)}, time.Second); err != nil {
+ t.Errorf("expected RST-ACK packet within a second but got none: %s", err)
+ }
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck)})
+}
+
+// TestTCPLingerShutdownSendNonZeroTimeout tests SO_LINGER with shutdown() and
+// non-zero timeout. DUT should close the socket after timeout.
+func TestTCPLingerShutdownSendNonZeroTimeout(t *testing.T) {
+ for _, tt := range []struct {
+ description string
+ lingerOn bool
+ }{
+ {"shutdownRDWR", true},
+ {"shutdownRDWR", false},
+ } {
+ t.Run(tt.description, func(t *testing.T) {
+ // Create a socket, listen, TCP connect, and accept.
+ dut := testbench.NewDUT(t)
+ acceptFD, listenFD, conn := createSocket(t, dut)
+ defer closeAll(t, dut, listenFD, conn)
+
+ dut.SetSockLingerOption(t, acceptFD, lingerDuration, tt.lingerOn)
+
+ // Send data.
+ sampleData := []byte("Sample Data")
+ dut.Send(t, acceptFD, sampleData, 0)
+
+ dut.Shutdown(t, acceptFD, syscall.SHUT_RDWR)
+
+ // Increase timeout as Close will take longer time to
+ // return when SO_LINGER is set with non-zero timeout.
+ timeout := lingerDuration + 1*time.Second
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
+ defer cancel()
+ start := time.Now()
+ dut.CloseWithErrno(ctx, t, acceptFD)
+ end := time.Now()
+ diff := end.Sub(start)
+
+ if tt.lingerOn && diff < lingerDuration {
+ t.Errorf("expected close to return after %v seconds, but returned sooner", lingerDuration)
+ } else if !tt.lingerOn && diff > 1*time.Second {
+ t.Errorf("expected close to return within a second, but returned later")
+ }
+
+ samplePayload := &testbench.Payload{Bytes: sampleData}
+ if _, err := conn.ExpectData(t, &testbench.TCP{}, samplePayload, time.Second); err != nil {
+ t.Fatalf("expected a packet with payload %v: %s", samplePayload, err)
+ }
+
+ if _, err := conn.Expect(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagFin | header.TCPFlagAck)}, time.Second); err != nil {
+ t.Errorf("expected FIN-ACK packet within a second but got none: %s", err)
+ }
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck)})
+ })
+ }
+}
+
+func TestTCPLingerNonEstablished(t *testing.T) {
+ dut := testbench.NewDUT(t)
+ newFD := dut.Socket(t, unix.AF_INET, unix.SOCK_STREAM, unix.IPPROTO_TCP)
+ dut.SetSockLingerOption(t, newFD, lingerDuration, true)
+
+ // As the socket is in the initial state, Close() should not linger
+ // and return immediately.
+ start := time.Now()
+ dut.CloseWithErrno(context.Background(), t, newFD)
+ diff := time.Since(start)
+
+ if diff > lingerDuration {
+ t.Errorf("expected close to return within %s, but returned after %s", lingerDuration, diff)
+ }
+ dut.TearDown()
+}
diff --git a/test/packetimpact/tests/tcp_queue_send_in_syn_sent_test.go b/test/packetimpact/tests/tcp_queue_send_in_syn_sent_test.go
new file mode 100644
index 000000000..0ec8fd748
--- /dev/null
+++ b/test/packetimpact/tests/tcp_queue_send_in_syn_sent_test.go
@@ -0,0 +1,133 @@
+// Copyright 2020 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 tcp_queue_send_in_syn_sent_test
+
+import (
+ "context"
+ "errors"
+ "flag"
+ "net"
+ "sync"
+ "syscall"
+ "testing"
+ "time"
+
+ "golang.org/x/sys/unix"
+ "gvisor.dev/gvisor/pkg/tcpip/header"
+ "gvisor.dev/gvisor/test/packetimpact/testbench"
+)
+
+func init() {
+ testbench.RegisterFlags(flag.CommandLine)
+}
+
+// TestQueueSendInSynSent tests send behavior when the TCP state
+// is SYN-SENT.
+// It tests for 2 variants when in SYN_SENT state and:
+// (1) DUT blocks on send and complete handshake
+// (2) DUT blocks on send and receive a TCP RST.
+func TestQueueSendInSynSent(t *testing.T) {
+ for _, tt := range []struct {
+ description string
+ reset bool
+ }{
+ {description: "Complete handshake", reset: false},
+ {description: "Send RST", reset: true},
+ } {
+ t.Run(tt.description, func(t *testing.T) {
+ dut := testbench.NewDUT(t)
+ defer dut.TearDown()
+
+ socket, remotePort := dut.CreateBoundSocket(t, unix.SOCK_STREAM, unix.IPPROTO_TCP, net.ParseIP(testbench.RemoteIPv4))
+ conn := testbench.NewTCPIPv4(t, testbench.TCP{DstPort: &remotePort}, testbench.TCP{SrcPort: &remotePort})
+ defer conn.Close(t)
+
+ sampleData := []byte("Sample Data")
+ samplePayload := &testbench.Payload{Bytes: sampleData}
+ dut.SetNonBlocking(t, socket, true)
+ if _, err := dut.ConnectWithErrno(context.Background(), t, socket, conn.LocalAddr(t)); !errors.Is(err, syscall.EINPROGRESS) {
+ t.Fatalf("failed to bring DUT to SYN-SENT, got: %s, want EINPROGRESS", err)
+ }
+ if _, err := conn.Expect(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagSyn)}, time.Second); err != nil {
+ t.Fatalf("expected a SYN from DUT, but got none: %s", err)
+ }
+ if _, err := dut.SendWithErrno(context.Background(), t, socket, sampleData, 0); err != syscall.Errno(unix.EWOULDBLOCK) {
+ t.Fatalf("expected error %s, got %s", syscall.Errno(unix.EWOULDBLOCK), err)
+ }
+
+ // Test blocking write.
+ dut.SetNonBlocking(t, socket, false)
+
+ var wg sync.WaitGroup
+ defer wg.Wait()
+ wg.Add(1)
+ var block sync.WaitGroup
+ block.Add(1)
+ go func() {
+ defer wg.Done()
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
+ defer cancel()
+
+ block.Done()
+ // Issue SEND call in SYN-SENT, this should be queued for
+ // process until the connection is established.
+ n, err := dut.SendWithErrno(ctx, t, socket, sampleData, 0)
+ if tt.reset {
+ if err != syscall.Errno(unix.ECONNREFUSED) {
+ t.Errorf("expected error %s, got %s", syscall.Errno(unix.ECONNREFUSED), err)
+ }
+ if n != -1 {
+ t.Errorf("expected return value %d, got %d", -1, n)
+ }
+ return
+ }
+ if n != int32(len(sampleData)) {
+ t.Errorf("failed to send on DUT: %s", err)
+ }
+ }()
+
+ // Wait for the goroutine to be scheduled and before it
+ // blocks on endpoint send.
+ block.Wait()
+ // The following sleep is used to prevent the connection
+ // from being established before we are blocked on send.
+ time.Sleep(100 * time.Millisecond)
+
+ if tt.reset {
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagRst | header.TCPFlagAck)})
+ return
+ }
+
+ // Bring the connection to Established.
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagSyn | header.TCPFlagAck)})
+
+ // Expect the data from the DUT's enqueued send request.
+ //
+ // On Linux, this can be piggybacked with the ACK completing the
+ // handshake. On gVisor, getting such a piggyback is a bit more
+ // complicated because the actual data enqueuing occurs in the
+ // callers of endpoint Write.
+ if _, err := conn.ExpectData(t, &testbench.TCP{Flags: testbench.Uint8(header.TCPFlagPsh | header.TCPFlagAck)}, samplePayload, time.Second); err != nil {
+ t.Fatalf("expected payload was not received: %s", err)
+ }
+
+ // Send sample payload and expect an ACK to ensure connection is still ESTABLISHED.
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagPsh | header.TCPFlagAck)}, &testbench.Payload{Bytes: sampleData})
+ if _, err := conn.Expect(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck)}, time.Second); err != nil {
+ t.Fatalf("expected an ACK from DUT, but got none: %s", err)
+ }
+ })
+ }
+}
diff --git a/test/packetimpact/tests/tcp_rcv_buf_space_test.go b/test/packetimpact/tests/tcp_rcv_buf_space_test.go
new file mode 100644
index 000000000..cfbba1e8e
--- /dev/null
+++ b/test/packetimpact/tests/tcp_rcv_buf_space_test.go
@@ -0,0 +1,80 @@
+// Copyright 2020 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 tcp_rcv_buf_space_test
+
+import (
+ "context"
+ "flag"
+ "syscall"
+ "testing"
+
+ "golang.org/x/sys/unix"
+ "gvisor.dev/gvisor/pkg/tcpip/header"
+ "gvisor.dev/gvisor/test/packetimpact/testbench"
+)
+
+func init() {
+ testbench.RegisterFlags(flag.CommandLine)
+}
+
+// TestReduceRecvBuf tests that a packet within window is still dropped
+// if the available buffer space drops below the size of the incoming
+// segment.
+func TestReduceRecvBuf(t *testing.T) {
+ dut := testbench.NewDUT(t)
+ defer dut.TearDown()
+ listenFd, remotePort := dut.CreateListener(t, unix.SOCK_STREAM, unix.IPPROTO_TCP, 1)
+ defer dut.Close(t, listenFd)
+ conn := testbench.NewTCPIPv4(t, testbench.TCP{DstPort: &remotePort}, testbench.TCP{SrcPort: &remotePort})
+ defer conn.Close(t)
+
+ conn.Connect(t)
+ acceptFd, _ := dut.Accept(t, listenFd)
+ defer dut.Close(t, acceptFd)
+
+ // Set a small receive buffer for the test.
+ const rcvBufSz = 4096
+ dut.SetSockOptInt(t, acceptFd, unix.SOL_SOCKET, unix.SO_RCVBUF, rcvBufSz)
+
+ // Retrieve the actual buffer.
+ bufSz := dut.GetSockOptInt(t, acceptFd, unix.SOL_SOCKET, unix.SO_RCVBUF)
+
+ // Generate a payload of 1 more than the actual buffer size used by the
+ // DUT.
+ sampleData := testbench.GenerateRandomPayload(t, int(bufSz)+1)
+ // Send and receive sample data to the dut.
+ const pktSize = 1400
+ for payload := sampleData; len(payload) != 0; {
+ payloadBytes := pktSize
+ if l := len(payload); l < payloadBytes {
+ payloadBytes = l
+ }
+
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck)}, []testbench.Layer{&testbench.Payload{Bytes: payload[:payloadBytes]}}...)
+ payload = payload[payloadBytes:]
+ }
+
+ // First read should read < len(sampleData)
+ if ret, _, err := dut.RecvWithErrno(context.Background(), t, acceptFd, int32(len(sampleData)), 0); ret == -1 || int(ret) == len(sampleData) {
+ t.Fatalf("dut.RecvWithErrno(ctx, t, %d, %d, 0) = %d,_, %s", acceptFd, int32(len(sampleData)), ret, err)
+ }
+
+ // Second read should return EAGAIN as the last segment should have been
+ // dropped due to it exceeding the receive buffer space available in the
+ // socket.
+ if ret, got, err := dut.RecvWithErrno(context.Background(), t, acceptFd, int32(len(sampleData)), syscall.MSG_DONTWAIT); got != nil || ret != -1 || err != syscall.EAGAIN {
+ t.Fatalf("expected no packets but got: %s", got)
+ }
+}
diff --git a/test/root/root.go b/test/root/root.go
index 0f1d29faf..441fa5e2e 100644
--- a/test/root/root.go
+++ b/test/root/root.go
@@ -17,5 +17,5 @@
// docker, containerd, and crictl installed. To run these tests from the
// project root directory:
//
-// ./scripts/root_tests.sh
+// make root-tests
package root
diff --git a/test/runner/defs.bzl b/test/runner/defs.bzl
index 032ebd04e..248053dc3 100644
--- a/test/runner/defs.bzl
+++ b/test/runner/defs.bzl
@@ -203,7 +203,6 @@ def syscall_test(
tags = platform_tags + tags,
)
- # TODO(gvisor.dev/issue/1487): Enable VFS2 overlay tests.
if add_overlay:
_syscall_test(
test = test,
@@ -216,6 +215,23 @@ def syscall_test(
overlay = True,
)
+ # TODO(gvisor.dev/issue/4407): Remove tags to enable VFS2 overlay tests.
+ overlay_vfs2_tags = list(vfs2_tags)
+ overlay_vfs2_tags.append("manual")
+ overlay_vfs2_tags.append("noguitar")
+ overlay_vfs2_tags.append("notap")
+ _syscall_test(
+ test = test,
+ shard_count = shard_count,
+ size = size,
+ platform = default_platform,
+ use_tmpfs = use_tmpfs,
+ add_uds_tree = add_uds_tree,
+ tags = platforms[default_platform] + overlay_vfs2_tags,
+ overlay = True,
+ vfs2 = True,
+ )
+
if add_hostinet:
_syscall_test(
test = test,
diff --git a/test/runtimes/BUILD b/test/runtimes/BUILD
index 066338ee3..22b526f59 100644
--- a/test/runtimes/BUILD
+++ b/test/runtimes/BUILD
@@ -5,7 +5,7 @@ package(licenses = ["notice"])
runtime_test(
name = "go1.12",
- exclude_file = "exclude_go1.12.csv",
+ exclude_file = "exclude/go1.12.csv",
lang = "go",
shard_count = 8,
)
@@ -13,28 +13,28 @@ runtime_test(
runtime_test(
name = "java11",
batch = 100,
- exclude_file = "exclude_java11.csv",
+ exclude_file = "exclude/java11.csv",
lang = "java",
shard_count = 16,
)
runtime_test(
name = "nodejs12.4.0",
- exclude_file = "exclude_nodejs12.4.0.csv",
+ exclude_file = "exclude/nodejs12.4.0.csv",
lang = "nodejs",
shard_count = 8,
)
runtime_test(
name = "php7.3.6",
- exclude_file = "exclude_php7.3.6.csv",
+ exclude_file = "exclude/php7.3.6.csv",
lang = "php",
shard_count = 8,
)
runtime_test(
name = "python3.7.3",
- exclude_file = "exclude_python3.7.3.csv",
+ exclude_file = "exclude/python3.7.3.csv",
lang = "python",
shard_count = 8,
)
diff --git a/test/runtimes/README.md b/test/runtimes/README.md
new file mode 100644
index 000000000..9dda1a728
--- /dev/null
+++ b/test/runtimes/README.md
@@ -0,0 +1,62 @@
+# gVisor Runtime Tests
+
+App Engine uses gvisor to sandbox application containers. The runtime tests aim
+to test `runsc` compatibility with these
+[standard runtimes](https://cloud.google.com/appengine/docs/standard/runtimes).
+The test itself runs the language-defined tests inside the sandboxed standard
+runtime container.
+
+Note: [Ruby runtime](https://cloud.google.com/appengine/docs/standard/ruby) is
+currently in beta mode and so we do not run tests for it yet.
+
+### Testing Locally
+
+To run runtime tests individually from a given runtime, use the following table.
+
+Language | Version | Download Image | Run Test(s)
+-------- | ------- | ------------------------------------------- | -----------
+Go | 1.12 | `make -C images load-runtimes_go1.12` | If the test name ends with `.go`, it is an on-disk test: <br> `docker run --runtime=runsc -it gvisor.dev/images/runtimes/go1.12 ( cd /usr/local/go/test ; go run run.go -v -- <TEST_NAME>... )` <br> Otherwise it is a tool test: <br> `docker run --runtime=runsc -it gvisor.dev/images/runtimes/go1.12 go tool dist test -v -no-rebuild ^TEST1$\|^TEST2$...`
+Java | 11 | `make -C images load-runtimes_java11` | `docker run --runtime=runsc -it gvisor.dev/images/runtimes/java11 jtreg -agentvm -dir:/root/test/jdk -noreport -timeoutFactor:20 -verbose:summary <TEST_NAME>...`
+NodeJS | 12.4.0 | `make -C images load-runtimes_nodejs12.4.0` | `docker run --runtime=runsc -it gvisor.dev/images/runtimes/nodejs12.4.0 python tools/test.py --timeout=180 <TEST_NAME>...`
+Php | 7.3.6 | `make -C images load-runtimes_php7.3.6` | `docker run --runtime=runsc -it gvisor.dev/images/runtimes/php7.3.6 make test "TESTS=<TEST_NAME>..."`
+Python | 3.7.3 | `make -C images load-runtimes_python3.7.3` | `docker run --runtime=runsc -it gvisor.dev/images/runtimes/python3.7.3 ./python -m test <TEST_NAME>...`
+
+To run an entire runtime test locally, use the following table.
+
+Note: java runtime test take 1+ hours with 16 cores.
+
+Language | Version | Running the test suite
+-------- | ------- | ----------------------------------------
+Go | 1.12 | `make go1.12-runtime-tests{_vfs2}`
+Java | 11 | `make java11-runtime-tests{_vfs2}`
+NodeJS | 12.4.0 | `make nodejs12.4.0-runtime-tests{_vfs2}`
+Php | 7.3.6 | `make php7.3.6-runtime-tests{_vfs2}`
+Python | 3.7.3 | `make python3.7.3-runtime-tests{_vfs2}`
+
+#### Clean Up
+
+Sometimes when runtime tests fail or when the testing container itself crashes
+unexpectedly, the containers are not removed or sometimes do not even exit. This
+can cause some docker commands like `docker system prune` to hang forever.
+
+Here are some helpful commands (should be executed in order):
+
+```bash
+docker ps -a # Lists all docker processes; useful when investigating hanging containers.
+docker kill $(docker ps -a -q) # Kills all running containers.
+docker rm $(docker ps -a -q) # Removes all exited containers.
+docker system prune # Remove unused data.
+```
+
+### Testing Infrastructure
+
+There are 3 components to this tests infrastructure:
+
+- [`runner`](runner) - This is the test entrypoint. This is the binary is
+ invoked by `bazel test`. The runner spawns the target runtime container
+ using `runsc` and then copies over the `proctor` binary into the container.
+- [`proctor`](proctor) - This binary acts as our agent inside the container
+ which communicates with the runner and actually executes tests.
+- [`exclude`](exclude) - Holds a CSV file for each language runtime containing
+ the full path of tests that should be excluded from running along with a
+ reason for exclusion.
diff --git a/test/runtimes/exclude_go1.12.csv b/test/runtimes/exclude/go1.12.csv
index 81e02cf64..81e02cf64 100644
--- a/test/runtimes/exclude_go1.12.csv
+++ b/test/runtimes/exclude/go1.12.csv
diff --git a/test/runtimes/exclude_java11.csv b/test/runtimes/exclude/java11.csv
index 997a29cad..f779df8d5 100644
--- a/test/runtimes/exclude_java11.csv
+++ b/test/runtimes/exclude/java11.csv
@@ -1,9 +1,11 @@
test name,bug id,comment
com/sun/crypto/provider/Cipher/PBE/PKCS12Cipher.java,,Fails in Docker
+com/sun/jdi/InvokeHangTest.java,https://bugs.openjdk.java.net/browse/JDK-8218463,
com/sun/jdi/NashornPopFrameTest.java,,
com/sun/jdi/ProcessAttachTest.java,,
com/sun/management/HotSpotDiagnosticMXBean/CheckOrigin.java,,Fails in Docker
com/sun/management/OperatingSystemMXBean/GetCommittedVirtualMemorySize.java,,
+com/sun/management/ThreadMXBean/ThreadCpuTimeArray.java,,Test assumes high CPU clock precision
com/sun/management/UnixOperatingSystemMXBean/GetMaxFileDescriptorCount.sh,,
com/sun/tools/attach/AttachSelf.java,,
com/sun/tools/attach/BasicTests.java,,
diff --git a/test/runtimes/exclude_nodejs12.4.0.csv b/test/runtimes/exclude/nodejs12.4.0.csv
index 1740dbb76..ba993814f 100644
--- a/test/runtimes/exclude_nodejs12.4.0.csv
+++ b/test/runtimes/exclude/nodejs12.4.0.csv
@@ -1,4 +1,5 @@
test name,bug id,comment
+async-hooks/test-statwatcher.js,https://github.com/nodejs/node/issues/21425,Check for fix inclusion in nodejs releases after 2020-03-29
benchmark/test-benchmark-fs.js,,
benchmark/test-benchmark-napi.js,,
doctool/test-make-doc.js,b/68848110,Expected to fail.
@@ -12,7 +13,7 @@ parallel/test-dns-channel-timeout.js,b/161893056,
parallel/test-fs-access.js,,
parallel/test-fs-watchfile.js,,Flaky - File already exists error
parallel/test-fs-write-stream.js,b/166819807,Flaky
-parallel/test-fs-write-stream-double-close,b/166819807,Flaky
+parallel/test-fs-write-stream-double-close.js,b/166819807,Flaky
parallel/test-fs-write-stream-throw-type-error.js,b/166819807,Flaky
parallel/test-http-writable-true-after-close.js,,Flaky - Mismatched <anonymous> function calls. Expected exactly 1 actual 2
parallel/test-os.js,b/63997097,
diff --git a/test/runtimes/exclude_php7.3.6.csv b/test/runtimes/exclude/php7.3.6.csv
index 815a137c5..a73f3bcfb 100644
--- a/test/runtimes/exclude_php7.3.6.csv
+++ b/test/runtimes/exclude/php7.3.6.csv
@@ -32,6 +32,7 @@ ext/standard/tests/file/symlink_link_linkinfo_is_link_variation4.phpt,b/16289534
ext/standard/tests/file/symlink_link_linkinfo_is_link_variation8.phpt,b/162896223,
ext/standard/tests/general_functions/escapeshellarg_bug71270.phpt,,
ext/standard/tests/general_functions/escapeshellcmd_bug71270.phpt,,
+ext/standard/tests/streams/proc_open_bug60120.phpt,,Flaky until php-src 3852a35fdbcb
ext/standard/tests/streams/proc_open_bug69900.phpt,,Flaky
ext/standard/tests/streams/stream_socket_sendto.phpt,,
ext/standard/tests/strings/007.phpt,,
diff --git a/test/runtimes/exclude_python3.7.3.csv b/test/runtimes/exclude/python3.7.3.csv
index 8760f8951..8760f8951 100644
--- a/test/runtimes/exclude_python3.7.3.csv
+++ b/test/runtimes/exclude/python3.7.3.csv
diff --git a/test/runtimes/proctor/BUILD b/test/runtimes/proctor/BUILD
index d1935cbe8..fdc6d3173 100644
--- a/test/runtimes/proctor/BUILD
+++ b/test/runtimes/proctor/BUILD
@@ -1,29 +1,11 @@
-load("//tools:defs.bzl", "go_binary", "go_test")
+load("//tools:defs.bzl", "go_binary")
package(licenses = ["notice"])
go_binary(
name = "proctor",
- srcs = [
- "go.go",
- "java.go",
- "nodejs.go",
- "php.go",
- "proctor.go",
- "python.go",
- ],
+ srcs = ["main.go"],
pure = True,
visibility = ["//test/runtimes:__pkg__"],
-)
-
-go_test(
- name = "proctor_test",
- size = "small",
- srcs = ["proctor_test.go"],
- library = ":proctor",
- nogo = False, # FIXME(gvisor.dev/issue/3374): Not working with all build systems.
- pure = True,
- deps = [
- "//pkg/test/testutil",
- ],
+ deps = ["//test/runtimes/proctor/lib"],
)
diff --git a/test/runtimes/proctor/lib/BUILD b/test/runtimes/proctor/lib/BUILD
new file mode 100644
index 000000000..0c8367dfe
--- /dev/null
+++ b/test/runtimes/proctor/lib/BUILD
@@ -0,0 +1,24 @@
+load("//tools:defs.bzl", "go_library", "go_test")
+
+package(licenses = ["notice"])
+
+go_library(
+ name = "lib",
+ srcs = [
+ "go.go",
+ "java.go",
+ "lib.go",
+ "nodejs.go",
+ "php.go",
+ "python.go",
+ ],
+ visibility = ["//test/runtimes/proctor:__pkg__"],
+)
+
+go_test(
+ name = "lib_test",
+ size = "small",
+ srcs = ["lib_test.go"],
+ library = ":lib",
+ deps = ["//pkg/test/testutil"],
+)
diff --git a/test/runtimes/proctor/go.go b/test/runtimes/proctor/lib/go.go
index d0ae844e6..5c48fb60b 100644
--- a/test/runtimes/proctor/go.go
+++ b/test/runtimes/proctor/lib/go.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package main
+package lib
import (
"fmt"
@@ -59,7 +59,7 @@ func (goRunner) ListTests() ([]string, error) {
}
// Go tests on disk.
- diskSlice, err := search(goTestDir, goTestRegEx)
+ diskSlice, err := Search(goTestDir, goTestRegEx)
if err != nil {
return nil, err
}
diff --git a/test/runtimes/proctor/java.go b/test/runtimes/proctor/lib/java.go
index d456fa681..3105011ff 100644
--- a/test/runtimes/proctor/java.go
+++ b/test/runtimes/proctor/lib/java.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package main
+package lib
import (
"fmt"
diff --git a/test/runtimes/proctor/proctor.go b/test/runtimes/proctor/lib/lib.go
index 9e0642424..f2ba82498 100644
--- a/test/runtimes/proctor/proctor.go
+++ b/test/runtimes/proctor/lib/lib.go
@@ -12,20 +12,16 @@
// 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
+// Package lib contains proctor functions.
+package lib
import (
- "flag"
"fmt"
- "log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
- "strings"
"syscall"
)
@@ -42,66 +38,8 @@ type TestRunner interface {
TestCmds(tests []string) []*exec.Cmd
}
-var (
- runtime = flag.String("runtime", "", "name of runtime")
- list = flag.Bool("list", false, "list all available tests")
- testNames = flag.String("tests", "", "run a subset of the 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
- }
-
- var tests []string
- if *testNames == "" {
- // Run every test.
- tests, err = tr.ListTests()
- if err != nil {
- log.Fatalf("failed to get all tests: %v", err)
- }
- } else {
- // Run subset of test.
- tests = strings.Split(*testNames, ",")
- }
-
- // Run tests.
- cmds := tr.TestCmds(tests)
- for _, cmd := range cmds {
- 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) {
+// TestRunnerForRuntime returns a new TestRunner for the given runtime.
+func TestRunnerForRuntime(runtime string) (TestRunner, error) {
switch runtime {
case "go":
return goRunner{}, nil
@@ -117,8 +55,8 @@ func testRunnerForRuntime(runtime string) (TestRunner, error) {
return nil, fmt.Errorf("invalid runtime %q", runtime)
}
-// pauseAndReap is like init. It runs forever and reaps any children.
-func pauseAndReap() {
+// 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)
@@ -138,9 +76,9 @@ func pauseAndReap() {
}
}
-// search is a helper function to find tests in the given directory that match
+// 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) {
+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 {
diff --git a/test/runtimes/proctor/proctor_test.go b/test/runtimes/proctor/lib/lib_test.go
index 6ef2de085..1193d2e28 100644
--- a/test/runtimes/proctor/proctor_test.go
+++ b/test/runtimes/proctor/lib/lib_test.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package main
+package lib
import (
"io/ioutil"
@@ -47,7 +47,7 @@ func TestSearchEmptyDir(t *testing.T) {
var want []string
testFilter := regexp.MustCompile(`^test-[^-].+\.tc$`)
- got, err := search(td, testFilter)
+ got, err := Search(td, testFilter)
if err != nil {
t.Errorf("search error: %v", err)
}
@@ -116,7 +116,7 @@ func TestSearch(t *testing.T) {
}
testFilter := regexp.MustCompile(`^test-[^-].+\.tc$`)
- got, err := search(td, testFilter)
+ got, err := Search(td, testFilter)
if err != nil {
t.Errorf("search error: %v", err)
}
diff --git a/test/runtimes/proctor/nodejs.go b/test/runtimes/proctor/lib/nodejs.go
index dead5af4f..320597aa5 100644
--- a/test/runtimes/proctor/nodejs.go
+++ b/test/runtimes/proctor/lib/nodejs.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package main
+package lib
import (
"os/exec"
@@ -32,7 +32,7 @@ var _ TestRunner = nodejsRunner{}
// ListTests implements TestRunner.ListTests.
func (nodejsRunner) ListTests() ([]string, error) {
- testSlice, err := search(nodejsTestDir, nodejsTestRegEx)
+ testSlice, err := Search(nodejsTestDir, nodejsTestRegEx)
if err != nil {
return nil, err
}
diff --git a/test/runtimes/proctor/php.go b/test/runtimes/proctor/lib/php.go
index 6a83d64e3..b67a60a97 100644
--- a/test/runtimes/proctor/php.go
+++ b/test/runtimes/proctor/lib/php.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package main
+package lib
import (
"os/exec"
@@ -29,7 +29,7 @@ var _ TestRunner = phpRunner{}
// ListTests implements TestRunner.ListTests.
func (phpRunner) ListTests() ([]string, error) {
- testSlice, err := search(".", phpTestRegEx)
+ testSlice, err := Search(".", phpTestRegEx)
if err != nil {
return nil, err
}
diff --git a/test/runtimes/proctor/python.go b/test/runtimes/proctor/lib/python.go
index 7c598801b..429bfd850 100644
--- a/test/runtimes/proctor/python.go
+++ b/test/runtimes/proctor/lib/python.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package main
+package lib
import (
"fmt"
diff --git a/test/runtimes/proctor/main.go b/test/runtimes/proctor/main.go
new file mode 100644
index 000000000..e5607ac92
--- /dev/null
+++ b/test/runtimes/proctor/main.go
@@ -0,0 +1,85 @@
+// 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"
+ "strings"
+
+ "gvisor.dev/gvisor/test/runtimes/proctor/lib"
+)
+
+var (
+ runtime = flag.String("runtime", "", "name of runtime")
+ list = flag.Bool("list", false, "list all available tests")
+ testNames = flag.String("tests", "", "run a subset of the available tests")
+ pause = flag.Bool("pause", false, "cause container to pause indefinitely, reaping any zombie children")
+)
+
+func main() {
+ flag.Parse()
+
+ if *pause {
+ lib.PauseAndReap()
+ panic("pauseAndReap should never return")
+ }
+
+ if *runtime == "" {
+ log.Fatalf("runtime flag must be provided")
+ }
+
+ tr, err := lib.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
+ }
+
+ var tests []string
+ if *testNames == "" {
+ // Run every test.
+ tests, err = tr.ListTests()
+ if err != nil {
+ log.Fatalf("failed to get all tests: %v", err)
+ }
+ } else {
+ // Run subset of test.
+ tests = strings.Split(*testNames, ",")
+ }
+
+ // Run tests.
+ cmds := tr.TestCmds(tests)
+ for _, cmd := range cmds {
+ cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
+ if err := cmd.Run(); err != nil {
+ log.Fatalf("FAIL: %v", err)
+ }
+ }
+}
diff --git a/test/runtimes/runner/BUILD b/test/runtimes/runner/BUILD
index dc0d5d5b4..70cc01594 100644
--- a/test/runtimes/runner/BUILD
+++ b/test/runtimes/runner/BUILD
@@ -1,4 +1,4 @@
-load("//tools:defs.bzl", "go_binary", "go_test")
+load("//tools:defs.bzl", "go_binary")
package(licenses = ["notice"])
@@ -7,16 +7,5 @@ go_binary(
testonly = 1,
srcs = ["main.go"],
visibility = ["//test/runtimes:__pkg__"],
- deps = [
- "//pkg/log",
- "//pkg/test/dockerutil",
- "//pkg/test/testutil",
- ],
-)
-
-go_test(
- name = "exclude_test",
- size = "small",
- srcs = ["exclude_test.go"],
- library = ":runner",
+ deps = ["//test/runtimes/runner/lib"],
)
diff --git a/test/runtimes/runner/lib/BUILD b/test/runtimes/runner/lib/BUILD
new file mode 100644
index 000000000..d308f41b0
--- /dev/null
+++ b/test/runtimes/runner/lib/BUILD
@@ -0,0 +1,22 @@
+load("//tools:defs.bzl", "go_library", "go_test")
+
+package(licenses = ["notice"])
+
+go_library(
+ name = "lib",
+ testonly = 1,
+ srcs = ["lib.go"],
+ visibility = ["//test/runtimes/runner:__pkg__"],
+ deps = [
+ "//pkg/log",
+ "//pkg/test/dockerutil",
+ "//pkg/test/testutil",
+ ],
+)
+
+go_test(
+ name = "lib_test",
+ size = "small",
+ srcs = ["exclude_test.go"],
+ library = ":lib",
+)
diff --git a/test/runtimes/runner/exclude_test.go b/test/runtimes/runner/lib/exclude_test.go
index 67c2170c8..f996e895b 100644
--- a/test/runtimes/runner/exclude_test.go
+++ b/test/runtimes/runner/lib/exclude_test.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package main
+package lib
import (
"flag"
@@ -20,6 +20,8 @@ import (
"testing"
)
+var excludeFile = flag.String("exclude_file", "", "file to test (standard format)")
+
func TestMain(m *testing.M) {
flag.Parse()
os.Exit(m.Run())
@@ -27,7 +29,7 @@ func TestMain(m *testing.M) {
// Test that the exclude file parses without error.
func TestExcludelist(t *testing.T) {
- ex, err := getExcludes()
+ ex, err := getExcludes(*excludeFile)
if err != nil {
t.Fatalf("error parsing exclude file: %v", err)
}
diff --git a/test/runtimes/runner/lib/lib.go b/test/runtimes/runner/lib/lib.go
new file mode 100644
index 000000000..78285cb0e
--- /dev/null
+++ b/test/runtimes/runner/lib/lib.go
@@ -0,0 +1,185 @@
+// 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 lib provides utilities for runner.
+package lib
+
+import (
+ "context"
+ "encoding/csv"
+ "fmt"
+ "io"
+ "os"
+ "sort"
+ "strings"
+ "testing"
+ "time"
+
+ "gvisor.dev/gvisor/pkg/log"
+ "gvisor.dev/gvisor/pkg/test/dockerutil"
+ "gvisor.dev/gvisor/pkg/test/testutil"
+)
+
+// 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(lang, image, excludeFile string, batchSize int, timeout time.Duration) int {
+ // Get tests to exclude..
+ excludes, err := getExcludes(excludeFile)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error getting exclude list: %s\n", err.Error())
+ return 1
+ }
+
+ // Construct the shared docker instance.
+ ctx := context.Background()
+ d := dockerutil.MakeContainer(ctx, testutil.DefaultLogger(lang))
+ defer d.CleanUp(ctx)
+
+ if err := testutil.TouchShardStatusFile(); err != nil {
+ fmt.Fprintf(os.Stderr, "error touching status shard file: %v\n", err)
+ return 1
+ }
+
+ // 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(ctx, d, lang, image, batchSize, timeout, excludes)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%s\n", err.Error())
+ return 1
+ }
+
+ m := testing.MainStart(testDeps{}, tests, nil, nil)
+ return m.Run()
+}
+
+// getTests executes all tests as table tests.
+func getTests(ctx context.Context, d *dockerutil.Container, lang, image string, batchSize int, timeout time.Duration, excludes map[string]struct{}) ([]testing.InternalTest, error) {
+ // Start the container.
+ opts := dockerutil.RunOpts{
+ Image: fmt.Sprintf("runtimes/%s", image),
+ }
+ d.CopyFiles(&opts, "/proctor", "test/runtimes/proctor/proctor")
+ if err := d.Spawn(ctx, opts, "/proctor/proctor", "--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(ctx, dockerutil.ExecOpts{}, "/proctor/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)
+ indices, err := testutil.TestIndicesForShard(len(tests))
+ if err != nil {
+ return nil, fmt.Errorf("TestsForShard() failed: %v", err)
+ }
+
+ var itests []testing.InternalTest
+ for i := 0; i < len(indices); i += batchSize {
+ var tcs []string
+ end := i + batchSize
+ if end > len(indices) {
+ end = len(indices)
+ }
+ for _, tc := range indices[i:end] {
+ // Add test if not excluded.
+ if _, ok := excludes[tests[tc]]; ok {
+ log.Infof("Skipping test case %s\n", tests[tc])
+ continue
+ }
+ tcs = append(tcs, tests[tc])
+ }
+ itests = append(itests, testing.InternalTest{
+ Name: strings.Join(tcs, ", "),
+ F: func(t *testing.T) {
+ var (
+ now = time.Now()
+ done = make(chan struct{})
+ output string
+ err error
+ )
+
+ go func() {
+ fmt.Printf("RUNNING the following in a batch\n%s\n", strings.Join(tcs, "\n"))
+ output, err = d.Exec(ctx, dockerutil.ExecOpts{}, "/proctor/proctor", "--runtime", lang, "--tests", strings.Join(tcs, ","))
+ close(done)
+ }()
+
+ select {
+ case <-done:
+ if err == nil {
+ fmt.Printf("PASS: (%v)\n\n", time.Since(now))
+ return
+ }
+ t.Errorf("FAIL: (%v):\n%s\n", time.Since(now), output)
+ case <-time.After(timeout):
+ t.Errorf("TIMEOUT: (%v):\n%s\n", time.Since(now), output)
+ }
+ },
+ })
+ }
+
+ return itests, nil
+}
+
+// getBlacklist reads the exclude file and returns a set of test names to
+// exclude.
+func getExcludes(excludeFile string) (map[string]struct{}, error) {
+ excludes := make(map[string]struct{})
+ if excludeFile == "" {
+ return excludes, nil
+ }
+ f, err := os.Open(excludeFile)
+ 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
+ }
+ excludes[record[0]] = struct{}{}
+ }
+ return excludes, 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/main.go b/test/runtimes/runner/main.go
index 948e7cf9c..ec79a22c2 100644
--- a/test/runtimes/runner/main.go
+++ b/test/runtimes/runner/main.go
@@ -16,20 +16,12 @@
package main
import (
- "context"
- "encoding/csv"
"flag"
"fmt"
- "io"
"os"
- "sort"
- "strings"
- "testing"
"time"
- "gvisor.dev/gvisor/pkg/log"
- "gvisor.dev/gvisor/pkg/test/dockerutil"
- "gvisor.dev/gvisor/pkg/test/testutil"
+ "gvisor.dev/gvisor/test/runtimes/runner/lib"
)
var (
@@ -37,169 +29,14 @@ var (
image = flag.String("image", "", "docker image with runtime tests")
excludeFile = flag.String("exclude_file", "", "file containing list of tests to exclude, in CSV format with fields: test name, bug id, comment")
batchSize = flag.Int("batch", 50, "number of test cases run in one command")
+ timeout = flag.Duration("timeout", 90*time.Minute, "batch timeout")
)
-// Wait time for each test to run.
-const timeout = 90 * 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 exclude..
- excludes, err := getExcludes()
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error getting exclude list: %s\n", err.Error())
- return 1
- }
-
- // Construct the shared docker instance.
- ctx := context.Background()
- d := dockerutil.MakeContainer(ctx, testutil.DefaultLogger(*lang))
- defer d.CleanUp(ctx)
-
- if err := testutil.TouchShardStatusFile(); err != nil {
- fmt.Fprintf(os.Stderr, "error touching status shard file: %v\n", err)
- return 1
- }
-
- // 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(ctx, d, excludes)
- if err != nil {
- fmt.Fprintf(os.Stderr, "%s\n", err.Error())
- return 1
- }
-
- m := testing.MainStart(testDeps{}, tests, nil, nil)
- return m.Run()
-}
-
-// getTests executes all tests as table tests.
-func getTests(ctx context.Context, d *dockerutil.Container, excludes map[string]struct{}) ([]testing.InternalTest, error) {
- // Start the container.
- opts := dockerutil.RunOpts{
- Image: fmt.Sprintf("runtimes/%s", *image),
- }
- d.CopyFiles(&opts, "/proctor", "test/runtimes/proctor/proctor")
- if err := d.Spawn(ctx, opts, "/proctor/proctor", "--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(ctx, dockerutil.ExecOpts{}, "/proctor/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)
- indices, err := testutil.TestIndicesForShard(len(tests))
- if err != nil {
- return nil, fmt.Errorf("TestsForShard() failed: %v", err)
- }
-
- var itests []testing.InternalTest
- for i := 0; i < len(indices); i += *batchSize {
- var tcs []string
- end := i + *batchSize
- if end > len(indices) {
- end = len(indices)
- }
- for _, tc := range indices[i:end] {
- // Add test if not excluded.
- if _, ok := excludes[tests[tc]]; ok {
- log.Infof("Skipping test case %s\n", tests[tc])
- continue
- }
- tcs = append(tcs, tests[tc])
- }
- itests = append(itests, testing.InternalTest{
- Name: strings.Join(tcs, ", "),
- F: func(t *testing.T) {
- var (
- now = time.Now()
- done = make(chan struct{})
- output string
- err error
- )
-
- go func() {
- fmt.Printf("RUNNING the following in a batch\n%s\n", strings.Join(tcs, "\n"))
- output, err = d.Exec(ctx, dockerutil.ExecOpts{}, "/proctor/proctor", "--runtime", *lang, "--tests", strings.Join(tcs, ","))
- close(done)
- }()
-
- select {
- case <-done:
- if err == nil {
- fmt.Printf("PASS: (%v)\n\n", time.Since(now))
- return
- }
- t.Errorf("FAIL: (%v):\n%s\n", time.Since(now), output)
- case <-time.After(timeout):
- t.Errorf("TIMEOUT: (%v):\n%s\n", time.Since(now), output)
- }
- },
- })
- }
-
- return itests, nil
+ os.Exit(lib.RunTests(*lang, *image, *excludeFile, *batchSize, *timeout))
}
-
-// getBlacklist reads the exclude file and returns a set of test names to
-// exclude.
-func getExcludes() (map[string]struct{}, error) {
- excludes := make(map[string]struct{})
- if *excludeFile == "" {
- return excludes, nil
- }
- f, err := os.Open(*excludeFile)
- 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
- }
- excludes[record[0]] = struct{}{}
- }
- return excludes, 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/syscalls/BUILD b/test/syscalls/BUILD
index f949bc0e3..f66a9ceb4 100644
--- a/test/syscalls/BUILD
+++ b/test/syscalls/BUILD
@@ -238,7 +238,7 @@ syscall_test(
syscall_test(
size = "medium",
- add_overlay = False, # TODO(gvisor.dev/issue/317): enable when fixed.
+ add_overlay = True,
test = "//test/syscalls/linux:inotify_test",
)
@@ -286,6 +286,10 @@ syscall_test(
)
syscall_test(
+ test = "//test/syscalls/linux:membarrier_test",
+)
+
+syscall_test(
test = "//test/syscalls/linux:memory_accounting_test",
)
diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD
index de753fc4e..36b7f1b97 100644
--- a/test/syscalls/linux/BUILD
+++ b/test/syscalls/linux/BUILD
@@ -1079,6 +1079,7 @@ cc_binary(
gtest,
"//test/util:test_main",
"//test/util:test_util",
+ "//test/util:thread_util",
],
)
@@ -1155,6 +1156,24 @@ cc_binary(
)
cc_binary(
+ name = "membarrier_test",
+ testonly = 1,
+ srcs = ["membarrier.cc"],
+ linkstatic = 1,
+ deps = [
+ "@com_google_absl//absl/time",
+ gtest,
+ "//test/util:cleanup",
+ "//test/util:logging",
+ "//test/util:memory_util",
+ "//test/util:posix_error",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ "//test/util:thread_util",
+ ],
+)
+
+cc_binary(
name = "mempolicy_test",
testonly = 1,
srcs = ["mempolicy.cc"],
@@ -1667,12 +1686,14 @@ cc_binary(
"//test/util:cleanup",
"//test/util:file_descriptor",
"//test/util:fs_util",
+ "@com_google_absl//absl/container:node_hash_set",
"@com_google_absl//absl/strings",
"@com_google_absl//absl/synchronization",
"@com_google_absl//absl/time",
gtest,
"//test/util:memory_util",
"//test/util:posix_error",
+ "//test/util:proc_util",
"//test/util:temp_path",
"//test/util:test_util",
"//test/util:thread_util",
@@ -2412,6 +2433,7 @@ cc_library(
":socket_test_util",
"@com_google_absl//absl/memory",
gtest,
+ "//test/util:posix_error",
"//test/util:test_util",
],
alwayslink = 1,
@@ -2430,6 +2452,7 @@ cc_library(
":socket_netlink_route_util",
":socket_test_util",
"//test/util:capability_util",
+ "//test/util:cleanup",
gtest,
],
alwayslink = 1,
diff --git a/test/syscalls/linux/fallocate.cc b/test/syscalls/linux/fallocate.cc
index cabc2b751..edd23e063 100644
--- a/test/syscalls/linux/fallocate.cc
+++ b/test/syscalls/linux/fallocate.cc
@@ -179,6 +179,12 @@ TEST_F(AllocateTest, FallocateOtherFDs) {
auto sock0 = FileDescriptor(socks[0]);
auto sock1 = FileDescriptor(socks[1]);
EXPECT_THAT(fallocate(sock0.get(), 0, 0, 10), SyscallFailsWithErrno(ENODEV));
+
+ int pipefds[2];
+ ASSERT_THAT(pipe(pipefds), SyscallSucceeds());
+ EXPECT_THAT(fallocate(pipefds[1], 0, 0, 10), SyscallFailsWithErrno(ESPIPE));
+ close(pipefds[0]);
+ close(pipefds[1]);
}
} // namespace
diff --git a/test/syscalls/linux/inotify.cc b/test/syscalls/linux/inotify.cc
index a5c421118..e4392a450 100644
--- a/test/syscalls/linux/inotify.cc
+++ b/test/syscalls/linux/inotify.cc
@@ -465,7 +465,9 @@ TEST(Inotify, ConcurrentFileDeletionAndWatchRemoval) {
for (int i = 0; i < 100; ++i) {
FileDescriptor file_fd =
ASSERT_NO_ERRNO_AND_VALUE(Open(filename, O_CREAT, S_IRUSR | S_IWUSR));
- file_fd.reset(); // Close before unlinking (although save is disabled).
+ // Close before unlinking (although S/R is disabled). Some filesystems
+ // cannot restore an open fd on an unlinked file.
+ file_fd.reset();
EXPECT_THAT(unlink(filename.c_str()), SyscallSucceeds());
}
};
@@ -1256,10 +1258,7 @@ TEST(Inotify, MknodGeneratesCreateEvent) {
InotifyAddWatch(fd.get(), root.path(), IN_ALL_EVENTS));
const TempPath file1(root.path() + "/file1");
- const int rc = mknod(file1.path().c_str(), S_IFREG, 0);
- // mknod(2) is only supported on tmpfs in the sandbox.
- SKIP_IF(IsRunningOnGvisor() && rc != 0);
- ASSERT_THAT(rc, SyscallSucceeds());
+ ASSERT_THAT(mknod(file1.path().c_str(), S_IFREG, 0), SyscallSucceeds());
const std::vector<Event> events =
ASSERT_NO_ERRNO_AND_VALUE(DrainEvents(fd.get()));
@@ -1289,6 +1288,10 @@ TEST(Inotify, SymlinkGeneratesCreateEvent) {
}
TEST(Inotify, LinkGeneratesAttribAndCreateEvents) {
+ // Inotify does not work properly with hard links in gofer and overlay fs.
+ SKIP_IF(IsRunningOnGvisor() &&
+ !ASSERT_NO_ERRNO_AND_VALUE(IsTmpfs(GetAbsoluteTestTmpdir())));
+
const TempPath root = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
const TempPath file1 =
ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileIn(root.path()));
@@ -1301,11 +1304,8 @@ TEST(Inotify, LinkGeneratesAttribAndCreateEvents) {
const int file1_wd = ASSERT_NO_ERRNO_AND_VALUE(
InotifyAddWatch(fd.get(), file1.path(), IN_ALL_EVENTS));
- const int rc = link(file1.path().c_str(), link1.path().c_str());
- // NOTE(b/34861058): link(2) is only supported on tmpfs in the sandbox.
- SKIP_IF(IsRunningOnGvisor() && rc != 0 &&
- (errno == EPERM || errno == ENOENT));
- ASSERT_THAT(rc, SyscallSucceeds());
+ ASSERT_THAT(link(file1.path().c_str(), link1.path().c_str()),
+ SyscallSucceeds());
const std::vector<Event> events =
ASSERT_NO_ERRNO_AND_VALUE(DrainEvents(fd.get()));
@@ -1334,68 +1334,70 @@ TEST(Inotify, UtimesGeneratesAttribEvent) {
}
TEST(Inotify, HardlinksReuseSameWatch) {
+ // Inotify does not work properly with hard links in gofer and overlay fs.
+ SKIP_IF(IsRunningOnGvisor() &&
+ !ASSERT_NO_ERRNO_AND_VALUE(IsTmpfs(GetAbsoluteTestTmpdir())));
+
const TempPath root = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
- TempPath file1 =
+ TempPath file =
ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileIn(root.path()));
- TempPath link1(root.path() + "/link1");
- const int rc = link(file1.path().c_str(), link1.path().c_str());
- // link(2) is only supported on tmpfs in the sandbox.
- SKIP_IF(IsRunningOnGvisor() && rc != 0 &&
- (errno == EPERM || errno == ENOENT));
- ASSERT_THAT(rc, SyscallSucceeds());
+
+ TempPath file2(root.path() + "/file2");
+ ASSERT_THAT(link(file.path().c_str(), file2.path().c_str()),
+ SyscallSucceeds());
const FileDescriptor fd =
ASSERT_NO_ERRNO_AND_VALUE(InotifyInit1(IN_NONBLOCK));
const int root_wd = ASSERT_NO_ERRNO_AND_VALUE(
InotifyAddWatch(fd.get(), root.path(), IN_ALL_EVENTS));
- const int file1_wd = ASSERT_NO_ERRNO_AND_VALUE(
- InotifyAddWatch(fd.get(), file1.path(), IN_ALL_EVENTS));
- const int link1_wd = ASSERT_NO_ERRNO_AND_VALUE(
- InotifyAddWatch(fd.get(), link1.path(), IN_ALL_EVENTS));
+ const int file_wd = ASSERT_NO_ERRNO_AND_VALUE(
+ InotifyAddWatch(fd.get(), file.path(), IN_ALL_EVENTS));
+ const int file2_wd = ASSERT_NO_ERRNO_AND_VALUE(
+ InotifyAddWatch(fd.get(), file2.path(), IN_ALL_EVENTS));
// The watch descriptors for watches on different links to the same file
// should be identical.
- EXPECT_NE(root_wd, file1_wd);
- EXPECT_EQ(file1_wd, link1_wd);
+ EXPECT_NE(root_wd, file_wd);
+ EXPECT_EQ(file_wd, file2_wd);
- FileDescriptor file1_fd =
- ASSERT_NO_ERRNO_AND_VALUE(Open(file1.path(), O_WRONLY));
+ FileDescriptor file_fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_WRONLY));
std::vector<Event> events = ASSERT_NO_ERRNO_AND_VALUE(DrainEvents(fd.get()));
ASSERT_THAT(events,
- AreUnordered({Event(IN_OPEN, root_wd, Basename(file1.path())),
- Event(IN_OPEN, file1_wd)}));
+ AreUnordered({Event(IN_OPEN, root_wd, Basename(file.path())),
+ Event(IN_OPEN, file_wd)}));
// For the next step, we want to ensure all fds to the file are closed. Do
// that now and drain the resulting events.
- file1_fd.reset();
+ file_fd.reset();
events = ASSERT_NO_ERRNO_AND_VALUE(DrainEvents(fd.get()));
ASSERT_THAT(
events,
- AreUnordered({Event(IN_CLOSE_WRITE, root_wd, Basename(file1.path())),
- Event(IN_CLOSE_WRITE, file1_wd)}));
+ AreUnordered({Event(IN_CLOSE_WRITE, root_wd, Basename(file.path())),
+ Event(IN_CLOSE_WRITE, file_wd)}));
// Try removing the link and let's see what events show up. Note that after
// this, we still have a link to the file so the watch shouldn't be
// automatically removed.
- const std::string link1_path = link1.reset();
+ const std::string file2_path = file2.reset();
events = ASSERT_NO_ERRNO_AND_VALUE(DrainEvents(fd.get()));
ASSERT_THAT(events,
- AreUnordered({Event(IN_ATTRIB, link1_wd),
- Event(IN_DELETE, root_wd, Basename(link1_path))}));
+ AreUnordered({Event(IN_ATTRIB, file2_wd),
+ Event(IN_DELETE, root_wd, Basename(file2_path))}));
// Now remove the other link. Since this is the last link to the file, the
// watch should be automatically removed.
- const std::string file1_path = file1.reset();
+ const std::string file_path = file.reset();
events = ASSERT_NO_ERRNO_AND_VALUE(DrainEvents(fd.get()));
ASSERT_THAT(
events,
- AreUnordered({Event(IN_ATTRIB, file1_wd), Event(IN_DELETE_SELF, file1_wd),
- Event(IN_IGNORED, file1_wd),
- Event(IN_DELETE, root_wd, Basename(file1_path))}));
+ AreUnordered({Event(IN_ATTRIB, file_wd), Event(IN_DELETE_SELF, file_wd),
+ Event(IN_IGNORED, file_wd),
+ Event(IN_DELETE, root_wd, Basename(file_path))}));
}
// Calling mkdir within "parent/child" should generate an event for child, but
@@ -1806,17 +1808,17 @@ TEST(Inotify, SpliceOnInotifyFD) {
// Watches on a parent should not be triggered by actions on a hard link to one
// of its children that has a different parent.
TEST(Inotify, LinkOnOtherParent) {
+ // Inotify does not work properly with hard links in gofer and overlay fs.
+ SKIP_IF(IsRunningOnGvisor() &&
+ !ASSERT_NO_ERRNO_AND_VALUE(IsTmpfs(GetAbsoluteTestTmpdir())));
+
const TempPath dir1 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
const TempPath dir2 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
const TempPath file =
ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileIn(dir1.path()));
std::string link_path = NewTempAbsPathInDir(dir2.path());
- const int rc = link(file.path().c_str(), link_path.c_str());
- // NOTE(b/34861058): link(2) is only supported on tmpfs in the sandbox.
- SKIP_IF(IsRunningOnGvisor() && rc != 0 &&
- (errno == EPERM || errno == ENOENT));
- ASSERT_THAT(rc, SyscallSucceeds());
+ ASSERT_THAT(link(file.path().c_str(), link_path.c_str()), SyscallSucceeds());
const FileDescriptor inotify_fd =
ASSERT_NO_ERRNO_AND_VALUE(InotifyInit1(IN_NONBLOCK));
@@ -1825,13 +1827,18 @@ TEST(Inotify, LinkOnOtherParent) {
// Perform various actions on the link outside of dir1, which should trigger
// no inotify events.
- const FileDescriptor fd =
+ FileDescriptor fd =
ASSERT_NO_ERRNO_AND_VALUE(Open(link_path.c_str(), O_RDWR));
int val = 0;
ASSERT_THAT(write(fd.get(), &val, sizeof(val)), SyscallSucceeds());
ASSERT_THAT(read(fd.get(), &val, sizeof(val)), SyscallSucceeds());
ASSERT_THAT(ftruncate(fd.get(), 12345), SyscallSucceeds());
+
+ // Close before unlinking; some filesystems cannot restore an open fd on an
+ // unlinked file.
+ fd.reset();
ASSERT_THAT(unlink(link_path.c_str()), SyscallSucceeds());
+
const std::vector<Event> events =
ASSERT_NO_ERRNO_AND_VALUE(DrainEvents(inotify_fd.get()));
EXPECT_THAT(events, Are({}));
@@ -2055,21 +2062,21 @@ TEST(Inotify, ExcludeUnlinkDirectory_NoRandomSave) {
// We need to disable S/R because there are filesystems where we cannot re-open
// fds to an unlinked file across S/R, e.g. gofer-backed filesytems.
TEST(Inotify, ExcludeUnlinkMultipleChildren_NoRandomSave) {
- const DisableSave ds;
+ // Inotify does not work properly with hard links in gofer and overlay fs.
+ SKIP_IF(IsRunningOnGvisor() &&
+ !ASSERT_NO_ERRNO_AND_VALUE(IsTmpfs(GetAbsoluteTestTmpdir())));
// TODO(gvisor.dev/issue/1624): This test fails on VFS1.
SKIP_IF(IsRunningWithVFS1());
+ const DisableSave ds;
+
const TempPath dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
const TempPath file =
ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileIn(dir.path()));
std::string path1 = file.path();
std::string path2 = NewTempAbsPathInDir(dir.path());
+ ASSERT_THAT(link(path1.c_str(), path2.c_str()), SyscallSucceeds());
- const int rc = link(path1.c_str(), path2.c_str());
- // NOTE(b/34861058): link(2) is only supported on tmpfs in the sandbox.
- SKIP_IF(IsRunningOnGvisor() && rc != 0 &&
- (errno == EPERM || errno == ENOENT));
- ASSERT_THAT(rc, SyscallSucceeds());
const FileDescriptor fd1 =
ASSERT_NO_ERRNO_AND_VALUE(Open(path1.c_str(), O_RDWR));
const FileDescriptor fd2 =
@@ -2101,6 +2108,15 @@ TEST(Inotify, ExcludeUnlinkMultipleChildren_NoRandomSave) {
// We need to disable S/R because there are filesystems where we cannot re-open
// fds to an unlinked file across S/R, e.g. gofer-backed filesytems.
TEST(Inotify, ExcludeUnlinkInodeEvents_NoRandomSave) {
+ // TODO(gvisor.dev/issue/1624): Fails on VFS1.
+ SKIP_IF(IsRunningWithVFS1());
+
+ // NOTE(gvisor.dev/issue/3654): In the gofer filesystem, we do not allow
+ // setting attributes through an fd if the file at the open path has been
+ // deleted.
+ SKIP_IF(IsRunningOnGvisor() &&
+ !ASSERT_NO_ERRNO_AND_VALUE(IsTmpfs(GetAbsoluteTestTmpdir())));
+
const DisableSave ds;
const TempPath dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
@@ -2110,18 +2126,6 @@ TEST(Inotify, ExcludeUnlinkInodeEvents_NoRandomSave) {
const FileDescriptor fd =
ASSERT_NO_ERRNO_AND_VALUE(Open(file.path().c_str(), O_RDWR));
- // NOTE(b/157163751): Create another link before unlinking. This is needed for
- // the gofer filesystem in gVisor, where open fds will not work once the link
- // count hits zero. In VFS2, we end up skipping the gofer test anyway, because
- // hard links are not supported for gofer fs.
- if (IsRunningOnGvisor()) {
- std::string link_path = NewTempAbsPath();
- const int rc = link(file.path().c_str(), link_path.c_str());
- // NOTE(b/34861058): link(2) is only supported on tmpfs in the sandbox.
- SKIP_IF(rc != 0 && (errno == EPERM || errno == ENOENT));
- ASSERT_THAT(rc, SyscallSucceeds());
- }
-
const FileDescriptor inotify_fd =
ASSERT_NO_ERRNO_AND_VALUE(InotifyInit1(IN_NONBLOCK));
const int dir_wd = ASSERT_NO_ERRNO_AND_VALUE(InotifyAddWatch(
diff --git a/test/syscalls/linux/ip6tables.cc b/test/syscalls/linux/ip6tables.cc
index 78e1fa09d..de0a1c114 100644
--- a/test/syscalls/linux/ip6tables.cc
+++ b/test/syscalls/linux/ip6tables.cc
@@ -82,13 +82,37 @@ TEST(IP6TablesBasic, GetEntriesErrorPrecedence) {
SyscallFailsWithErrno(EINVAL));
}
+TEST(IP6TablesBasic, GetRevision) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW)));
+
+ int sock;
+ ASSERT_THAT(sock = socket(AF_INET6, SOCK_RAW, IPPROTO_RAW),
+ SyscallSucceeds());
+
+ struct xt_get_revision rev = {
+ .name = "REDIRECT",
+ .revision = 0,
+ };
+ socklen_t rev_len = sizeof(rev);
+
+ // Revision 0 exists.
+ EXPECT_THAT(
+ getsockopt(sock, SOL_IPV6, IP6T_SO_GET_REVISION_TARGET, &rev, &rev_len),
+ SyscallSucceeds());
+ EXPECT_EQ(rev.revision, 0);
+
+ // Revisions > 0 don't exist.
+ rev.revision = 1;
+ EXPECT_THAT(
+ getsockopt(sock, SOL_IPV6, IP6T_SO_GET_REVISION_TARGET, &rev, &rev_len),
+ SyscallFailsWithErrno(EPROTONOSUPPORT));
+}
+
// This tests the initial state of a machine with empty ip6tables via
// getsockopt(IP6T_SO_GET_INFO). We don't have a guarantee that the iptables are
// empty when running in native, but we can test that gVisor has the same
// initial state that a newly-booted Linux machine would have.
TEST(IP6TablesTest, InitialInfo) {
- // TODO(gvisor.dev/issue/3549): Enable for ip6tables.
- SKIP_IF(true);
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW)));
FileDescriptor sock =
@@ -132,8 +156,6 @@ TEST(IP6TablesTest, InitialInfo) {
// are empty when running in native, but we can test that gVisor has the same
// initial state that a newly-booted Linux machine would have.
TEST(IP6TablesTest, InitialEntries) {
- // TODO(gvisor.dev/issue/3549): Enable for ip6tables.
- SKIP_IF(true);
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW)));
FileDescriptor sock =
diff --git a/test/syscalls/linux/iptables.cc b/test/syscalls/linux/iptables.cc
index 83b6a164a..7ee10bbde 100644
--- a/test/syscalls/linux/iptables.cc
+++ b/test/syscalls/linux/iptables.cc
@@ -117,6 +117,32 @@ TEST(IPTablesBasic, OriginalDstErrors) {
SyscallFailsWithErrno(ENOTCONN));
}
+TEST(IPTablesBasic, GetRevision) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW)));
+
+ int sock;
+ ASSERT_THAT(sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP),
+ SyscallSucceeds());
+
+ struct xt_get_revision rev = {
+ .name = "REDIRECT",
+ .revision = 0,
+ };
+ socklen_t rev_len = sizeof(rev);
+
+ // Revision 0 exists.
+ EXPECT_THAT(
+ getsockopt(sock, SOL_IP, IPT_SO_GET_REVISION_TARGET, &rev, &rev_len),
+ SyscallSucceeds());
+ EXPECT_EQ(rev.revision, 0);
+
+ // Revisions > 0 don't exist.
+ rev.revision = 1;
+ EXPECT_THAT(
+ getsockopt(sock, SOL_IP, IPT_SO_GET_REVISION_TARGET, &rev, &rev_len),
+ SyscallFailsWithErrno(EPROTONOSUPPORT));
+}
+
// Fixture for iptables tests.
class IPTablesTest : public ::testing::Test {
protected:
diff --git a/test/syscalls/linux/kcov.cc b/test/syscalls/linux/kcov.cc
index f3c30444e..6816c1fd0 100644
--- a/test/syscalls/linux/kcov.cc
+++ b/test/syscalls/linux/kcov.cc
@@ -16,38 +16,47 @@
#include <sys/ioctl.h>
#include <sys/mman.h>
+#include <atomic>
+
#include "gtest/gtest.h"
#include "test/util/capability_util.h"
#include "test/util/file_descriptor.h"
#include "test/util/test_util.h"
+#include "test/util/thread_util.h"
namespace gvisor {
namespace testing {
namespace {
-// For this test to work properly, it must be run with coverage enabled. On
+// For this set of tests to run, they must be run with coverage enabled. On
// native Linux, this involves compiling the kernel with kcov enabled. For
-// gVisor, we need to enable the Go coverage tool, e.g.
-// bazel test --collect_coverage_data --instrumentation_filter=//pkg/... <test>.
+// gVisor, we need to enable the Go coverage tool, e.g. bazel test --
+// collect_coverage_data --instrumentation_filter=//pkg/... <test>.
+
+constexpr char kcovPath[] = "/sys/kernel/debug/kcov";
+constexpr int kSize = 4096;
+constexpr int KCOV_INIT_TRACE = 0x80086301;
+constexpr int KCOV_ENABLE = 0x6364;
+constexpr int KCOV_DISABLE = 0x6365;
+
+uint64_t* KcovMmap(int fd) {
+ return (uint64_t*)mmap(nullptr, kSize * sizeof(uint64_t),
+ PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+}
+
TEST(KcovTest, Kcov) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability((CAP_DAC_OVERRIDE))));
- constexpr int kSize = 4096;
- constexpr int KCOV_INIT_TRACE = 0x80086301;
- constexpr int KCOV_ENABLE = 0x6364;
-
int fd;
- ASSERT_THAT(fd = open("/sys/kernel/debug/kcov", O_RDWR),
+ ASSERT_THAT(fd = open(kcovPath, O_RDWR),
AnyOf(SyscallSucceeds(), SyscallFailsWithErrno(ENOENT)));
-
- // Kcov not enabled.
+ // Kcov not available.
SKIP_IF(errno == ENOENT);
+ auto fd_closer = Cleanup([fd]() { close(fd); });
ASSERT_THAT(ioctl(fd, KCOV_INIT_TRACE, kSize), SyscallSucceeds());
- uint64_t* area = (uint64_t*)mmap(nullptr, kSize * sizeof(uint64_t),
- PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
-
+ uint64_t* area = KcovMmap(fd);
ASSERT_TRUE(area != MAP_FAILED);
ASSERT_THAT(ioctl(fd, KCOV_ENABLE, 0), SyscallSucceeds());
@@ -62,6 +71,111 @@ TEST(KcovTest, Kcov) {
// Verify that PCs are in the standard kernel range.
EXPECT_GT(area[i], 0xffffffff7fffffffL);
}
+
+ ASSERT_THAT(ioctl(fd, KCOV_DISABLE, 0), SyscallSucceeds());
+}
+
+TEST(KcovTest, PrematureMmap) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability((CAP_DAC_OVERRIDE))));
+
+ int fd;
+ ASSERT_THAT(fd = open(kcovPath, O_RDWR),
+ AnyOf(SyscallSucceeds(), SyscallFailsWithErrno(ENOENT)));
+ // Kcov not available.
+ SKIP_IF(errno == ENOENT);
+ auto fd_closer = Cleanup([fd]() { close(fd); });
+
+ // Cannot mmap before KCOV_INIT_TRACE.
+ uint64_t* area = KcovMmap(fd);
+ ASSERT_TRUE(area == MAP_FAILED);
+}
+
+// Tests that multiple kcov fds can be used simultaneously.
+TEST(KcovTest, MultipleFds) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability((CAP_DAC_OVERRIDE))));
+
+ int fd1;
+ ASSERT_THAT(fd1 = open(kcovPath, O_RDWR),
+ AnyOf(SyscallSucceeds(), SyscallFailsWithErrno(ENOENT)));
+ // Kcov not available.
+ SKIP_IF(errno == ENOENT);
+
+ int fd2;
+ ASSERT_THAT(fd2 = open(kcovPath, O_RDWR), SyscallSucceeds());
+ auto fd_closer = Cleanup([fd1, fd2]() {
+ close(fd1);
+ close(fd2);
+ });
+
+ auto t1 = ScopedThread([&] {
+ ASSERT_THAT(ioctl(fd1, KCOV_INIT_TRACE, kSize), SyscallSucceeds());
+ uint64_t* area = KcovMmap(fd1);
+ ASSERT_TRUE(area != MAP_FAILED);
+ ASSERT_THAT(ioctl(fd1, KCOV_ENABLE, 0), SyscallSucceeds());
+ });
+
+ ASSERT_THAT(ioctl(fd2, KCOV_INIT_TRACE, kSize), SyscallSucceeds());
+ uint64_t* area = KcovMmap(fd2);
+ ASSERT_TRUE(area != MAP_FAILED);
+ ASSERT_THAT(ioctl(fd2, KCOV_ENABLE, 0), SyscallSucceeds());
+}
+
+// Tests behavior for two threads trying to use the same kcov fd.
+TEST(KcovTest, MultipleThreads) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability((CAP_DAC_OVERRIDE))));
+
+ int fd;
+ ASSERT_THAT(fd = open(kcovPath, O_RDWR),
+ AnyOf(SyscallSucceeds(), SyscallFailsWithErrno(ENOENT)));
+ // Kcov not available.
+ SKIP_IF(errno == ENOENT);
+ auto fd_closer = Cleanup([fd]() { close(fd); });
+
+ // Test the behavior of multiple threads trying to use the same kcov fd
+ // simultaneously.
+ std::atomic<bool> t1_enabled(false), t1_disabled(false), t2_failed(false),
+ t2_exited(false);
+ auto t1 = ScopedThread([&] {
+ ASSERT_THAT(ioctl(fd, KCOV_INIT_TRACE, kSize), SyscallSucceeds());
+ uint64_t* area = KcovMmap(fd);
+ ASSERT_TRUE(area != MAP_FAILED);
+ ASSERT_THAT(ioctl(fd, KCOV_ENABLE, 0), SyscallSucceeds());
+ t1_enabled = true;
+
+ // After t2 has made sure that enabling kcov again fails, disable it.
+ while (!t2_failed) {
+ sched_yield();
+ }
+ ASSERT_THAT(ioctl(fd, KCOV_DISABLE, 0), SyscallSucceeds());
+ t1_disabled = true;
+
+ // Wait for t2 to enable kcov and then exit, after which we should be able
+ // to enable kcov again, without needing to set up a new memory mapping.
+ while (!t2_exited) {
+ sched_yield();
+ }
+ ASSERT_THAT(ioctl(fd, KCOV_ENABLE, 0), SyscallSucceeds());
+ });
+
+ auto t2 = ScopedThread([&] {
+ // Wait for t1 to enable kcov, and make sure that enabling kcov again fails.
+ while (!t1_enabled) {
+ sched_yield();
+ }
+ ASSERT_THAT(ioctl(fd, KCOV_ENABLE, 0), SyscallFailsWithErrno(EINVAL));
+ t2_failed = true;
+
+ // Wait for t1 to disable kcov, after which using fd should now succeed.
+ while (!t1_disabled) {
+ sched_yield();
+ }
+ uint64_t* area = KcovMmap(fd);
+ ASSERT_TRUE(area != MAP_FAILED);
+ ASSERT_THAT(ioctl(fd, KCOV_ENABLE, 0), SyscallSucceeds());
+ });
+
+ t2.Join();
+ t2_exited = true;
}
} // namespace
diff --git a/test/syscalls/linux/membarrier.cc b/test/syscalls/linux/membarrier.cc
new file mode 100644
index 000000000..516956a25
--- /dev/null
+++ b/test/syscalls/linux/membarrier.cc
@@ -0,0 +1,268 @@
+// Copyright 2020 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 <signal.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <atomic>
+
+#include "absl/time/clock.h"
+#include "absl/time/time.h"
+#include "test/util/cleanup.h"
+#include "test/util/logging.h"
+#include "test/util/memory_util.h"
+#include "test/util/posix_error.h"
+#include "test/util/test_util.h"
+#include "test/util/thread_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+// This is the classic test case for memory fences on architectures with total
+// store ordering; see e.g. Intel SDM Vol. 3A Sec. 8.2.3.4 "Loads May Be
+// Reordered with Earlier Stores to Different Locations". In each iteration of
+// the test, given two variables X and Y initially set to 0
+// (MembarrierTestSharedState::local_var and remote_var in the code), two
+// threads execute as follows:
+//
+// T1 T2
+// -- --
+//
+// X = 1 Y = 1
+// T1fence() T2fence()
+// read Y read X
+//
+// On architectures where memory writes may be locally buffered by each CPU
+// (essentially all architectures), if T1fence() and T2fence() are omitted or
+// ineffective, it is possible for both T1 and T2 to read 0 because the memory
+// write from the other CPU is not yet visible outside that CPU. T1fence() and
+// T2fence() are expected to perform the necessary synchronization to restore
+// sequential consistency: both threads agree on a order of memory accesses that
+// is consistent with program order in each thread, such that at least one
+// thread reads 1.
+//
+// In the NoMembarrier test, T1fence() and T2fence() are both ordinary memory
+// fences establishing ordering between memory accesses before and after the
+// fence (std::atomic_thread_fence). In all other test cases, T1fence() is not a
+// memory fence at all, but only prevents compiler reordering of memory accesses
+// (std::atomic_signal_fence); T2fence() is an invocation of the membarrier()
+// syscall, which establishes ordering of memory accesses before and after the
+// syscall on both threads.
+
+template <typename F>
+int DoMembarrierTestSide(std::atomic<int>* our_var,
+ std::atomic<int> const& their_var,
+ F const& test_fence) {
+ our_var->store(1, std::memory_order_relaxed);
+ test_fence();
+ return their_var.load(std::memory_order_relaxed);
+}
+
+struct MembarrierTestSharedState {
+ std::atomic<int64_t> remote_iter_cur;
+ std::atomic<int64_t> remote_iter_done;
+ std::atomic<int> local_var;
+ std::atomic<int> remote_var;
+ int remote_obs_of_local_var;
+
+ void Init() {
+ remote_iter_cur.store(-1, std::memory_order_relaxed);
+ remote_iter_done.store(-1, std::memory_order_relaxed);
+ }
+};
+
+// Special value for MembarrierTestSharedState::remote_iter_cur indicating that
+// the remote thread should terminate.
+constexpr int64_t kRemoteIterStop = -2;
+
+// Must be async-signal-safe.
+template <typename F>
+void RunMembarrierTestRemoteSide(MembarrierTestSharedState* state,
+ F const& test_fence) {
+ int64_t i = 0;
+ int64_t cur;
+ while (true) {
+ while ((cur = state->remote_iter_cur.load(std::memory_order_acquire)) < i) {
+ if (cur == kRemoteIterStop) {
+ return;
+ }
+ // spin
+ }
+ state->remote_obs_of_local_var =
+ DoMembarrierTestSide(&state->remote_var, state->local_var, test_fence);
+ state->remote_iter_done.store(i, std::memory_order_release);
+ i++;
+ }
+}
+
+template <typename F>
+void RunMembarrierTestLocalSide(MembarrierTestSharedState* state,
+ F const& test_fence) {
+ // On test completion, instruct the remote thread to terminate.
+ Cleanup cleanup_remote([&] {
+ state->remote_iter_cur.store(kRemoteIterStop, std::memory_order_relaxed);
+ });
+
+ int64_t i = 0;
+ absl::Time end = absl::Now() + absl::Seconds(5); // arbitrary test duration
+ while (absl::Now() < end) {
+ // Reset both vars to 0.
+ state->local_var.store(0, std::memory_order_relaxed);
+ state->remote_var.store(0, std::memory_order_relaxed);
+ // Instruct the remote thread to begin this iteration.
+ state->remote_iter_cur.store(i, std::memory_order_release);
+ // Perform our side of the test.
+ auto local_obs_of_remote_var =
+ DoMembarrierTestSide(&state->local_var, state->remote_var, test_fence);
+ // Wait for the remote thread to finish this iteration.
+ while (state->remote_iter_done.load(std::memory_order_acquire) < i) {
+ // spin
+ }
+ ASSERT_TRUE(local_obs_of_remote_var != 0 ||
+ state->remote_obs_of_local_var != 0);
+ i++;
+ }
+}
+
+TEST(MembarrierTest, NoMembarrier) {
+ MembarrierTestSharedState state;
+ state.Init();
+
+ ScopedThread remote_thread([&] {
+ RunMembarrierTestRemoteSide(
+ &state, [] { std::atomic_thread_fence(std::memory_order_seq_cst); });
+ });
+ RunMembarrierTestLocalSide(
+ &state, [] { std::atomic_thread_fence(std::memory_order_seq_cst); });
+}
+
+enum membarrier_cmd {
+ MEMBARRIER_CMD_QUERY = 0,
+ MEMBARRIER_CMD_GLOBAL = (1 << 0),
+ MEMBARRIER_CMD_GLOBAL_EXPEDITED = (1 << 1),
+ MEMBARRIER_CMD_REGISTER_GLOBAL_EXPEDITED = (1 << 2),
+ MEMBARRIER_CMD_PRIVATE_EXPEDITED = (1 << 3),
+ MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED = (1 << 4),
+};
+
+int membarrier(membarrier_cmd cmd, int flags) {
+ return syscall(SYS_membarrier, cmd, flags);
+}
+
+PosixErrorOr<int> SupportedMembarrierCommands() {
+ int cmds = membarrier(MEMBARRIER_CMD_QUERY, 0);
+ if (cmds < 0) {
+ if (errno == ENOSYS) {
+ // No commands are supported.
+ return 0;
+ }
+ return PosixError(errno, "membarrier(MEMBARRIER_CMD_QUERY) failed");
+ }
+ return cmds;
+}
+
+TEST(MembarrierTest, Global) {
+ SKIP_IF((ASSERT_NO_ERRNO_AND_VALUE(SupportedMembarrierCommands()) &
+ MEMBARRIER_CMD_GLOBAL) == 0);
+
+ Mapping m = ASSERT_NO_ERRNO_AND_VALUE(
+ MmapAnon(kPageSize, PROT_READ | PROT_WRITE, MAP_SHARED));
+ auto state = static_cast<MembarrierTestSharedState*>(m.ptr());
+ state->Init();
+
+ pid_t const child_pid = fork();
+ if (child_pid == 0) {
+ // In child process.
+ RunMembarrierTestRemoteSide(
+ state, [] { TEST_PCHECK(membarrier(MEMBARRIER_CMD_GLOBAL, 0) == 0); });
+ _exit(0);
+ }
+ // In parent process.
+ ASSERT_THAT(child_pid, SyscallSucceeds());
+ Cleanup cleanup_child([&] {
+ int status;
+ ASSERT_THAT(waitpid(child_pid, &status, 0),
+ SyscallSucceedsWithValue(child_pid));
+ EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0)
+ << " status " << status;
+ });
+ RunMembarrierTestLocalSide(
+ state, [] { std::atomic_signal_fence(std::memory_order_seq_cst); });
+}
+
+TEST(MembarrierTest, GlobalExpedited) {
+ constexpr int kRequiredCommands = MEMBARRIER_CMD_GLOBAL_EXPEDITED |
+ MEMBARRIER_CMD_REGISTER_GLOBAL_EXPEDITED;
+ SKIP_IF((ASSERT_NO_ERRNO_AND_VALUE(SupportedMembarrierCommands()) &
+ kRequiredCommands) != kRequiredCommands);
+
+ ASSERT_THAT(membarrier(MEMBARRIER_CMD_REGISTER_GLOBAL_EXPEDITED, 0),
+ SyscallSucceeds());
+
+ Mapping m = ASSERT_NO_ERRNO_AND_VALUE(
+ MmapAnon(kPageSize, PROT_READ | PROT_WRITE, MAP_SHARED));
+ auto state = static_cast<MembarrierTestSharedState*>(m.ptr());
+ state->Init();
+
+ pid_t const child_pid = fork();
+ if (child_pid == 0) {
+ // In child process.
+ RunMembarrierTestRemoteSide(state, [] {
+ TEST_PCHECK(membarrier(MEMBARRIER_CMD_GLOBAL_EXPEDITED, 0) == 0);
+ });
+ _exit(0);
+ }
+ // In parent process.
+ ASSERT_THAT(child_pid, SyscallSucceeds());
+ Cleanup cleanup_child([&] {
+ int status;
+ ASSERT_THAT(waitpid(child_pid, &status, 0),
+ SyscallSucceedsWithValue(child_pid));
+ EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0)
+ << " status " << status;
+ });
+ RunMembarrierTestLocalSide(
+ state, [] { std::atomic_signal_fence(std::memory_order_seq_cst); });
+}
+
+TEST(MembarrierTest, PrivateExpedited) {
+ constexpr int kRequiredCommands = MEMBARRIER_CMD_PRIVATE_EXPEDITED |
+ MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED;
+ SKIP_IF((ASSERT_NO_ERRNO_AND_VALUE(SupportedMembarrierCommands()) &
+ kRequiredCommands) != kRequiredCommands);
+
+ ASSERT_THAT(membarrier(MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED, 0),
+ SyscallSucceeds());
+
+ MembarrierTestSharedState state;
+ state.Init();
+
+ ScopedThread remote_thread([&] {
+ RunMembarrierTestRemoteSide(&state, [] {
+ TEST_PCHECK(membarrier(MEMBARRIER_CMD_PRIVATE_EXPEDITED, 0) == 0);
+ });
+ });
+ RunMembarrierTestLocalSide(
+ &state, [] { std::atomic_signal_fence(std::memory_order_seq_cst); });
+}
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/syscalls/linux/mknod.cc b/test/syscalls/linux/mknod.cc
index 89e4564e8..ae65d366b 100644
--- a/test/syscalls/linux/mknod.cc
+++ b/test/syscalls/linux/mknod.cc
@@ -105,11 +105,13 @@ TEST(MknodTest, UnimplementedTypesReturnError) {
}
TEST(MknodTest, Socket) {
+ SKIP_IF(IsRunningOnGvisor() && IsRunningWithVFS1());
+
ASSERT_THAT(chdir(GetAbsoluteTestTmpdir().c_str()), SyscallSucceeds());
- SKIP_IF(IsRunningOnGvisor() && IsRunningWithVFS1());
+ auto filename = NewTempRelPath();
- ASSERT_THAT(mknod("./file0", S_IFSOCK | S_IRUSR | S_IWUSR, 0),
+ ASSERT_THAT(mknod(filename.c_str(), S_IFSOCK | S_IRUSR | S_IWUSR, 0),
SyscallSucceeds());
int sk;
@@ -117,9 +119,10 @@ TEST(MknodTest, Socket) {
FileDescriptor fd(sk);
struct sockaddr_un addr = {.sun_family = AF_UNIX};
- absl::SNPrintF(addr.sun_path, sizeof(addr.sun_path), "./file0");
+ absl::SNPrintF(addr.sun_path, sizeof(addr.sun_path), "%s", filename.c_str());
ASSERT_THAT(connect(sk, (struct sockaddr *)&addr, sizeof(addr)),
SyscallFailsWithErrno(ECONNREFUSED));
+ ASSERT_THAT(unlink(filename.c_str()), SyscallSucceeds());
}
TEST(MknodTest, Fifo) {
diff --git a/test/syscalls/linux/packet_socket_raw.cc b/test/syscalls/linux/packet_socket_raw.cc
index f3c1d6bc9..b558e3a01 100644
--- a/test/syscalls/linux/packet_socket_raw.cc
+++ b/test/syscalls/linux/packet_socket_raw.cc
@@ -643,6 +643,27 @@ TEST_P(RawPacketTest, GetSocketDetachFilter) {
SyscallFailsWithErrno(ENOPROTOOPT));
}
+TEST_P(RawPacketTest, SetAndGetSocketLinger) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW)));
+
+ int level = SOL_SOCKET;
+ int type = SO_LINGER;
+
+ struct linger sl;
+ sl.l_onoff = 1;
+ sl.l_linger = 5;
+ ASSERT_THAT(setsockopt(s_, level, type, &sl, sizeof(sl)),
+ SyscallSucceedsWithValue(0));
+
+ struct linger got_linger = {};
+ socklen_t length = sizeof(sl);
+ ASSERT_THAT(getsockopt(s_, level, type, &got_linger, &length),
+ SyscallSucceedsWithValue(0));
+
+ ASSERT_EQ(length, sizeof(got_linger));
+ EXPECT_EQ(0, memcmp(&sl, &got_linger, length));
+}
+
INSTANTIATE_TEST_SUITE_P(AllInetTests, RawPacketTest,
::testing::Values(ETH_P_IP, ETH_P_ALL));
diff --git a/test/syscalls/linux/proc.cc b/test/syscalls/linux/proc.cc
index 1ba7bf4f5..e8fcc4439 100644
--- a/test/syscalls/linux/proc.cc
+++ b/test/syscalls/linux/proc.cc
@@ -47,6 +47,7 @@
#include "gmock/gmock.h"
#include "gtest/gtest.h"
+#include "absl/container/node_hash_set.h"
#include "absl/strings/ascii.h"
#include "absl/strings/match.h"
#include "absl/strings/numbers.h"
@@ -63,6 +64,7 @@
#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"
#include "test/util/thread_util.h"
@@ -672,6 +674,23 @@ TEST(ProcSelfMaps, Mprotect) {
3 * kPageSize, PROT_READ)));
}
+TEST(ProcSelfMaps, SharedAnon) {
+ const Mapping m = ASSERT_NO_ERRNO_AND_VALUE(
+ MmapAnon(kPageSize, PROT_READ, MAP_SHARED | MAP_ANONYMOUS));
+
+ const auto proc_self_maps =
+ ASSERT_NO_ERRNO_AND_VALUE(GetContents("/proc/self/maps"));
+ for (const auto& line : absl::StrSplit(proc_self_maps, '\n')) {
+ const auto entry = ASSERT_NO_ERRNO_AND_VALUE(ParseProcMapsLine(line));
+ if (entry.start <= m.addr() && m.addr() < entry.end) {
+ // cf. proc(5), "/proc/[pid]/map_files/"
+ EXPECT_EQ(entry.filename, "/dev/zero (deleted)");
+ return;
+ }
+ }
+ FAIL() << "no maps entry containing mapping at " << m.ptr();
+}
+
TEST(ProcSelfFd, OpenFd) {
int pipe_fds[2];
ASSERT_THAT(pipe2(pipe_fds, O_CLOEXEC), SyscallSucceeds());
@@ -703,8 +722,8 @@ static void CheckFdDirGetdentsDuplicates(const std::string& path) {
EXPECT_GE(newfd, 1024);
auto fd_closer = Cleanup([newfd]() { close(newfd); });
auto fd_files = ASSERT_NO_ERRNO_AND_VALUE(ListDir(path.c_str(), false));
- std::unordered_set<std::string> fd_files_dedup(fd_files.begin(),
- fd_files.end());
+ absl::node_hash_set<std::string> fd_files_dedup(fd_files.begin(),
+ fd_files.end());
EXPECT_EQ(fd_files.size(), fd_files_dedup.size());
}
@@ -761,8 +780,12 @@ TEST(ProcSelfFdInfo, Flags) {
}
TEST(ProcSelfExe, Absolute) {
- auto exe = ASSERT_NO_ERRNO_AND_VALUE(
- ReadLink(absl::StrCat("/proc/", getpid(), "/exe")));
+ auto exe = ASSERT_NO_ERRNO_AND_VALUE(ReadLink("/proc/self/exe"));
+ EXPECT_EQ(exe[0], '/');
+}
+
+TEST(ProcSelfCwd, Absolute) {
+ auto exe = ASSERT_NO_ERRNO_AND_VALUE(ReadLink("/proc/self/cwd"));
EXPECT_EQ(exe[0], '/');
}
@@ -1454,6 +1477,16 @@ TEST(ProcPidExe, Subprocess) {
EXPECT_EQ(actual, expected_absolute_path);
}
+// /proc/PID/cwd points to the correct directory.
+TEST(ProcPidCwd, Subprocess) {
+ auto want = ASSERT_NO_ERRNO_AND_VALUE(GetCWD());
+
+ char got[PATH_MAX + 1] = {};
+ ASSERT_THAT(ReadlinkWhileRunning("cwd", got, sizeof(got)),
+ SyscallSucceedsWithValue(Gt(0)));
+ EXPECT_EQ(got, want);
+}
+
// Test whether /proc/PID/ files can be read for a running process.
TEST(ProcPidFile, SubprocessRunning) {
char buf[1];
diff --git a/test/syscalls/linux/proc_net.cc b/test/syscalls/linux/proc_net.cc
index 4fab097f4..23677e296 100644
--- a/test/syscalls/linux/proc_net.cc
+++ b/test/syscalls/linux/proc_net.cc
@@ -39,6 +39,7 @@ namespace testing {
namespace {
constexpr const char kProcNet[] = "/proc/net";
+constexpr const char kIpForward[] = "/proc/sys/net/ipv4/ip_forward";
TEST(ProcNetSymlinkTarget, FileMode) {
struct stat s;
@@ -515,6 +516,46 @@ TEST(ProcSysNetIpv4Recovery, CanReadAndWrite) {
SyscallSucceedsWithValue(sizeof(kMessage)));
EXPECT_EQ(strcmp(buf, "100\n"), 0);
}
+
+TEST(ProcSysNetIpv4IpForward, Exists) {
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(Open(kIpForward, O_RDONLY));
+}
+
+TEST(ProcSysNetIpv4IpForward, DefaultValueEqZero) {
+ // Test is only valid in sandbox. Not hermetic in native tests
+ // running on a arbitrary machine.
+ SKIP_IF(!IsRunningOnGvisor());
+ auto const fd = ASSERT_NO_ERRNO_AND_VALUE(Open(kIpForward, O_RDONLY));
+
+ char buf = 101;
+ EXPECT_THAT(PreadFd(fd.get(), &buf, sizeof(buf), 0),
+ SyscallSucceedsWithValue(sizeof(buf)));
+
+ EXPECT_EQ(buf, '0') << "unexpected ip_forward: " << buf;
+}
+
+TEST(ProcSysNetIpv4IpForward, CanReadAndWrite) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability((CAP_DAC_OVERRIDE))));
+
+ auto const fd = ASSERT_NO_ERRNO_AND_VALUE(Open(kIpForward, O_RDWR));
+
+ char buf;
+ EXPECT_THAT(PreadFd(fd.get(), &buf, sizeof(buf), 0),
+ SyscallSucceedsWithValue(sizeof(buf)));
+
+ EXPECT_TRUE(buf == '0' || buf == '1') << "unexpected ip_forward: " << buf;
+
+ // constexpr char to_write = '1';
+ char to_write = (buf == '1') ? '0' : '1';
+ EXPECT_THAT(PwriteFd(fd.get(), &to_write, sizeof(to_write), 0),
+ SyscallSucceedsWithValue(sizeof(to_write)));
+
+ buf = 0;
+ EXPECT_THAT(PreadFd(fd.get(), &buf, sizeof(buf), 0),
+ SyscallSucceedsWithValue(sizeof(buf)));
+ EXPECT_EQ(buf, to_write);
+}
+
} // namespace
} // namespace testing
} // namespace gvisor
diff --git a/test/syscalls/linux/raw_socket_icmp.cc b/test/syscalls/linux/raw_socket_icmp.cc
index 3de898df7..1b9dbc584 100644
--- a/test/syscalls/linux/raw_socket_icmp.cc
+++ b/test/syscalls/linux/raw_socket_icmp.cc
@@ -416,6 +416,28 @@ TEST_F(RawSocketICMPTest, BindConnectSendAndReceive) {
ASSERT_NO_FATAL_FAILURE(ExpectICMPSuccess(icmp));
}
+// Set and get SO_LINGER.
+TEST_F(RawSocketICMPTest, SetAndGetSocketLinger) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_RAW)));
+
+ int level = SOL_SOCKET;
+ int type = SO_LINGER;
+
+ struct linger sl;
+ sl.l_onoff = 1;
+ sl.l_linger = 5;
+ ASSERT_THAT(setsockopt(s_, level, type, &sl, sizeof(sl)),
+ SyscallSucceedsWithValue(0));
+
+ struct linger got_linger = {};
+ socklen_t length = sizeof(sl);
+ ASSERT_THAT(getsockopt(s_, level, type, &got_linger, &length),
+ SyscallSucceedsWithValue(0));
+
+ ASSERT_EQ(length, sizeof(got_linger));
+ EXPECT_EQ(0, memcmp(&sl, &got_linger, length));
+}
+
void RawSocketICMPTest::ExpectICMPSuccess(const struct icmphdr& icmp) {
// We're going to receive both the echo request and reply, but the order is
// indeterminate.
diff --git a/test/syscalls/linux/rename.cc b/test/syscalls/linux/rename.cc
index 833c0dc4f..5458f54ad 100644
--- a/test/syscalls/linux/rename.cc
+++ b/test/syscalls/linux/rename.cc
@@ -170,6 +170,9 @@ TEST(RenameTest, FileOverwritesFile) {
}
TEST(RenameTest, DirectoryOverwritesDirectoryLinkCount) {
+ // Directory link counts are synthetic on overlay filesystems.
+ SKIP_IF(ASSERT_NO_ERRNO_AND_VALUE(IsOverlayfs(GetAbsoluteTestTmpdir())));
+
auto parent1 = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
EXPECT_THAT(Links(parent1.path()), IsPosixErrorOkAndHolds(2));
diff --git a/test/syscalls/linux/socket_inet_loopback.cc b/test/syscalls/linux/socket_inet_loopback.cc
index 54fee2e82..11fcec443 100644
--- a/test/syscalls/linux/socket_inet_loopback.cc
+++ b/test/syscalls/linux/socket_inet_loopback.cc
@@ -1116,9 +1116,6 @@ TEST_P(SocketInetLoopbackTest, TCPAcceptAfterReset) {
TestAddress const& listener = param.listener;
TestAddress const& connector = param.connector;
- // TODO(gvisor.dev/issue/1400): Remove this after SO_LINGER is fixed.
- SKIP_IF(IsRunningOnGvisor());
-
// Create the listening socket.
const FileDescriptor listen_fd = ASSERT_NO_ERRNO_AND_VALUE(
Socket(listener.family(), SOCK_STREAM, IPPROTO_TCP));
@@ -1178,12 +1175,20 @@ TEST_P(SocketInetLoopbackTest, TCPAcceptAfterReset) {
listen_fd.get(), reinterpret_cast<sockaddr*>(&accept_addr), &addrlen));
ASSERT_EQ(addrlen, listener.addr_len);
- int err;
- socklen_t optlen = sizeof(err);
- ASSERT_THAT(getsockopt(accept_fd.get(), SOL_SOCKET, SO_ERROR, &err, &optlen),
- SyscallSucceeds());
- ASSERT_EQ(err, ECONNRESET);
- ASSERT_EQ(optlen, sizeof(err));
+ // TODO(gvisor.dev/issue/3812): Remove after SO_ERROR is fixed.
+ if (IsRunningOnGvisor()) {
+ char buf[10];
+ ASSERT_THAT(ReadFd(accept_fd.get(), buf, sizeof(buf)),
+ SyscallFailsWithErrno(ECONNRESET));
+ } else {
+ int err;
+ socklen_t optlen = sizeof(err);
+ ASSERT_THAT(
+ getsockopt(accept_fd.get(), SOL_SOCKET, SO_ERROR, &err, &optlen),
+ SyscallSucceeds());
+ ASSERT_EQ(err, ECONNRESET);
+ ASSERT_EQ(optlen, sizeof(err));
+ }
}
// TODO(gvisor.dev/issue/1688): Partially completed passive endpoints are not
diff --git a/test/syscalls/linux/socket_ip_tcp_generic.cc b/test/syscalls/linux/socket_ip_tcp_generic.cc
index 04356b780..f4b69c46c 100644
--- a/test/syscalls/linux/socket_ip_tcp_generic.cc
+++ b/test/syscalls/linux/socket_ip_tcp_generic.cc
@@ -1080,5 +1080,124 @@ TEST_P(TCPSocketPairTest, TCPResetDuringClose_NoRandomSave) {
}
}
+// Test setsockopt and getsockopt for a socket with SO_LINGER option.
+TEST_P(TCPSocketPairTest, SetAndGetLingerOption) {
+ auto sockets = ASSERT_NO_ERRNO_AND_VALUE(NewSocketPair());
+
+ // Check getsockopt before SO_LINGER option is set.
+ struct linger got_linger = {-1, -1};
+ socklen_t got_len = sizeof(got_linger);
+
+ ASSERT_THAT(getsockopt(sockets->first_fd(), SOL_SOCKET, SO_LINGER,
+ &got_linger, &got_len),
+ SyscallSucceeds());
+ ASSERT_THAT(got_len, sizeof(got_linger));
+ struct linger want_linger = {};
+ EXPECT_EQ(0, memcmp(&want_linger, &got_linger, got_len));
+
+ // Set and get SO_LINGER with negative values.
+ struct linger sl;
+ sl.l_onoff = 1;
+ sl.l_linger = -3;
+ ASSERT_THAT(
+ setsockopt(sockets->first_fd(), SOL_SOCKET, SO_LINGER, &sl, sizeof(sl)),
+ SyscallSucceeds());
+ ASSERT_THAT(getsockopt(sockets->first_fd(), SOL_SOCKET, SO_LINGER,
+ &got_linger, &got_len),
+ SyscallSucceeds());
+ ASSERT_EQ(got_len, sizeof(got_linger));
+ EXPECT_EQ(sl.l_onoff, got_linger.l_onoff);
+ // Linux returns a different value as it uses HZ to convert the seconds to
+ // jiffies which overflows for negative values. We want to be compatible with
+ // linux for getsockopt return value.
+ if (IsRunningOnGvisor()) {
+ EXPECT_EQ(sl.l_linger, got_linger.l_linger);
+ }
+
+ // Set and get SO_LINGER option with positive values.
+ sl.l_onoff = 1;
+ sl.l_linger = 5;
+ ASSERT_THAT(
+ setsockopt(sockets->first_fd(), SOL_SOCKET, SO_LINGER, &sl, sizeof(sl)),
+ SyscallSucceeds());
+ ASSERT_THAT(getsockopt(sockets->first_fd(), SOL_SOCKET, SO_LINGER,
+ &got_linger, &got_len),
+ SyscallSucceeds());
+ ASSERT_EQ(got_len, sizeof(got_linger));
+ EXPECT_EQ(0, memcmp(&sl, &got_linger, got_len));
+}
+
+// Test socket to disable SO_LINGER option.
+TEST_P(TCPSocketPairTest, SetOffLingerOption) {
+ auto sockets = ASSERT_NO_ERRNO_AND_VALUE(NewSocketPair());
+
+ // Set the SO_LINGER option.
+ struct linger sl;
+ sl.l_onoff = 1;
+ sl.l_linger = 5;
+ ASSERT_THAT(
+ setsockopt(sockets->first_fd(), SOL_SOCKET, SO_LINGER, &sl, sizeof(sl)),
+ SyscallSucceeds());
+
+ // Check getsockopt after SO_LINGER option is set.
+ struct linger got_linger = {-1, -1};
+ socklen_t got_len = sizeof(got_linger);
+ ASSERT_THAT(getsockopt(sockets->first_fd(), SOL_SOCKET, SO_LINGER,
+ &got_linger, &got_len),
+ SyscallSucceeds());
+ ASSERT_EQ(got_len, sizeof(got_linger));
+ EXPECT_EQ(0, memcmp(&sl, &got_linger, got_len));
+
+ sl.l_onoff = 0;
+ sl.l_linger = 5;
+ ASSERT_THAT(
+ setsockopt(sockets->first_fd(), SOL_SOCKET, SO_LINGER, &sl, sizeof(sl)),
+ SyscallSucceeds());
+
+ // Check getsockopt after SO_LINGER option is set to zero.
+ ASSERT_THAT(getsockopt(sockets->first_fd(), SOL_SOCKET, SO_LINGER,
+ &got_linger, &got_len),
+ SyscallSucceeds());
+ ASSERT_EQ(got_len, sizeof(got_linger));
+ EXPECT_EQ(0, memcmp(&sl, &got_linger, got_len));
+}
+
+// Test close on dup'd socket with SO_LINGER option set.
+TEST_P(TCPSocketPairTest, CloseWithLingerOption) {
+ auto sockets = ASSERT_NO_ERRNO_AND_VALUE(NewSocketPair());
+
+ // Set the SO_LINGER option.
+ struct linger sl;
+ sl.l_onoff = 1;
+ sl.l_linger = 5;
+ ASSERT_THAT(
+ setsockopt(sockets->first_fd(), SOL_SOCKET, SO_LINGER, &sl, sizeof(sl)),
+ SyscallSucceeds());
+
+ // Check getsockopt after SO_LINGER option is set.
+ struct linger got_linger = {-1, -1};
+ socklen_t got_len = sizeof(got_linger);
+ ASSERT_THAT(getsockopt(sockets->first_fd(), SOL_SOCKET, SO_LINGER,
+ &got_linger, &got_len),
+ SyscallSucceeds());
+ ASSERT_EQ(got_len, sizeof(got_linger));
+ EXPECT_EQ(0, memcmp(&sl, &got_linger, got_len));
+
+ FileDescriptor dupFd = FileDescriptor(dup(sockets->first_fd()));
+ ASSERT_THAT(close(sockets->release_first_fd()), SyscallSucceeds());
+ char buf[10] = {};
+ // Write on dupFd should succeed as socket will not be closed until
+ // all references are removed.
+ ASSERT_THAT(RetryEINTR(write)(dupFd.get(), buf, sizeof(buf)),
+ SyscallSucceedsWithValue(sizeof(buf)));
+ ASSERT_THAT(RetryEINTR(write)(sockets->first_fd(), buf, sizeof(buf)),
+ SyscallFailsWithErrno(EBADF));
+
+ // Close the socket.
+ dupFd.reset();
+ // Write on dupFd should fail as all references for socket are removed.
+ ASSERT_THAT(RetryEINTR(write)(dupFd.get(), buf, sizeof(buf)),
+ SyscallFailsWithErrno(EBADF));
+}
} // namespace testing
} // namespace gvisor
diff --git a/test/syscalls/linux/socket_ip_udp_generic.cc b/test/syscalls/linux/socket_ip_udp_generic.cc
index bbe356116..3f2c0fdf2 100644
--- a/test/syscalls/linux/socket_ip_udp_generic.cc
+++ b/test/syscalls/linux/socket_ip_udp_generic.cc
@@ -450,5 +450,27 @@ TEST_P(UDPSocketPairTest, TClassRecvMismatch) {
SyscallFailsWithErrno(EOPNOTSUPP));
}
+// Test the SO_LINGER option can be set/get on udp socket.
+TEST_P(UDPSocketPairTest, SetAndGetSocketLinger) {
+ auto sockets = ASSERT_NO_ERRNO_AND_VALUE(NewSocketPair());
+ int level = SOL_SOCKET;
+ int type = SO_LINGER;
+
+ struct linger sl;
+ sl.l_onoff = 1;
+ sl.l_linger = 5;
+ ASSERT_THAT(setsockopt(sockets->first_fd(), level, type, &sl, sizeof(sl)),
+ SyscallSucceedsWithValue(0));
+
+ struct linger got_linger = {};
+ socklen_t length = sizeof(sl);
+ ASSERT_THAT(
+ getsockopt(sockets->first_fd(), level, type, &got_linger, &length),
+ SyscallSucceedsWithValue(0));
+
+ ASSERT_EQ(length, sizeof(got_linger));
+ EXPECT_EQ(0, memcmp(&sl, &got_linger, length));
+}
+
} // namespace testing
} // namespace gvisor
diff --git a/test/syscalls/linux/socket_ipv4_udp_unbound.cc b/test/syscalls/linux/socket_ipv4_udp_unbound.cc
index 02ea05e22..a72c76c97 100644
--- a/test/syscalls/linux/socket_ipv4_udp_unbound.cc
+++ b/test/syscalls/linux/socket_ipv4_udp_unbound.cc
@@ -27,6 +27,7 @@
#include "absl/memory/memory.h"
#include "test/syscalls/linux/ip_socket_test_util.h"
#include "test/syscalls/linux/socket_test_util.h"
+#include "test/util/posix_error.h"
#include "test/util/test_util.h"
namespace gvisor {
@@ -73,9 +74,9 @@ TEST_P(IPv4UDPUnboundSocketTest, IpMulticastLoopbackNoGroup) {
// Check that we did not receive the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- EXPECT_THAT(RetryEINTR(recv)(socket2->get(), recv_buf, sizeof(recv_buf),
- MSG_DONTWAIT),
- SyscallFailsWithErrno(EAGAIN));
+ EXPECT_THAT(
+ RecvMsgTimeout(socket2->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ PosixErrorIs(EAGAIN, ::testing::_));
}
// Check that not setting a default send interface prevents multicast packets
@@ -207,8 +208,9 @@ TEST_P(IPv4UDPUnboundSocketTest, IpMulticastLoopbackAddr) {
// Check that we received the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(RetryEINTR(recv)(socket2->get(), recv_buf, sizeof(recv_buf), 0),
- SyscallSucceedsWithValue(sizeof(recv_buf)));
+ ASSERT_THAT(
+ RecvMsgTimeout(socket2->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(recv_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
@@ -262,8 +264,9 @@ TEST_P(IPv4UDPUnboundSocketTest, IpMulticastLoopbackNic) {
// Check that we received the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(RetryEINTR(recv)(socket2->get(), recv_buf, sizeof(recv_buf), 0),
- SyscallSucceedsWithValue(sizeof(recv_buf)));
+ ASSERT_THAT(
+ RecvMsgTimeout(socket2->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(recv_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
@@ -317,8 +320,9 @@ TEST_P(IPv4UDPUnboundSocketTest, IpMulticastLoopbackIfAddr) {
// Check that we received the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(RetryEINTR(recv)(socket2->get(), recv_buf, sizeof(recv_buf), 0),
- SyscallSucceedsWithValue(sizeof(recv_buf)));
+ ASSERT_THAT(
+ RecvMsgTimeout(socket2->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(recv_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
@@ -372,8 +376,9 @@ TEST_P(IPv4UDPUnboundSocketTest, IpMulticastLoopbackIfNic) {
// Check that we received the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(RetryEINTR(recv)(socket2->get(), recv_buf, sizeof(recv_buf), 0),
- SyscallSucceedsWithValue(sizeof(recv_buf)));
+ ASSERT_THAT(
+ RecvMsgTimeout(socket2->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(recv_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
@@ -431,8 +436,9 @@ TEST_P(IPv4UDPUnboundSocketTest, IpMulticastLoopbackIfAddrConnect) {
// Check that we received the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(RetryEINTR(recv)(socket2->get(), recv_buf, sizeof(recv_buf), 0),
- SyscallSucceedsWithValue(sizeof(recv_buf)));
+ ASSERT_THAT(
+ RecvMsgTimeout(socket2->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(recv_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
@@ -490,8 +496,9 @@ TEST_P(IPv4UDPUnboundSocketTest, IpMulticastLoopbackIfNicConnect) {
// Check that we received the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(RetryEINTR(recv)(socket2->get(), recv_buf, sizeof(recv_buf), 0),
- SyscallSucceedsWithValue(sizeof(recv_buf)));
+ ASSERT_THAT(
+ RecvMsgTimeout(socket2->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(recv_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
@@ -545,8 +552,9 @@ TEST_P(IPv4UDPUnboundSocketTest, IpMulticastLoopbackIfAddrSelf) {
// Check that we received the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(RetryEINTR(recv)(socket1->get(), recv_buf, sizeof(recv_buf), 0),
- SyscallSucceedsWithValue(sizeof(recv_buf)));
+ ASSERT_THAT(
+ RecvMsgTimeout(socket1->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(recv_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
@@ -600,8 +608,9 @@ TEST_P(IPv4UDPUnboundSocketTest, IpMulticastLoopbackIfNicSelf) {
// Check that we received the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(RetryEINTR(recv)(socket1->get(), recv_buf, sizeof(recv_buf), 0),
- SyscallSucceedsWithValue(sizeof(recv_buf)));
+ ASSERT_THAT(
+ RecvMsgTimeout(socket1->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(recv_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
@@ -659,9 +668,9 @@ TEST_P(IPv4UDPUnboundSocketTest, IpMulticastLoopbackIfAddrSelfConnect) {
// Check that we did not receive the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- EXPECT_THAT(RetryEINTR(recv)(socket1->get(), recv_buf, sizeof(recv_buf),
- MSG_DONTWAIT),
- SyscallFailsWithErrno(EAGAIN));
+ EXPECT_THAT(
+ RecvMsgTimeout(socket1->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ PosixErrorIs(EAGAIN, ::testing::_));
}
// Check that multicast works when the default send interface is configured by
@@ -717,9 +726,9 @@ TEST_P(IPv4UDPUnboundSocketTest, IpMulticastLoopbackIfNicSelfConnect) {
// Check that we did not receive the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- EXPECT_THAT(RetryEINTR(recv)(socket1->get(), recv_buf, sizeof(recv_buf),
- MSG_DONTWAIT),
- SyscallFailsWithErrno(EAGAIN));
+ EXPECT_THAT(
+ RecvMsgTimeout(socket1->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ PosixErrorIs(EAGAIN, ::testing::_));
}
// Check that multicast works when the default send interface is configured by
@@ -775,8 +784,9 @@ TEST_P(IPv4UDPUnboundSocketTest, IpMulticastLoopbackIfAddrSelfNoLoop) {
// Check that we received the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(RetryEINTR(recv)(socket1->get(), recv_buf, sizeof(recv_buf), 0),
- SyscallSucceedsWithValue(sizeof(recv_buf)));
+ ASSERT_THAT(
+ RecvMsgTimeout(socket1->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(recv_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
@@ -834,8 +844,9 @@ TEST_P(IPv4UDPUnboundSocketTest, IpMulticastLoopbackIfNicSelfNoLoop) {
// Check that we received the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(RetryEINTR(recv)(socket1->get(), recv_buf, sizeof(recv_buf), 0),
- SyscallSucceedsWithValue(sizeof(recv_buf)));
+ ASSERT_THAT(
+ RecvMsgTimeout(socket1->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(recv_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
@@ -907,9 +918,9 @@ TEST_P(IPv4UDPUnboundSocketTest, IpMulticastDropAddr) {
// Check that we did not receive the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- EXPECT_THAT(RetryEINTR(recv)(socket2->get(), recv_buf, sizeof(recv_buf),
- MSG_DONTWAIT),
- SyscallFailsWithErrno(EAGAIN));
+ EXPECT_THAT(
+ RecvMsgTimeout(socket2->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ PosixErrorIs(EAGAIN, ::testing::_));
}
// Check that dropping a group membership prevents multicast packets from being
@@ -965,9 +976,9 @@ TEST_P(IPv4UDPUnboundSocketTest, IpMulticastDropNic) {
// Check that we did not receive the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- EXPECT_THAT(RetryEINTR(recv)(socket2->get(), recv_buf, sizeof(recv_buf),
- MSG_DONTWAIT),
- SyscallFailsWithErrno(EAGAIN));
+ EXPECT_THAT(
+ RecvMsgTimeout(socket2->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ PosixErrorIs(EAGAIN, ::testing::_));
}
TEST_P(IPv4UDPUnboundSocketTest, IpMulticastIfZero) {
@@ -1319,9 +1330,9 @@ TEST_P(IPv4UDPUnboundSocketTest, TestMcastReceptionOnTwoSockets) {
// Check that we received the multicast packet on both sockets.
for (auto& sockets : socket_pairs) {
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(
- RetryEINTR(recv)(sockets->second_fd(), recv_buf, sizeof(recv_buf), 0),
- SyscallSucceedsWithValue(sizeof(recv_buf)));
+ ASSERT_THAT(RecvMsgTimeout(sockets->second_fd(), recv_buf,
+ sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(recv_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
}
@@ -1398,9 +1409,9 @@ TEST_P(IPv4UDPUnboundSocketTest, TestMcastReceptionWhenDroppingMemberships) {
// Check that we received the multicast packet on both sockets.
for (auto& sockets : socket_pairs) {
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(
- RetryEINTR(recv)(sockets->second_fd(), recv_buf, sizeof(recv_buf), 0),
- SyscallSucceedsWithValue(sizeof(recv_buf)));
+ ASSERT_THAT(RecvMsgTimeout(sockets->second_fd(), recv_buf,
+ sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(recv_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
}
@@ -1421,9 +1432,9 @@ TEST_P(IPv4UDPUnboundSocketTest, TestMcastReceptionWhenDroppingMemberships) {
char recv_buf[sizeof(send_buf)] = {};
for (auto& sockets : socket_pairs) {
- ASSERT_THAT(RetryEINTR(recv)(sockets->second_fd(), recv_buf,
- sizeof(recv_buf), MSG_DONTWAIT),
- SyscallFailsWithErrno(EAGAIN));
+ ASSERT_THAT(RecvMsgTimeout(sockets->second_fd(), recv_buf,
+ sizeof(recv_buf), 1 /*timeout*/),
+ PosixErrorIs(EAGAIN, ::testing::_));
}
}
}
@@ -1474,9 +1485,9 @@ TEST_P(IPv4UDPUnboundSocketTest, TestBindToMcastThenJoinThenReceive) {
// Check that we received the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(RetryEINTR(recv)(socket2->get(), recv_buf, sizeof(recv_buf),
- MSG_DONTWAIT),
- SyscallSucceedsWithValue(sizeof(recv_buf)));
+ ASSERT_THAT(
+ RecvMsgTimeout(socket2->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(recv_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
@@ -1518,9 +1529,9 @@ TEST_P(IPv4UDPUnboundSocketTest, TestBindToMcastThenNoJoinThenNoReceive) {
// Check that we don't receive the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(RetryEINTR(recv)(socket2->get(), recv_buf, sizeof(recv_buf),
- MSG_DONTWAIT),
- SyscallFailsWithErrno(EAGAIN));
+ ASSERT_THAT(
+ RecvMsgTimeout(socket2->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ PosixErrorIs(EAGAIN, ::testing::_));
}
// Check that a socket can bind to a multicast address and still send out
@@ -1568,9 +1579,9 @@ TEST_P(IPv4UDPUnboundSocketTest, TestBindToMcastThenSend) {
// Check that we received the packet.
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(RetryEINTR(recv)(socket2->get(), recv_buf, sizeof(recv_buf),
- MSG_DONTWAIT),
- SyscallSucceedsWithValue(sizeof(recv_buf)));
+ ASSERT_THAT(
+ RecvMsgTimeout(socket2->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(recv_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
@@ -1615,9 +1626,9 @@ TEST_P(IPv4UDPUnboundSocketTest, TestBindToBcastThenReceive) {
// Check that we received the multicast packet.
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(RetryEINTR(recv)(socket2->get(), recv_buf, sizeof(recv_buf),
- MSG_DONTWAIT),
- SyscallSucceedsWithValue(sizeof(recv_buf)));
+ ASSERT_THAT(
+ RecvMsgTimeout(socket2->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(recv_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
@@ -1666,9 +1677,9 @@ TEST_P(IPv4UDPUnboundSocketTest, TestBindToBcastThenSend) {
// Check that we received the packet.
char recv_buf[sizeof(send_buf)] = {};
- ASSERT_THAT(RetryEINTR(recv)(socket2->get(), recv_buf, sizeof(recv_buf),
- MSG_DONTWAIT),
- SyscallSucceedsWithValue(sizeof(recv_buf)));
+ ASSERT_THAT(
+ RecvMsgTimeout(socket2->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(recv_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
@@ -1726,17 +1737,17 @@ TEST_P(IPv4UDPUnboundSocketTest, ReuseAddrDistribution_NoRandomSave) {
// of the other sockets to have received it, but we will check that later.
char recv_buf[sizeof(send_buf)] = {};
EXPECT_THAT(
- RetryEINTR(recv)(last->get(), recv_buf, sizeof(recv_buf), MSG_DONTWAIT),
- SyscallSucceedsWithValue(sizeof(send_buf)));
+ RecvMsgTimeout(last->get(), recv_buf, sizeof(recv_buf), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(send_buf)));
EXPECT_EQ(0, memcmp(send_buf, recv_buf, sizeof(send_buf)));
}
// Verify that no other messages were received.
for (auto& socket : sockets) {
char recv_buf[kMessageSize] = {};
- EXPECT_THAT(RetryEINTR(recv)(socket->get(), recv_buf, sizeof(recv_buf),
- MSG_DONTWAIT),
- SyscallFailsWithErrno(EAGAIN));
+ EXPECT_THAT(RecvMsgTimeout(socket->get(), recv_buf, sizeof(recv_buf),
+ 1 /*timeout*/),
+ PosixErrorIs(EAGAIN, ::testing::_));
}
}
@@ -2113,12 +2124,12 @@ TEST_P(IPv4UDPUnboundSocketTest, ReuseAddrReusePortDistribution) {
// balancing (REUSEPORT) instead of the most recently bound socket
// (REUSEADDR).
char recv_buf[kMessageSize] = {};
- EXPECT_THAT(RetryEINTR(recv)(receiver1->get(), recv_buf, sizeof(recv_buf),
- MSG_DONTWAIT),
- SyscallSucceedsWithValue(kMessageSize));
- EXPECT_THAT(RetryEINTR(recv)(receiver2->get(), recv_buf, sizeof(recv_buf),
- MSG_DONTWAIT),
- SyscallSucceedsWithValue(kMessageSize));
+ EXPECT_THAT(RecvMsgTimeout(receiver1->get(), recv_buf, sizeof(recv_buf),
+ 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(kMessageSize));
+ EXPECT_THAT(RecvMsgTimeout(receiver2->get(), recv_buf, sizeof(recv_buf),
+ 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(kMessageSize));
}
// Test that socket will receive packet info control message.
diff --git a/test/syscalls/linux/socket_ipv4_udp_unbound_netlink.cc b/test/syscalls/linux/socket_ipv4_udp_unbound_netlink.cc
index 79eb48afa..49a0f06d9 100644
--- a/test/syscalls/linux/socket_ipv4_udp_unbound_netlink.cc
+++ b/test/syscalls/linux/socket_ipv4_udp_unbound_netlink.cc
@@ -15,10 +15,12 @@
#include "test/syscalls/linux/socket_ipv4_udp_unbound_netlink.h"
#include <arpa/inet.h>
+#include <poll.h>
#include "gtest/gtest.h"
#include "test/syscalls/linux/socket_netlink_route_util.h"
#include "test/util/capability_util.h"
+#include "test/util/cleanup.h"
namespace gvisor {
namespace testing {
@@ -33,9 +35,23 @@ TEST_P(IPv4UDPUnboundSocketNetlinkTest, JoinSubnet) {
// Add an IP address to the loopback interface.
Link loopback_link = ASSERT_NO_ERRNO_AND_VALUE(LoopbackLink());
struct in_addr addr;
- EXPECT_EQ(1, inet_pton(AF_INET, "192.0.2.1", &addr));
- EXPECT_NO_ERRNO(LinkAddLocalAddr(loopback_link.index, AF_INET,
+ ASSERT_EQ(1, inet_pton(AF_INET, "192.0.2.1", &addr));
+ ASSERT_NO_ERRNO(LinkAddLocalAddr(loopback_link.index, AF_INET,
/*prefixlen=*/24, &addr, sizeof(addr)));
+ Cleanup defer_addr_removal = Cleanup(
+ [loopback_link = std::move(loopback_link), addr = std::move(addr)] {
+ if (IsRunningOnGvisor()) {
+ // TODO(gvisor.dev/issue/3921): Remove this once deleting addresses
+ // via netlink is supported.
+ EXPECT_THAT(LinkDelLocalAddr(loopback_link.index, AF_INET,
+ /*prefixlen=*/24, &addr, sizeof(addr)),
+ PosixErrorIs(EOPNOTSUPP, ::testing::_));
+ } else {
+ EXPECT_NO_ERRNO(LinkDelLocalAddr(loopback_link.index, AF_INET,
+ /*prefixlen=*/24, &addr,
+ sizeof(addr)));
+ }
+ });
auto snd_sock = ASSERT_NO_ERRNO_AND_VALUE(NewSocket());
auto rcv_sock = ASSERT_NO_ERRNO_AND_VALUE(NewSocket());
@@ -45,10 +61,10 @@ TEST_P(IPv4UDPUnboundSocketNetlinkTest, JoinSubnet) {
TestAddress sender_addr("V4NotAssignd1");
sender_addr.addr.ss_family = AF_INET;
sender_addr.addr_len = sizeof(sockaddr_in);
- EXPECT_EQ(1, inet_pton(AF_INET, "192.0.2.2",
+ ASSERT_EQ(1, inet_pton(AF_INET, "192.0.2.2",
&(reinterpret_cast<sockaddr_in*>(&sender_addr.addr)
->sin_addr.s_addr)));
- EXPECT_THAT(
+ ASSERT_THAT(
bind(snd_sock->get(), reinterpret_cast<sockaddr*>(&sender_addr.addr),
sender_addr.addr_len),
SyscallSucceeds());
@@ -58,10 +74,10 @@ TEST_P(IPv4UDPUnboundSocketNetlinkTest, JoinSubnet) {
TestAddress receiver_addr("V4NotAssigned2");
receiver_addr.addr.ss_family = AF_INET;
receiver_addr.addr_len = sizeof(sockaddr_in);
- EXPECT_EQ(1, inet_pton(AF_INET, "192.0.2.254",
+ ASSERT_EQ(1, inet_pton(AF_INET, "192.0.2.254",
&(reinterpret_cast<sockaddr_in*>(&receiver_addr.addr)
->sin_addr.s_addr)));
- EXPECT_THAT(
+ ASSERT_THAT(
bind(rcv_sock->get(), reinterpret_cast<sockaddr*>(&receiver_addr.addr),
receiver_addr.addr_len),
SyscallSucceeds());
@@ -70,10 +86,10 @@ TEST_P(IPv4UDPUnboundSocketNetlinkTest, JoinSubnet) {
reinterpret_cast<sockaddr*>(&receiver_addr.addr),
&receiver_addr_len),
SyscallSucceeds());
- EXPECT_EQ(receiver_addr_len, receiver_addr.addr_len);
+ ASSERT_EQ(receiver_addr_len, receiver_addr.addr_len);
char send_buf[kSendBufSize];
RandomizeBuffer(send_buf, kSendBufSize);
- EXPECT_THAT(
+ ASSERT_THAT(
RetryEINTR(sendto)(snd_sock->get(), send_buf, kSendBufSize, 0,
reinterpret_cast<sockaddr*>(&receiver_addr.addr),
receiver_addr.addr_len),
@@ -83,7 +99,126 @@ TEST_P(IPv4UDPUnboundSocketNetlinkTest, JoinSubnet) {
char recv_buf[kSendBufSize] = {};
ASSERT_THAT(RetryEINTR(recv)(rcv_sock->get(), recv_buf, kSendBufSize, 0),
SyscallSucceedsWithValue(kSendBufSize));
- EXPECT_EQ(0, memcmp(send_buf, recv_buf, kSendBufSize));
+ ASSERT_EQ(0, memcmp(send_buf, recv_buf, kSendBufSize));
+}
+
+// Tests that broadcast packets are delivered to all interested sockets
+// (wildcard and broadcast address specified sockets).
+//
+// Note, we cannot test the IPv4 Broadcast (255.255.255.255) because we do
+// not have a route to it.
+TEST_P(IPv4UDPUnboundSocketNetlinkTest, ReuseAddrSubnetDirectedBroadcast) {
+ constexpr uint16_t kPort = 9876;
+ // Wait up to 20 seconds for the data.
+ constexpr int kPollTimeoutMs = 20000;
+ // Number of sockets per socket type.
+ constexpr int kNumSocketsPerType = 2;
+
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_ADMIN)));
+
+ // Add an IP address to the loopback interface.
+ Link loopback_link = ASSERT_NO_ERRNO_AND_VALUE(LoopbackLink());
+ struct in_addr addr;
+ ASSERT_EQ(1, inet_pton(AF_INET, "192.0.2.1", &addr));
+ ASSERT_NO_ERRNO(LinkAddLocalAddr(loopback_link.index, AF_INET,
+ 24 /* prefixlen */, &addr, sizeof(addr)));
+ Cleanup defer_addr_removal = Cleanup(
+ [loopback_link = std::move(loopback_link), addr = std::move(addr)] {
+ if (IsRunningOnGvisor()) {
+ // TODO(gvisor.dev/issue/3921): Remove this once deleting addresses
+ // via netlink is supported.
+ EXPECT_THAT(LinkDelLocalAddr(loopback_link.index, AF_INET,
+ /*prefixlen=*/24, &addr, sizeof(addr)),
+ PosixErrorIs(EOPNOTSUPP, ::testing::_));
+ } else {
+ EXPECT_NO_ERRNO(LinkDelLocalAddr(loopback_link.index, AF_INET,
+ /*prefixlen=*/24, &addr,
+ sizeof(addr)));
+ }
+ });
+
+ TestAddress broadcast_address("SubnetBroadcastAddress");
+ broadcast_address.addr.ss_family = AF_INET;
+ broadcast_address.addr_len = sizeof(sockaddr_in);
+ auto broadcast_address_in =
+ reinterpret_cast<sockaddr_in*>(&broadcast_address.addr);
+ ASSERT_EQ(1, inet_pton(AF_INET, "192.0.2.255",
+ &broadcast_address_in->sin_addr.s_addr));
+ broadcast_address_in->sin_port = htons(kPort);
+
+ TestAddress any_address = V4Any();
+ reinterpret_cast<sockaddr_in*>(&any_address.addr)->sin_port = htons(kPort);
+
+ // We create sockets bound to both the wildcard address and the broadcast
+ // address to make sure both of these types of "broadcast interested" sockets
+ // receive broadcast packets.
+ std::vector<std::unique_ptr<FileDescriptor>> socks;
+ for (bool bind_wildcard : {false, true}) {
+ // Create multiple sockets for each type of "broadcast interested"
+ // socket so we can test that all sockets receive the broadcast packet.
+ for (int i = 0; i < kNumSocketsPerType; i++) {
+ auto sock = ASSERT_NO_ERRNO_AND_VALUE(NewSocket());
+ auto idx = socks.size();
+
+ ASSERT_THAT(setsockopt(sock->get(), SOL_SOCKET, SO_REUSEADDR, &kSockOptOn,
+ sizeof(kSockOptOn)),
+ SyscallSucceedsWithValue(0))
+ << "socks[" << idx << "]";
+
+ ASSERT_THAT(setsockopt(sock->get(), SOL_SOCKET, SO_BROADCAST, &kSockOptOn,
+ sizeof(kSockOptOn)),
+ SyscallSucceedsWithValue(0))
+ << "socks[" << idx << "]";
+
+ if (bind_wildcard) {
+ ASSERT_THAT(
+ bind(sock->get(), reinterpret_cast<sockaddr*>(&any_address.addr),
+ any_address.addr_len),
+ SyscallSucceeds())
+ << "socks[" << idx << "]";
+ } else {
+ ASSERT_THAT(bind(sock->get(),
+ reinterpret_cast<sockaddr*>(&broadcast_address.addr),
+ broadcast_address.addr_len),
+ SyscallSucceeds())
+ << "socks[" << idx << "]";
+ }
+
+ socks.push_back(std::move(sock));
+ }
+ }
+
+ char send_buf[kSendBufSize];
+ RandomizeBuffer(send_buf, kSendBufSize);
+
+ // Broadcasts from each socket should be received by every socket (including
+ // the sending socket).
+ for (int w = 0; w < socks.size(); w++) {
+ auto& w_sock = socks[w];
+ ASSERT_THAT(
+ RetryEINTR(sendto)(w_sock->get(), send_buf, kSendBufSize, 0,
+ reinterpret_cast<sockaddr*>(&broadcast_address.addr),
+ broadcast_address.addr_len),
+ SyscallSucceedsWithValue(kSendBufSize))
+ << "write socks[" << w << "]";
+
+ // Check that we received the packet on all sockets.
+ for (int r = 0; r < socks.size(); r++) {
+ auto& r_sock = socks[r];
+
+ struct pollfd poll_fd = {r_sock->get(), POLLIN, 0};
+ EXPECT_THAT(RetryEINTR(poll)(&poll_fd, 1, kPollTimeoutMs),
+ SyscallSucceedsWithValue(1))
+ << "write socks[" << w << "] & read socks[" << r << "]";
+
+ char recv_buf[kSendBufSize] = {};
+ EXPECT_THAT(RetryEINTR(recv)(r_sock->get(), recv_buf, kSendBufSize, 0),
+ SyscallSucceedsWithValue(kSendBufSize))
+ << "write socks[" << w << "] & read socks[" << r << "]";
+ EXPECT_EQ(0, memcmp(send_buf, recv_buf, kSendBufSize))
+ << "write socks[" << w << "] & read socks[" << r << "]";
+ }
+ }
}
} // namespace testing
diff --git a/test/syscalls/linux/socket_netlink_route_util.cc b/test/syscalls/linux/socket_netlink_route_util.cc
index bde1dbb4d..7a0bad4cb 100644
--- a/test/syscalls/linux/socket_netlink_route_util.cc
+++ b/test/syscalls/linux/socket_netlink_route_util.cc
@@ -26,6 +26,62 @@ namespace {
constexpr uint32_t kSeq = 12345;
+// Types of address modifications that may be performed on an interface.
+enum class LinkAddrModification {
+ kAdd,
+ kDelete,
+};
+
+// Populates |hdr| with appripriate values for the modification type.
+PosixError PopulateNlmsghdr(LinkAddrModification modification,
+ struct nlmsghdr* hdr) {
+ switch (modification) {
+ case LinkAddrModification::kAdd:
+ hdr->nlmsg_type = RTM_NEWADDR;
+ hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+ return NoError();
+ case LinkAddrModification::kDelete:
+ hdr->nlmsg_type = RTM_DELADDR;
+ hdr->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
+ return NoError();
+ }
+
+ return PosixError(EINVAL);
+}
+
+// Adds or removes the specified address from the specified interface.
+PosixError LinkModifyLocalAddr(int index, int family, int prefixlen,
+ const void* addr, int addrlen,
+ LinkAddrModification modification) {
+ ASSIGN_OR_RETURN_ERRNO(FileDescriptor fd, NetlinkBoundSocket(NETLINK_ROUTE));
+
+ struct request {
+ struct nlmsghdr hdr;
+ struct ifaddrmsg ifaddr;
+ char attrbuf[512];
+ };
+
+ struct request req = {};
+ PosixError err = PopulateNlmsghdr(modification, &req.hdr);
+ if (!err.ok()) {
+ return err;
+ }
+ req.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(req.ifaddr));
+ req.hdr.nlmsg_seq = kSeq;
+ req.ifaddr.ifa_index = index;
+ req.ifaddr.ifa_family = family;
+ req.ifaddr.ifa_prefixlen = prefixlen;
+
+ struct rtattr* rta = reinterpret_cast<struct rtattr*>(
+ reinterpret_cast<int8_t*>(&req) + NLMSG_ALIGN(req.hdr.nlmsg_len));
+ rta->rta_type = IFA_LOCAL;
+ rta->rta_len = RTA_LENGTH(addrlen);
+ req.hdr.nlmsg_len = NLMSG_ALIGN(req.hdr.nlmsg_len) + RTA_LENGTH(addrlen);
+ memcpy(RTA_DATA(rta), addr, addrlen);
+
+ return NetlinkRequestAckOrError(fd, kSeq, &req, req.hdr.nlmsg_len);
+}
+
} // namespace
PosixError DumpLinks(
@@ -84,31 +140,14 @@ PosixErrorOr<Link> LoopbackLink() {
PosixError LinkAddLocalAddr(int index, int family, int prefixlen,
const void* addr, int addrlen) {
- ASSIGN_OR_RETURN_ERRNO(FileDescriptor fd, NetlinkBoundSocket(NETLINK_ROUTE));
-
- struct request {
- struct nlmsghdr hdr;
- struct ifaddrmsg ifaddr;
- char attrbuf[512];
- };
-
- struct request req = {};
- req.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(req.ifaddr));
- req.hdr.nlmsg_type = RTM_NEWADDR;
- req.hdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
- req.hdr.nlmsg_seq = kSeq;
- req.ifaddr.ifa_index = index;
- req.ifaddr.ifa_family = family;
- req.ifaddr.ifa_prefixlen = prefixlen;
-
- struct rtattr* rta = reinterpret_cast<struct rtattr*>(
- reinterpret_cast<int8_t*>(&req) + NLMSG_ALIGN(req.hdr.nlmsg_len));
- rta->rta_type = IFA_LOCAL;
- rta->rta_len = RTA_LENGTH(addrlen);
- req.hdr.nlmsg_len = NLMSG_ALIGN(req.hdr.nlmsg_len) + RTA_LENGTH(addrlen);
- memcpy(RTA_DATA(rta), addr, addrlen);
+ return LinkModifyLocalAddr(index, family, prefixlen, addr, addrlen,
+ LinkAddrModification::kAdd);
+}
- return NetlinkRequestAckOrError(fd, kSeq, &req, req.hdr.nlmsg_len);
+PosixError LinkDelLocalAddr(int index, int family, int prefixlen,
+ const void* addr, int addrlen) {
+ return LinkModifyLocalAddr(index, family, prefixlen, addr, addrlen,
+ LinkAddrModification::kDelete);
}
PosixError LinkChangeFlags(int index, unsigned int flags, unsigned int change) {
diff --git a/test/syscalls/linux/socket_netlink_route_util.h b/test/syscalls/linux/socket_netlink_route_util.h
index 149c4a7f6..e5badca70 100644
--- a/test/syscalls/linux/socket_netlink_route_util.h
+++ b/test/syscalls/linux/socket_netlink_route_util.h
@@ -43,6 +43,10 @@ PosixErrorOr<Link> LoopbackLink();
PosixError LinkAddLocalAddr(int index, int family, int prefixlen,
const void* addr, int addrlen);
+// LinkDelLocalAddr removes IFA_LOCAL attribute on the interface.
+PosixError LinkDelLocalAddr(int index, int family, int prefixlen,
+ const void* addr, int addrlen);
+
// LinkChangeFlags changes interface flags. E.g. IFF_UP.
PosixError LinkChangeFlags(int index, unsigned int flags, unsigned int change);
diff --git a/test/syscalls/linux/socket_test_util.cc b/test/syscalls/linux/socket_test_util.cc
index 53b678e94..e11792309 100644
--- a/test/syscalls/linux/socket_test_util.cc
+++ b/test/syscalls/linux/socket_test_util.cc
@@ -753,6 +753,20 @@ PosixErrorOr<int> SendMsg(int sock, msghdr* msg, char buf[], int buf_size) {
return ret;
}
+PosixErrorOr<int> RecvMsgTimeout(int sock, char buf[], int buf_size,
+ int timeout) {
+ fd_set rfd;
+ struct timeval to = {.tv_sec = timeout, .tv_usec = 0};
+ FD_ZERO(&rfd);
+ FD_SET(sock, &rfd);
+
+ int ret;
+ RETURN_ERROR_IF_SYSCALL_FAIL(ret = select(1, &rfd, NULL, NULL, &to));
+ RETURN_ERROR_IF_SYSCALL_FAIL(
+ ret = RetryEINTR(recv)(sock, buf, buf_size, MSG_DONTWAIT));
+ return ret;
+}
+
void RecvNoData(int sock) {
char data = 0;
struct iovec iov;
diff --git a/test/syscalls/linux/socket_test_util.h b/test/syscalls/linux/socket_test_util.h
index 734b48b96..468bc96e0 100644
--- a/test/syscalls/linux/socket_test_util.h
+++ b/test/syscalls/linux/socket_test_util.h
@@ -467,6 +467,10 @@ PosixError FreeAvailablePort(int port);
// SendMsg converts a buffer to an iovec and adds it to msg before sending it.
PosixErrorOr<int> SendMsg(int sock, msghdr* msg, char buf[], int buf_size);
+// RecvMsgTimeout calls select on sock with timeout and then calls recv on sock.
+PosixErrorOr<int> RecvMsgTimeout(int sock, char buf[], int buf_size,
+ int timeout);
+
// RecvNoData checks that no data is receivable on sock.
void RecvNoData(int sock);
diff --git a/test/syscalls/linux/socket_unix_stream.cc b/test/syscalls/linux/socket_unix_stream.cc
index 99e77b89e..1edcb15a7 100644
--- a/test/syscalls/linux/socket_unix_stream.cc
+++ b/test/syscalls/linux/socket_unix_stream.cc
@@ -103,6 +103,24 @@ TEST_P(StreamUnixSocketPairTest, Sendto) {
SyscallFailsWithErrno(EISCONN));
}
+TEST_P(StreamUnixSocketPairTest, SetAndGetSocketLinger) {
+ auto sockets = ASSERT_NO_ERRNO_AND_VALUE(NewSocketPair());
+
+ struct linger sl = {1, 5};
+ EXPECT_THAT(
+ setsockopt(sockets->first_fd(), SOL_SOCKET, SO_LINGER, &sl, sizeof(sl)),
+ SyscallSucceedsWithValue(0));
+
+ struct linger got_linger = {};
+ socklen_t length = sizeof(sl);
+ EXPECT_THAT(getsockopt(sockets->first_fd(), SOL_SOCKET, SO_LINGER,
+ &got_linger, &length),
+ SyscallSucceedsWithValue(0));
+
+ ASSERT_EQ(length, sizeof(got_linger));
+ EXPECT_EQ(0, memcmp(&got_linger, &sl, length));
+}
+
INSTANTIATE_TEST_SUITE_P(
AllUnixDomainSockets, StreamUnixSocketPairTest,
::testing::ValuesIn(IncludeReversals(VecCat<SocketPairKind>(
diff --git a/test/syscalls/linux/stat.cc b/test/syscalls/linux/stat.cc
index 1b2941c37..92260b1e1 100644
--- a/test/syscalls/linux/stat.cc
+++ b/test/syscalls/linux/stat.cc
@@ -333,20 +333,23 @@ TEST_F(StatTest, LeadingDoubleSlash) {
// Test that a rename doesn't change the underlying file.
TEST_F(StatTest, StatDoesntChangeAfterRename) {
- const TempPath old_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ const TempPath old_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
const TempPath new_path(NewTempAbsPath());
struct stat st_old = {};
struct stat st_new = {};
- ASSERT_THAT(stat(old_dir.path().c_str(), &st_old), SyscallSucceeds());
- ASSERT_THAT(rename(old_dir.path().c_str(), new_path.path().c_str()),
+ ASSERT_THAT(stat(old_file.path().c_str(), &st_old), SyscallSucceeds());
+ ASSERT_THAT(rename(old_file.path().c_str(), new_path.path().c_str()),
SyscallSucceeds());
ASSERT_THAT(stat(new_path.path().c_str(), &st_new), SyscallSucceeds());
EXPECT_EQ(st_old.st_nlink, st_new.st_nlink);
EXPECT_EQ(st_old.st_dev, st_new.st_dev);
- EXPECT_EQ(st_old.st_ino, st_new.st_ino);
+ // Overlay filesystems may synthesize directory inode numbers on the fly.
+ if (!ASSERT_NO_ERRNO_AND_VALUE(IsOverlayfs(GetAbsoluteTestTmpdir()))) {
+ EXPECT_EQ(st_old.st_ino, st_new.st_ino);
+ }
EXPECT_EQ(st_old.st_mode, st_new.st_mode);
EXPECT_EQ(st_old.st_uid, st_new.st_uid);
EXPECT_EQ(st_old.st_gid, st_new.st_gid);
@@ -383,7 +386,9 @@ TEST_F(StatTest, LinkCountsWithRegularFileChild) {
// This test verifies that inodes remain around when there is an open fd
// after link count hits 0.
-TEST_F(StatTest, ZeroLinksOpenFdRegularFileChild_NoRandomSave) {
+//
+// It is marked NoSave because we don't support saving unlinked files.
+TEST_F(StatTest, ZeroLinksOpenFdRegularFileChild_NoSave) {
// Setting the enviornment variable GVISOR_GOFER_UNCACHED to any value
// will prevent this test from running, see the tmpfs lifecycle.
//
@@ -392,9 +397,6 @@ TEST_F(StatTest, ZeroLinksOpenFdRegularFileChild_NoRandomSave) {
const char* uncached_gofer = getenv("GVISOR_GOFER_UNCACHED");
SKIP_IF(uncached_gofer != nullptr);
- // We don't support saving unlinked files.
- const DisableSave ds;
-
const TempPath dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
const TempPath child = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith(
dir.path(), "hello", TempPath::kDefaultFileMode));
diff --git a/test/syscalls/linux/tcp_socket.cc b/test/syscalls/linux/tcp_socket.cc
index ab731db1d..e0981e28a 100644
--- a/test/syscalls/linux/tcp_socket.cc
+++ b/test/syscalls/linux/tcp_socket.cc
@@ -1643,6 +1643,36 @@ TEST_P(SimpleTcpSocketTest, GetSocketDetachFilter) {
SyscallFailsWithErrno(ENOPROTOOPT));
}
+TEST_P(SimpleTcpSocketTest, CloseNonConnectedLingerOption) {
+ FileDescriptor s =
+ ASSERT_NO_ERRNO_AND_VALUE(Socket(GetParam(), SOCK_STREAM, IPPROTO_TCP));
+
+ constexpr int kLingerTimeout = 10; // Seconds.
+
+ // Set the SO_LINGER option.
+ struct linger sl = {
+ .l_onoff = 1,
+ .l_linger = kLingerTimeout,
+ };
+ ASSERT_THAT(setsockopt(s.get(), SOL_SOCKET, SO_LINGER, &sl, sizeof(sl)),
+ SyscallSucceeds());
+
+ struct pollfd poll_fd = {
+ .fd = s.get(),
+ .events = POLLHUP,
+ };
+ constexpr int kPollTimeoutMs = 0;
+ ASSERT_THAT(RetryEINTR(poll)(&poll_fd, 1, kPollTimeoutMs),
+ SyscallSucceedsWithValue(1));
+
+ auto const start_time = absl::Now();
+ EXPECT_THAT(close(s.release()), SyscallSucceeds());
+ auto const end_time = absl::Now();
+
+ // Close() should not linger and return immediately.
+ ASSERT_LT((end_time - start_time), absl::Seconds(kLingerTimeout));
+}
+
INSTANTIATE_TEST_SUITE_P(AllInetTests, SimpleTcpSocketTest,
::testing::Values(AF_INET, AF_INET6));
diff --git a/test/syscalls/linux/truncate.cc b/test/syscalls/linux/truncate.cc
index c988c6380..bfc95ed38 100644
--- a/test/syscalls/linux/truncate.cc
+++ b/test/syscalls/linux/truncate.cc
@@ -196,6 +196,26 @@ TEST(TruncateTest, FtruncateNonWriteable) {
EXPECT_THAT(ftruncate(fd.get(), 0), SyscallFailsWithErrno(EINVAL));
}
+// ftruncate(2) should succeed as long as the file descriptor is writeable,
+// regardless of whether the file permissions allow writing.
+TEST(TruncateTest, FtruncateWithoutWritePermission_NoRandomSave) {
+ // Drop capabilities that allow us to override file permissions.
+ ASSERT_NO_ERRNO(SetCapability(CAP_DAC_OVERRIDE, false));
+
+ // The only time we can open a file with flags forbidden by its permissions
+ // is when we are creating the file. We cannot re-open with the same flags,
+ // so we cannot restore an fd obtained from such an operation.
+ const DisableSave ds;
+ auto path = NewTempAbsPath();
+ const FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(path, O_RDWR | O_CREAT, 0444));
+
+ // In goferfs, ftruncate may be converted to a remote truncate operation that
+ // unavoidably requires write permission.
+ SKIP_IF(IsRunningOnGvisor() && !ASSERT_NO_ERRNO_AND_VALUE(IsTmpfs(path)));
+ ASSERT_THAT(ftruncate(fd.get(), 100), SyscallSucceeds());
+}
+
TEST(TruncateTest, TruncateNonExist) {
EXPECT_THAT(truncate("/foo/bar", 0), SyscallFailsWithErrno(ENOENT));
}
diff --git a/test/syscalls/linux/udp_socket.cc b/test/syscalls/linux/udp_socket.cc
index 97db2b321..1a7673317 100644
--- a/test/syscalls/linux/udp_socket.cc
+++ b/test/syscalls/linux/udp_socket.cc
@@ -14,6 +14,9 @@
#include <arpa/inet.h>
#include <fcntl.h>
+
+#include <ctime>
+
#ifdef __linux__
#include <linux/errqueue.h>
#include <linux/filter.h>
@@ -834,8 +837,9 @@ TEST_P(UdpSocketTest, ReceiveBeforeConnect) {
// Receive the data. It works because it was sent before the connect.
char received[sizeof(buf)];
- EXPECT_THAT(recv(bind_.get(), received, sizeof(received), 0),
- SyscallSucceedsWithValue(sizeof(received)));
+ EXPECT_THAT(
+ RecvMsgTimeout(bind_.get(), received, sizeof(received), 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(sizeof(received)));
EXPECT_EQ(memcmp(buf, received, sizeof(buf)), 0);
// Send again. This time it should not be received.
@@ -924,7 +928,9 @@ TEST_P(UdpSocketTest, ReadShutdownNonblockPendingData) {
SyscallSucceedsWithValue(1));
// We should get the data even though read has been shutdown.
- EXPECT_THAT(recv(bind_.get(), received, 2, 0), SyscallSucceedsWithValue(2));
+ EXPECT_THAT(
+ RecvMsgTimeout(bind_.get(), received, 2 /*buf_size*/, 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(2));
// Because we read less than the entire packet length, since it's a packet
// based socket any subsequent reads should return EWOULDBLOCK.
@@ -1692,9 +1698,9 @@ TEST_P(UdpSocketTest, RecvBufLimitsEmptyRcvBuf) {
sendto(sock_.get(), buf.data(), buf.size(), 0, bind_addr_, addrlen_),
SyscallSucceedsWithValue(buf.size()));
std::vector<char> received(buf.size());
- EXPECT_THAT(
- recv(bind_.get(), received.data(), received.size(), MSG_DONTWAIT),
- SyscallSucceedsWithValue(received.size()));
+ EXPECT_THAT(RecvMsgTimeout(bind_.get(), received.data(), received.size(),
+ 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(received.size()));
}
{
@@ -1708,9 +1714,9 @@ TEST_P(UdpSocketTest, RecvBufLimitsEmptyRcvBuf) {
SyscallSucceedsWithValue(buf.size()));
std::vector<char> received(buf.size());
- EXPECT_THAT(
- recv(bind_.get(), received.data(), received.size(), MSG_DONTWAIT),
- SyscallSucceedsWithValue(received.size()));
+ ASSERT_THAT(RecvMsgTimeout(bind_.get(), received.data(), received.size(),
+ 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(received.size()));
}
}
@@ -1779,9 +1785,9 @@ TEST_P(UdpSocketTest, RecvBufLimits) {
for (int i = 0; i < sent - 1; i++) {
// Receive the data.
std::vector<char> received(buf.size());
- EXPECT_THAT(
- recv(bind_.get(), received.data(), received.size(), MSG_DONTWAIT),
- SyscallSucceedsWithValue(received.size()));
+ EXPECT_THAT(RecvMsgTimeout(bind_.get(), received.data(), received.size(),
+ 1 /*timeout*/),
+ IsPosixErrorOkAndHolds(received.size()));
EXPECT_EQ(memcmp(buf.data(), received.data(), buf.size()), 0);
}
diff --git a/test/syscalls/linux/vdso_clock_gettime.cc b/test/syscalls/linux/vdso_clock_gettime.cc
index ce1899f45..2a8699a7b 100644
--- a/test/syscalls/linux/vdso_clock_gettime.cc
+++ b/test/syscalls/linux/vdso_clock_gettime.cc
@@ -38,8 +38,6 @@ std::string PrintClockId(::testing::TestParamInfo<clockid_t> info) {
switch (info.param) {
case CLOCK_MONOTONIC:
return "CLOCK_MONOTONIC";
- case CLOCK_REALTIME:
- return "CLOCK_REALTIME";
case CLOCK_BOOTTIME:
return "CLOCK_BOOTTIME";
default:
@@ -47,59 +45,36 @@ std::string PrintClockId(::testing::TestParamInfo<clockid_t> info) {
}
}
-class CorrectVDSOClockTest : public ::testing::TestWithParam<clockid_t> {};
+class MonotonicVDSOClockTest : public ::testing::TestWithParam<clockid_t> {};
-TEST_P(CorrectVDSOClockTest, IsCorrect) {
+TEST_P(MonotonicVDSOClockTest, IsCorrect) {
+ // The VDSO implementation of clock_gettime() uses the TSC. On KVM, sentry and
+ // application TSCs can be very desynchronized; see
+ // sentry/platform/kvm/kvm.vCPU.setSystemTime().
+ SKIP_IF(GvisorPlatform() == Platform::kKVM);
+
+ // Check that when we alternate readings from the clock_gettime syscall and
+ // the VDSO's implementation, we observe the combined sequence as being
+ // monotonic.
struct timespec tvdso, tsys;
absl::Time vdso_time, sys_time;
- uint64_t total_calls = 0;
-
- // It is expected that 82.5% of clock_gettime calls will be less than 100us
- // skewed from the system time.
- // Unfortunately this is not only influenced by the VDSO clock skew, but also
- // by arbitrary scheduling delays and the like. The test is therefore
- // regularly disabled.
- std::map<absl::Duration, std::tuple<double, uint64_t, uint64_t>> confidence =
- {
- {absl::Microseconds(100), std::make_tuple(0.825, 0, 0)},
- {absl::Microseconds(250), std::make_tuple(0.94, 0, 0)},
- {absl::Milliseconds(1), std::make_tuple(0.999, 0, 0)},
- };
-
- absl::Time start = absl::Now();
- while (absl::Now() < start + absl::Seconds(30)) {
- EXPECT_THAT(clock_gettime(GetParam(), &tvdso), SyscallSucceeds());
- EXPECT_THAT(syscall(__NR_clock_gettime, GetParam(), &tsys),
- SyscallSucceeds());
-
+ ASSERT_THAT(syscall(__NR_clock_gettime, GetParam(), &tsys),
+ SyscallSucceeds());
+ sys_time = absl::TimeFromTimespec(tsys);
+ auto end = absl::Now() + absl::Seconds(10);
+ while (absl::Now() < end) {
+ ASSERT_THAT(clock_gettime(GetParam(), &tvdso), SyscallSucceeds());
vdso_time = absl::TimeFromTimespec(tvdso);
-
- for (auto const& conf : confidence) {
- std::get<1>(confidence[conf.first]) +=
- (sys_time - vdso_time) < conf.first;
- }
-
+ EXPECT_LE(sys_time, vdso_time);
+ ASSERT_THAT(syscall(__NR_clock_gettime, GetParam(), &tsys),
+ SyscallSucceeds());
sys_time = absl::TimeFromTimespec(tsys);
-
- for (auto const& conf : confidence) {
- std::get<2>(confidence[conf.first]) +=
- (vdso_time - sys_time) < conf.first;
- }
-
- ++total_calls;
- }
-
- for (auto const& conf : confidence) {
- EXPECT_GE(std::get<1>(conf.second) / static_cast<double>(total_calls),
- std::get<0>(conf.second));
- EXPECT_GE(std::get<2>(conf.second) / static_cast<double>(total_calls),
- std::get<0>(conf.second));
+ EXPECT_LE(vdso_time, sys_time);
}
}
-INSTANTIATE_TEST_SUITE_P(ClockGettime, CorrectVDSOClockTest,
- ::testing::Values(CLOCK_MONOTONIC, CLOCK_REALTIME,
- CLOCK_BOOTTIME),
+INSTANTIATE_TEST_SUITE_P(ClockGettime, MonotonicVDSOClockTest,
+ ::testing::Values(CLOCK_MONOTONIC, CLOCK_BOOTTIME),
PrintClockId);
} // namespace
diff --git a/test/util/BUILD b/test/util/BUILD
index 2a17c33ee..26c2b6a2f 100644
--- a/test/util/BUILD
+++ b/test/util/BUILD
@@ -1,4 +1,4 @@
-load("//tools:defs.bzl", "cc_library", "cc_test", "gbenchmark", "gtest", "select_system")
+load("//tools:defs.bzl", "cc_library", "cc_test", "coreutil", "gbenchmark", "gtest", "select_system")
package(
default_visibility = ["//:sandbox"],
@@ -46,6 +46,13 @@ cc_library(
)
cc_library(
+ name = "fuse_util",
+ testonly = 1,
+ srcs = ["fuse_util.cc"],
+ hdrs = ["fuse_util.h"],
+)
+
+cc_library(
name = "proc_util",
testonly = 1,
srcs = ["proc_util.cc"],
@@ -247,7 +254,7 @@ cc_library(
],
hdrs = ["test_util.h"],
defines = select_system(),
- deps = [
+ deps = coreutil() + [
":fs_util",
":logging",
":posix_error",
diff --git a/test/util/fuse_util.cc b/test/util/fuse_util.cc
new file mode 100644
index 000000000..027f8386c
--- /dev/null
+++ b/test/util/fuse_util.cc
@@ -0,0 +1,63 @@
+// Copyright 2020 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/fuse_util.h"
+
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <string>
+
+namespace gvisor {
+namespace testing {
+
+// Create a default FuseAttr struct with specified mode, inode, and size.
+fuse_attr DefaultFuseAttr(mode_t mode, uint64_t inode, uint64_t size) {
+ const int time_sec = 1595436289;
+ const int time_nsec = 134150844;
+ return (struct fuse_attr){
+ .ino = inode,
+ .size = size,
+ .blocks = 4,
+ .atime = time_sec,
+ .mtime = time_sec,
+ .ctime = time_sec,
+ .atimensec = time_nsec,
+ .mtimensec = time_nsec,
+ .ctimensec = time_nsec,
+ .mode = mode,
+ .nlink = 2,
+ .uid = 1234,
+ .gid = 4321,
+ .rdev = 12,
+ .blksize = 4096,
+ };
+}
+
+// Create response body with specified mode, nodeID, and size.
+fuse_entry_out DefaultEntryOut(mode_t mode, uint64_t node_id, uint64_t size) {
+ struct fuse_entry_out default_entry_out = {
+ .nodeid = node_id,
+ .generation = 0,
+ .entry_valid = 0,
+ .attr_valid = 0,
+ .entry_valid_nsec = 0,
+ .attr_valid_nsec = 0,
+ .attr = DefaultFuseAttr(mode, node_id, size),
+ };
+ return default_entry_out;
+}
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/util/fuse_util.h b/test/util/fuse_util.h
new file mode 100644
index 000000000..544fe1b38
--- /dev/null
+++ b/test/util/fuse_util.h
@@ -0,0 +1,75 @@
+// Copyright 2020 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_UTIL_FUSE_UTIL_H_
+#define GVISOR_TEST_UTIL_FUSE_UTIL_H_
+
+#include <linux/fuse.h>
+#include <sys/uio.h>
+
+#include <string>
+#include <vector>
+
+namespace gvisor {
+namespace testing {
+
+// The fundamental generation function with a single argument. If passed by
+// std::string or std::vector<char>, it will call specialized versions as
+// implemented below.
+template <typename T>
+std::vector<struct iovec> FuseGenerateIovecs(T &first) {
+ return {(struct iovec){.iov_base = &first, .iov_len = sizeof(first)}};
+}
+
+// If an argument is of type std::string, it must be used in read-only scenario.
+// Because we are setting up iovec, which contains the original address of a
+// data structure, we have to drop const qualification. Usually used with
+// variable-length payload data.
+template <typename T = std::string>
+std::vector<struct iovec> FuseGenerateIovecs(std::string &first) {
+ // Pad one byte for null-terminate c-string.
+ return {(struct iovec){.iov_base = const_cast<char *>(first.c_str()),
+ .iov_len = first.size() + 1}};
+}
+
+// If an argument is of type std::vector<char>, it must be used in write-only
+// scenario and the size of the variable must be greater than or equal to the
+// size of the expected data. Usually used with variable-length payload data.
+template <typename T = std::vector<char>>
+std::vector<struct iovec> FuseGenerateIovecs(std::vector<char> &first) {
+ return {(struct iovec){.iov_base = first.data(), .iov_len = first.size()}};
+}
+
+// A helper function to set up an array of iovec struct for testing purpose.
+// Use variadic class template to generalize different numbers and different
+// types of FUSE structs.
+template <typename T, typename... Types>
+std::vector<struct iovec> FuseGenerateIovecs(T &first, Types &...args) {
+ auto first_iovec = FuseGenerateIovecs(first);
+ auto iovecs = FuseGenerateIovecs(args...);
+ first_iovec.insert(std::end(first_iovec), std::begin(iovecs),
+ std::end(iovecs));
+ return first_iovec;
+}
+
+// Create a fuse_attr filled with the specified mode and inode.
+fuse_attr DefaultFuseAttr(mode_t mode, uint64_t inode, uint64_t size = 512);
+
+// Return a fuse_entry_out FUSE server response body.
+fuse_entry_out DefaultEntryOut(mode_t mode, uint64_t node_id,
+ uint64_t size = 512);
+
+} // namespace testing
+} // namespace gvisor
+#endif // GVISOR_TEST_UTIL_FUSE_UTIL_H_