summaryrefslogtreecommitdiffhomepage
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/e2e/integration_test.go74
-rw-r--r--test/packetimpact/dut/BUILD2
-rw-r--r--test/packetimpact/dut/posix_server.cc40
-rw-r--r--test/packetimpact/proto/posix_server.proto23
-rw-r--r--test/packetimpact/runner/defs.bzl6
-rw-r--r--test/packetimpact/testbench/connections.go40
-rw-r--r--test/packetimpact/testbench/dut.go52
-rw-r--r--test/packetimpact/tests/BUILD27
-rw-r--r--test/packetimpact/tests/ipv4_fragment_reassembly_test.go12
-rw-r--r--test/packetimpact/tests/tcp_info_test.go109
-rw-r--r--test/packetimpact/tests/tcp_noaccept_close_rst_test.go15
-rw-r--r--test/packetimpact/tests/tcp_outside_the_window_test.go18
-rw-r--r--test/packetimpact/tests/tcp_rack_test.go204
-rw-r--r--test/packetimpact/tests/tcp_zero_window_probe_retransmit_test.go28
-rw-r--r--test/packetimpact/tests/udp_recv_mcast_bcast_test.go115
-rw-r--r--test/packetimpact/tests/udp_send_recv_dgram_test.go338
-rw-r--r--test/syscalls/BUILD4
-rw-r--r--test/syscalls/linux/BUILD26
-rw-r--r--test/syscalls/linux/chmod.cc36
-rw-r--r--test/syscalls/linux/chown.cc38
-rw-r--r--test/syscalls/linux/chroot.cc425
-rw-r--r--test/syscalls/linux/dup.cc85
-rw-r--r--test/syscalls/linux/fadvise64.cc11
-rw-r--r--test/syscalls/linux/fallocate.cc7
-rw-r--r--test/syscalls/linux/fchdir.cc12
-rw-r--r--test/syscalls/linux/fcntl.cc93
-rw-r--r--test/syscalls/linux/getdents.cc26
-rw-r--r--test/syscalls/linux/getrusage.cc96
-rw-r--r--test/syscalls/linux/ioctl.cc13
-rw-r--r--test/syscalls/linux/link.cc55
-rw-r--r--test/syscalls/linux/madvise.cc12
-rw-r--r--test/syscalls/linux/mmap.cc12
-rw-r--r--test/syscalls/linux/open.cc75
-rw-r--r--test/syscalls/linux/open_create.cc76
-rw-r--r--test/syscalls/linux/ping_socket.cc55
-rw-r--r--test/syscalls/linux/pread64.cc10
-rw-r--r--test/syscalls/linux/preadv.cc14
-rw-r--r--test/syscalls/linux/preadv2.cc18
-rw-r--r--test/syscalls/linux/proc_net_unix.cc3
-rw-r--r--test/syscalls/linux/pty.cc11
-rw-r--r--test/syscalls/linux/pwrite64.cc11
-rw-r--r--test/syscalls/linux/pwritev2.cc17
-rw-r--r--test/syscalls/linux/read.cc9
-rw-r--r--test/syscalls/linux/readv.cc14
-rw-r--r--test/syscalls/linux/setgid.cc370
-rw-r--r--test/syscalls/linux/shm.cc8
-rw-r--r--test/syscalls/linux/socket_ip_tcp_generic.cc31
-rw-r--r--test/syscalls/linux/stat.cc37
-rw-r--r--test/syscalls/linux/statfs.cc10
-rw-r--r--test/syscalls/linux/symlink.cc30
-rw-r--r--test/syscalls/linux/sync.cc12
-rw-r--r--test/syscalls/linux/tcp_socket.cc36
-rw-r--r--test/syscalls/linux/truncate.cc10
-rw-r--r--test/syscalls/linux/uidgid.cc107
-rw-r--r--test/syscalls/linux/write.cc38
-rw-r--r--test/syscalls/linux/xattr.cc21
-rw-r--r--test/util/capability_util.h13
-rw-r--r--test/util/fs_util.cc16
-rw-r--r--test/util/fs_util.h2
-rw-r--r--test/util/logging.cc8
-rw-r--r--test/util/logging.h54
-rw-r--r--test/util/multiprocess_util.h3
-rw-r--r--test/util/posix_error.cc2
-rw-r--r--test/util/posix_error.h14
64 files changed, 2468 insertions, 721 deletions
diff --git a/test/e2e/integration_test.go b/test/e2e/integration_test.go
index d07ed6ba5..aaffabfd0 100644
--- a/test/e2e/integration_test.go
+++ b/test/e2e/integration_test.go
@@ -434,18 +434,7 @@ func TestTmpMount(t *testing.T) {
// runsc to hide the incoherence of FDs opened before and after overlayfs
// copy-up on the host.
func TestHostOverlayfsCopyUp(t *testing.T) {
- ctx := context.Background()
- d := dockerutil.MakeContainer(ctx, t)
- defer d.CleanUp(ctx)
-
- if got, err := d.Run(ctx, dockerutil.RunOpts{
- Image: "basic/hostoverlaytest",
- WorkDir: "/root",
- }, "./test_copy_up"); err != nil {
- t.Fatalf("docker run failed: %v", err)
- } else if got != "" {
- t.Errorf("test failed:\n%s", got)
- }
+ runIntegrationTest(t, nil, "sh", "-c", "gcc -O2 -o test_copy_up test_copy_up.c && ./test_copy_up")
}
// TestHostOverlayfsRewindDir tests that rewinddir() "causes the directory
@@ -460,36 +449,14 @@ func TestHostOverlayfsCopyUp(t *testing.T) {
// automated tests yield newly-added files from readdir() even if the fsgofer
// does not explicitly rewinddir(), but overlayfs does not.
func TestHostOverlayfsRewindDir(t *testing.T) {
- ctx := context.Background()
- d := dockerutil.MakeContainer(ctx, t)
- defer d.CleanUp(ctx)
-
- if got, err := d.Run(ctx, dockerutil.RunOpts{
- Image: "basic/hostoverlaytest",
- WorkDir: "/root",
- }, "./test_rewinddir"); err != nil {
- t.Fatalf("docker run failed: %v", err)
- } else if got != "" {
- t.Errorf("test failed:\n%s", got)
- }
+ runIntegrationTest(t, nil, "sh", "-c", "gcc -O2 -o test_rewinddir test_rewinddir.c && ./test_rewinddir")
}
// Basic test for linkat(2). Syscall tests requires CAP_DAC_READ_SEARCH and it
// cannot use tricks like userns as root. For this reason, run a basic link test
// to ensure some coverage.
func TestLink(t *testing.T) {
- ctx := context.Background()
- d := dockerutil.MakeContainer(ctx, t)
- defer d.CleanUp(ctx)
-
- if got, err := d.Run(ctx, dockerutil.RunOpts{
- Image: "basic/linktest",
- WorkDir: "/root",
- }, "./link_test"); err != nil {
- t.Fatalf("docker run failed: %v", err)
- } else if got != "" {
- t.Errorf("test failed:\n%s", got)
- }
+ runIntegrationTest(t, nil, "sh", "-c", "gcc -O2 -o link_test link_test.c && ./link_test")
}
// This test ensures we can run ping without errors.
@@ -500,17 +467,7 @@ func TestPing4Loopback(t *testing.T) {
t.Skip("hostnet only supports TCP/UDP sockets, so ping is not supported.")
}
- ctx := context.Background()
- d := dockerutil.MakeContainer(ctx, t)
- defer d.CleanUp(ctx)
-
- if got, err := d.Run(ctx, dockerutil.RunOpts{
- Image: "basic/ping4test",
- }, "/root/ping4.sh"); err != nil {
- t.Fatalf("docker run failed: %s", err)
- } else if got != "" {
- t.Errorf("test failed:\n%s", got)
- }
+ runIntegrationTest(t, nil, "./ping4.sh")
}
// This test ensures we can enable ipv6 on loopback and run ping6 without
@@ -522,20 +479,25 @@ func TestPing6Loopback(t *testing.T) {
t.Skip("hostnet only supports TCP/UDP sockets, so ping6 is not supported.")
}
+ // The CAP_NET_ADMIN capability is required to use the `ip` utility, which
+ // we use to enable ipv6 on loopback.
+ //
+ // By default, ipv6 loopback is not enabled by runsc, because docker does
+ // not assign an ipv6 address to the test container.
+ runIntegrationTest(t, []string{"NET_ADMIN"}, "./ping6.sh")
+}
+
+func runIntegrationTest(t *testing.T, capAdd []string, args ...string) {
ctx := context.Background()
d := dockerutil.MakeContainer(ctx, t)
defer d.CleanUp(ctx)
if got, err := d.Run(ctx, dockerutil.RunOpts{
- Image: "basic/ping6test",
- // The CAP_NET_ADMIN capability is required to use the `ip` utility, which
- // we use to enable ipv6 on loopback.
- //
- // By default, ipv6 loopback is not enabled by runsc, because docker does
- // not assign an ipv6 address to the test container.
- CapAdd: []string{"NET_ADMIN"},
- }, "/root/ping6.sh"); err != nil {
- t.Fatalf("docker run failed: %s", err)
+ Image: "basic/integrationtest",
+ WorkDir: "/root",
+ CapAdd: capAdd,
+ }, args...); err != nil {
+ t.Fatalf("docker run failed: %v", err)
} else if got != "" {
t.Errorf("test failed:\n%s", got)
}
diff --git a/test/packetimpact/dut/BUILD b/test/packetimpact/dut/BUILD
index ccf1c735f..0be14ca3e 100644
--- a/test/packetimpact/dut/BUILD
+++ b/test/packetimpact/dut/BUILD
@@ -14,6 +14,7 @@ cc_binary(
grpcpp,
"//test/packetimpact/proto:posix_server_cc_grpc_proto",
"//test/packetimpact/proto:posix_server_cc_proto",
+ "@com_google_absl//absl/strings:str_format",
],
)
@@ -24,5 +25,6 @@ cc_binary(
grpcpp,
"//test/packetimpact/proto:posix_server_cc_grpc_proto",
"//test/packetimpact/proto:posix_server_cc_proto",
+ "@com_google_absl//absl/strings:str_format",
],
)
diff --git a/test/packetimpact/dut/posix_server.cc b/test/packetimpact/dut/posix_server.cc
index 4de8540f6..eba21df12 100644
--- a/test/packetimpact/dut/posix_server.cc
+++ b/test/packetimpact/dut/posix_server.cc
@@ -16,6 +16,7 @@
#include <getopt.h>
#include <netdb.h>
#include <netinet/in.h>
+#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -30,6 +31,7 @@
#include "include/grpcpp/security/server_credentials.h"
#include "include/grpcpp/server_builder.h"
#include "include/grpcpp/server_context.h"
+#include "absl/strings/str_format.h"
#include "test/packetimpact/proto/posix_server.grpc.pb.h"
#include "test/packetimpact/proto/posix_server.pb.h"
@@ -256,6 +258,44 @@ class PosixImpl final : public posix_server::Posix::Service {
return ::grpc::Status::OK;
}
+ ::grpc::Status Poll(::grpc::ServerContext *context,
+ const ::posix_server::PollRequest *request,
+ ::posix_server::PollResponse *response) override {
+ std::vector<struct pollfd> pfds;
+ pfds.reserve(request->pfds_size());
+ for (const auto &pfd : request->pfds()) {
+ pfds.push_back({
+ .fd = pfd.fd(),
+ .events = static_cast<short>(pfd.events()),
+ });
+ }
+ int ret = ::poll(pfds.data(), pfds.size(), request->timeout_millis());
+
+ response->set_ret(ret);
+ if (ret < 0) {
+ response->set_errno_(errno);
+ } else {
+ // Only pollfds that have non-empty revents are returned, the client can't
+ // rely on indexes of the request array.
+ for (const auto &pfd : pfds) {
+ if (pfd.revents) {
+ auto *proto_pfd = response->add_pfds();
+ proto_pfd->set_fd(pfd.fd);
+ proto_pfd->set_events(pfd.revents);
+ }
+ }
+ if (int ready = response->pfds_size(); ret != ready) {
+ return ::grpc::Status(
+ ::grpc::StatusCode::INTERNAL,
+ absl::StrFormat(
+ "poll's return value(%d) doesn't match the number of "
+ "file descriptors that are actually ready(%d)",
+ ret, ready));
+ }
+ }
+ return ::grpc::Status::OK;
+ }
+
::grpc::Status Send(::grpc::ServerContext *context,
const ::posix_server::SendRequest *request,
::posix_server::SendResponse *response) override {
diff --git a/test/packetimpact/proto/posix_server.proto b/test/packetimpact/proto/posix_server.proto
index f32ed54ef..b4c68764a 100644
--- a/test/packetimpact/proto/posix_server.proto
+++ b/test/packetimpact/proto/posix_server.proto
@@ -142,6 +142,25 @@ message ListenResponse {
int32 errno_ = 2; // "errno" may fail to compile in c++.
}
+// The events field is overloaded: when used for request, it is copied into the
+// events field of posix struct pollfd; when used for response, it is filled by
+// the revents field from the posix struct pollfd.
+message PollFd {
+ int32 fd = 1;
+ uint32 events = 2;
+}
+
+message PollRequest {
+ repeated PollFd pfds = 1;
+ int32 timeout_millis = 2;
+}
+
+message PollResponse {
+ int32 ret = 1;
+ int32 errno_ = 2; // "errno" may fail to compile in c++.
+ repeated PollFd pfds = 3;
+}
+
message SendRequest {
int32 sockfd = 1;
bytes buf = 2;
@@ -226,6 +245,10 @@ service Posix {
rpc GetSockOpt(GetSockOptRequest) returns (GetSockOptResponse);
// Call listen() on the DUT.
rpc Listen(ListenRequest) returns (ListenResponse);
+ // Call poll() on the DUT. Only pollfds that have non-empty revents are
+ // returned, the only way to tie the response back to the original request
+ // is using the fd number.
+ rpc Poll(PollRequest) returns (PollResponse);
// Call send() on the DUT.
rpc Send(SendRequest) returns (SendResponse);
// Call sendto() on the DUT.
diff --git a/test/packetimpact/runner/defs.bzl b/test/packetimpact/runner/defs.bzl
index 5c3c569de..a7c46781f 100644
--- a/test/packetimpact/runner/defs.bzl
+++ b/test/packetimpact/runner/defs.bzl
@@ -175,9 +175,6 @@ ALL_TESTS = [
name = "udp_discard_mcast_source_addr",
),
PacketimpactTestInfo(
- name = "udp_recv_mcast_bcast",
- ),
- PacketimpactTestInfo(
name = "udp_any_addr_recv_unicast",
),
PacketimpactTestInfo(
@@ -281,6 +278,9 @@ ALL_TESTS = [
name = "tcp_rack",
expect_netstack_failure = True,
),
+ PacketimpactTestInfo(
+ name = "tcp_info",
+ ),
]
def validate_all_tests():
diff --git a/test/packetimpact/testbench/connections.go b/test/packetimpact/testbench/connections.go
index 576577310..1453ac232 100644
--- a/test/packetimpact/testbench/connections.go
+++ b/test/packetimpact/testbench/connections.go
@@ -1008,6 +1008,13 @@ func (conn *UDPIPv4) LocalAddr(t *testing.T) *unix.SockaddrInet4 {
return sa
}
+// SrcPort returns the source port of this connection.
+func (conn *UDPIPv4) SrcPort(t *testing.T) uint16 {
+ t.Helper()
+
+ return *conn.udpState(t).out.SrcPort
+}
+
// Send sends a packet with reasonable defaults, potentially overriding the UDP
// layer and adding additionLayers.
func (conn *UDPIPv4) Send(t *testing.T, udp UDP, additionalLayers ...Layer) {
@@ -1024,6 +1031,11 @@ func (conn *UDPIPv4) SendIP(t *testing.T, ip IPv4, udp UDP, additionalLayers ...
(*Connection)(conn).send(t, Layers{&ip, &udp}, additionalLayers...)
}
+// SendFrame sends a frame on the wire and updates the state of all layers.
+func (conn *UDPIPv4) SendFrame(t *testing.T, overrideLayers Layers, additionalLayers ...Layer) {
+ (*Connection)(conn).send(t, overrideLayers, additionalLayers...)
+}
+
// Expect expects a frame with the UDP layer matching the provided UDP within
// the timeout specified. If it doesn't arrive in time, an error is returned.
func (conn *UDPIPv4) Expect(t *testing.T, udp UDP, timeout time.Duration) (*UDP, error) {
@@ -1053,6 +1065,14 @@ func (conn *UDPIPv4) ExpectData(t *testing.T, udp UDP, payload Payload, timeout
return (*Connection)(conn).ExpectFrame(t, expected, timeout)
}
+// ExpectFrame expects a frame that matches the provided Layers within the
+// timeout specified. If it doesn't arrive in time, an error is returned.
+func (conn *UDPIPv4) ExpectFrame(t *testing.T, frame Layers, timeout time.Duration) (Layers, error) {
+ t.Helper()
+
+ return (*Connection)(conn).ExpectFrame(t, frame, timeout)
+}
+
// Close frees associated resources held by the UDPIPv4 connection.
func (conn *UDPIPv4) Close(t *testing.T) {
t.Helper()
@@ -1136,6 +1156,13 @@ func (conn *UDPIPv6) LocalAddr(t *testing.T, zoneID uint32) *unix.SockaddrInet6
return sa
}
+// SrcPort returns the source port of this connection.
+func (conn *UDPIPv6) SrcPort(t *testing.T) uint16 {
+ t.Helper()
+
+ return *conn.udpState(t).out.SrcPort
+}
+
// Send sends a packet with reasonable defaults, potentially overriding the UDP
// layer and adding additionLayers.
func (conn *UDPIPv6) Send(t *testing.T, udp UDP, additionalLayers ...Layer) {
@@ -1152,6 +1179,11 @@ func (conn *UDPIPv6) SendIPv6(t *testing.T, ip IPv6, udp UDP, additionalLayers .
(*Connection)(conn).send(t, Layers{&ip, &udp}, additionalLayers...)
}
+// SendFrame sends a frame on the wire and updates the state of all layers.
+func (conn *UDPIPv6) SendFrame(t *testing.T, overrideLayers Layers, additionalLayers ...Layer) {
+ (*Connection)(conn).send(t, overrideLayers, additionalLayers...)
+}
+
// Expect expects a frame with the UDP layer matching the provided UDP within
// the timeout specified. If it doesn't arrive in time, an error is returned.
func (conn *UDPIPv6) Expect(t *testing.T, udp UDP, timeout time.Duration) (*UDP, error) {
@@ -1181,6 +1213,14 @@ func (conn *UDPIPv6) ExpectData(t *testing.T, udp UDP, payload Payload, timeout
return (*Connection)(conn).ExpectFrame(t, expected, timeout)
}
+// ExpectFrame expects a frame that matches the provided Layers within the
+// timeout specified. If it doesn't arrive in time, an error is returned.
+func (conn *UDPIPv6) ExpectFrame(t *testing.T, frame Layers, timeout time.Duration) (Layers, error) {
+ t.Helper()
+
+ return (*Connection)(conn).ExpectFrame(t, frame, timeout)
+}
+
// Close frees associated resources held by the UDPIPv6 connection.
func (conn *UDPIPv6) Close(t *testing.T) {
t.Helper()
diff --git a/test/packetimpact/testbench/dut.go b/test/packetimpact/testbench/dut.go
index 66a0255b8..aedcf6013 100644
--- a/test/packetimpact/testbench/dut.go
+++ b/test/packetimpact/testbench/dut.go
@@ -486,6 +486,56 @@ func (dut *DUT) ListenWithErrno(ctx context.Context, t *testing.T, sockfd, backl
return resp.GetRet(), syscall.Errno(resp.GetErrno_())
}
+// Poll calls poll on the DUT and causes a fatal test failure if it doesn't
+// succeed. If more control over error handling is needed, use PollWithErrno.
+// Only pollfds with non-empty revents are returned, the only way to tie the
+// response back to the original request is using the fd number.
+func (dut *DUT) Poll(t *testing.T, pfds []unix.PollFd, timeout time.Duration) []unix.PollFd {
+ t.Helper()
+
+ ctx := context.Background()
+ var cancel context.CancelFunc
+ if timeout >= 0 {
+ ctx, cancel = context.WithTimeout(ctx, timeout+RPCTimeout)
+ defer cancel()
+ }
+ ret, result, err := dut.PollWithErrno(ctx, t, pfds, timeout)
+ if ret < 0 {
+ t.Fatalf("failed to poll: %s", err)
+ }
+ return result
+}
+
+// PollWithErrno calls poll on the DUT.
+func (dut *DUT) PollWithErrno(ctx context.Context, t *testing.T, pfds []unix.PollFd, timeout time.Duration) (int32, []unix.PollFd, error) {
+ t.Helper()
+
+ req := pb.PollRequest{
+ TimeoutMillis: int32(timeout.Milliseconds()),
+ }
+ for _, pfd := range pfds {
+ req.Pfds = append(req.Pfds, &pb.PollFd{
+ Fd: pfd.Fd,
+ Events: uint32(pfd.Events),
+ })
+ }
+ resp, err := dut.posixServer.Poll(ctx, &req)
+ if err != nil {
+ t.Fatalf("failed to call Poll: %s", err)
+ }
+ if ret, npfds := resp.GetRet(), len(resp.GetPfds()); ret >= 0 && int(ret) != npfds {
+ t.Fatalf("nonsensical poll response: ret(%d) != len(pfds)(%d)", ret, npfds)
+ }
+ var result []unix.PollFd
+ for _, protoPfd := range resp.GetPfds() {
+ result = append(result, unix.PollFd{
+ Fd: protoPfd.GetFd(),
+ Revents: int16(protoPfd.GetEvents()),
+ })
+ }
+ return resp.GetRet(), result, syscall.Errno(resp.GetErrno_())
+}
+
// Send calls send 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
// SendWithErrno.
@@ -544,7 +594,7 @@ func (dut *DUT) SendToWithErrno(ctx context.Context, t *testing.T, sockfd int32,
}
resp, err := dut.posixServer.SendTo(ctx, &req)
if err != nil {
- t.Fatalf("faled to call SendTo: %s", err)
+ t.Fatalf("failed to call SendTo: %s", err)
}
return resp.GetRet(), syscall.Errno(resp.GetErrno_())
}
diff --git a/test/packetimpact/tests/BUILD b/test/packetimpact/tests/BUILD
index 6c6f2bdf7..baa3ae5e9 100644
--- a/test/packetimpact/tests/BUILD
+++ b/test/packetimpact/tests/BUILD
@@ -38,18 +38,6 @@ packetimpact_testbench(
)
packetimpact_testbench(
- name = "udp_recv_mcast_bcast",
- srcs = ["udp_recv_mcast_bcast_test.go"],
- deps = [
- "//pkg/tcpip",
- "//pkg/tcpip/header",
- "//test/packetimpact/testbench",
- "@com_github_google_go_cmp//cmp:go_default_library",
- "@org_golang_x_sys//unix:go_default_library",
- ],
-)
-
-packetimpact_testbench(
name = "udp_any_addr_recv_unicast",
srcs = ["udp_any_addr_recv_unicast_test.go"],
deps = [
@@ -340,6 +328,8 @@ packetimpact_testbench(
name = "udp_send_recv_dgram",
srcs = ["udp_send_recv_dgram_test.go"],
deps = [
+ "//pkg/tcpip",
+ "//pkg/tcpip/header",
"//test/packetimpact/testbench",
"@com_github_google_go_cmp//cmp:go_default_library",
"@org_golang_x_sys//unix:go_default_library",
@@ -390,6 +380,19 @@ packetimpact_testbench(
],
)
+packetimpact_testbench(
+ name = "tcp_info",
+ srcs = ["tcp_info_test.go"],
+ deps = [
+ "//pkg/abi/linux",
+ "//pkg/binary",
+ "//pkg/tcpip/header",
+ "//pkg/usermem",
+ "//test/packetimpact/testbench",
+ "@org_golang_x_sys//unix:go_default_library",
+ ],
+)
+
validate_all_tests()
[packetimpact_go_test(
diff --git a/test/packetimpact/tests/ipv4_fragment_reassembly_test.go b/test/packetimpact/tests/ipv4_fragment_reassembly_test.go
index d2203082d..ee050e2c6 100644
--- a/test/packetimpact/tests/ipv4_fragment_reassembly_test.go
+++ b/test/packetimpact/tests/ipv4_fragment_reassembly_test.go
@@ -45,8 +45,6 @@ func TestIPv4FragmentReassembly(t *testing.T) {
ipPayloadLen int
fragments []fragmentInfo
expectReply bool
- skip bool
- skipReason string
}{
{
description: "basic reassembly",
@@ -78,8 +76,6 @@ func TestIPv4FragmentReassembly(t *testing.T) {
{offset: 2000, size: 1000, id: 7, more: 0},
},
expectReply: true,
- skip: true,
- skipReason: "gvisor.dev/issues/4971",
},
{
description: "fragment subset",
@@ -91,8 +87,6 @@ func TestIPv4FragmentReassembly(t *testing.T) {
{offset: 2000, size: 1000, id: 8, more: 0},
},
expectReply: true,
- skip: true,
- skipReason: "gvisor.dev/issues/4971",
},
{
description: "fragment overlap",
@@ -104,16 +98,10 @@ func TestIPv4FragmentReassembly(t *testing.T) {
{offset: 2000, size: 1000, id: 9, more: 0},
},
expectReply: false,
- skip: true,
- skipReason: "gvisor.dev/issues/4971",
},
}
for _, test := range tests {
- if test.skip {
- t.Skip("%s test skipped: %s", test.description, test.skipReason)
- continue
- }
t.Run(test.description, func(t *testing.T) {
dut := testbench.NewDUT(t)
conn := dut.Net.NewIPv4Conn(t, testbench.IPv4{}, testbench.IPv4{})
diff --git a/test/packetimpact/tests/tcp_info_test.go b/test/packetimpact/tests/tcp_info_test.go
new file mode 100644
index 000000000..69275e54b
--- /dev/null
+++ b/test/packetimpact/tests/tcp_info_test.go
@@ -0,0 +1,109 @@
+// 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_info_test
+
+import (
+ "flag"
+ "testing"
+ "time"
+
+ "golang.org/x/sys/unix"
+ "gvisor.dev/gvisor/pkg/abi/linux"
+ "gvisor.dev/gvisor/pkg/binary"
+ "gvisor.dev/gvisor/pkg/tcpip/header"
+ "gvisor.dev/gvisor/pkg/usermem"
+ "gvisor.dev/gvisor/test/packetimpact/testbench"
+)
+
+func init() {
+ testbench.Initialize(flag.CommandLine)
+}
+
+func TestTCPInfo(t *testing.T) {
+ // Create a socket, listen, TCP connect, and accept.
+ dut := testbench.NewDUT(t)
+ listenFD, remotePort := dut.CreateListener(t, unix.SOCK_STREAM, unix.IPPROTO_TCP, 1)
+ defer dut.Close(t, listenFD)
+
+ conn := dut.Net.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)
+
+ // Send and receive sample data.
+ sampleData := []byte("Sample Data")
+ samplePayload := &testbench.Payload{Bytes: sampleData}
+ dut.Send(t, acceptFD, sampleData, 0)
+ if _, err := conn.ExpectData(t, &testbench.TCP{}, samplePayload, time.Second); err != nil {
+ t.Fatalf("expected a packet with payload %v: %s", samplePayload, err)
+ }
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck)})
+
+ info := linux.TCPInfo{}
+ infoBytes := dut.GetSockOpt(t, acceptFD, unix.SOL_TCP, unix.TCP_INFO, int32(linux.SizeOfTCPInfo))
+ if got, want := len(infoBytes), linux.SizeOfTCPInfo; got != want {
+ t.Fatalf("expected %T, got %d bytes want %d bytes", info, got, want)
+ }
+ binary.Unmarshal(infoBytes, usermem.ByteOrder, &info)
+
+ rtt := time.Duration(info.RTT) * time.Microsecond
+ rttvar := time.Duration(info.RTTVar) * time.Microsecond
+ rto := time.Duration(info.RTO) * time.Microsecond
+ if rtt == 0 || rttvar == 0 || rto == 0 {
+ t.Errorf("expected rtt(%v), rttvar(%v) and rto(%v) to be greater than zero", rtt, rttvar, rto)
+ }
+ if info.ReordSeen != 0 {
+ t.Errorf("expected the connection to not have any reordering, got: %v want: 0", info.ReordSeen)
+ }
+ if info.SndCwnd == 0 {
+ t.Errorf("expected send congestion window to be greater than zero")
+ }
+ if info.CaState != linux.TCP_CA_Open {
+ t.Errorf("expected the connection to be in open state, got: %v want: %v", info.CaState, linux.TCP_CA_Open)
+ }
+
+ if t.Failed() {
+ t.FailNow()
+ }
+
+ // Check the congestion control state and send congestion window after
+ // retransmission timeout.
+ seq := testbench.Uint32(uint32(*conn.RemoteSeqNum(t)))
+ dut.Send(t, acceptFD, sampleData, 0)
+ if _, err := conn.ExpectData(t, &testbench.TCP{}, samplePayload, time.Second); err != nil {
+ t.Fatalf("expected a packet with payload %v: %s", samplePayload, err)
+ }
+
+ // Expect retransmission of the packet within 1.5*RTO.
+ timeout := time.Duration(float64(info.RTO)*1.5) * time.Microsecond
+ if _, err := conn.ExpectData(t, &testbench.TCP{SeqNum: seq}, samplePayload, timeout); err != nil {
+ t.Fatalf("expected a packet with payload %v: %s", samplePayload, err)
+ }
+
+ info = linux.TCPInfo{}
+ infoBytes = dut.GetSockOpt(t, acceptFD, unix.SOL_TCP, unix.TCP_INFO, int32(linux.SizeOfTCPInfo))
+ if got, want := len(infoBytes), linux.SizeOfTCPInfo; got != want {
+ t.Fatalf("expected %T, got %d bytes want %d bytes", info, got, want)
+ }
+ binary.Unmarshal(infoBytes, usermem.ByteOrder, &info)
+ if info.CaState != linux.TCP_CA_Loss {
+ t.Errorf("expected the connection to be in loss recovery, got: %v want: %v", info.CaState, linux.TCP_CA_Loss)
+ }
+ if info.SndCwnd != 1 {
+ t.Errorf("expected send congestion window to be 1, got: %v %v", info.SndCwnd)
+ }
+}
diff --git a/test/packetimpact/tests/tcp_noaccept_close_rst_test.go b/test/packetimpact/tests/tcp_noaccept_close_rst_test.go
index f0af5352d..c874a8912 100644
--- a/test/packetimpact/tests/tcp_noaccept_close_rst_test.go
+++ b/test/packetimpact/tests/tcp_noaccept_close_rst_test.go
@@ -34,6 +34,21 @@ func TestTcpNoAcceptCloseReset(t *testing.T) {
conn := dut.Net.NewTCPIPv4(t, testbench.TCP{DstPort: &remotePort}, testbench.TCP{SrcPort: &remotePort})
conn.Connect(t)
defer conn.Close(t)
+ // We need to wait for POLLIN event on listenFd to know the connection is
+ // established. Otherwise there could be a race when we issue the Close
+ // command prior to the DUT receiving the last ack of the handshake and
+ // it will only respond RST instead of RST+ACK.
+ timeout := time.Second
+ pfds := dut.Poll(t, []unix.PollFd{{Fd: listenFd, Events: unix.POLLIN}}, timeout)
+ if n := len(pfds); n != 1 {
+ t.Fatalf("poll returned %d ready file descriptors, expected 1", n)
+ }
+ if readyFd := pfds[0].Fd; readyFd != listenFd {
+ t.Fatalf("poll returned an fd %d that was not requested (%d)", readyFd, listenFd)
+ }
+ if got, want := pfds[0].Revents, int16(unix.POLLIN); got&want == 0 {
+ t.Fatalf("poll returned no events in our interest, got: %#b, want: %#b", got, want)
+ }
dut.Close(t, listenFd)
if _, err := conn.Expect(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagRst | header.TCPFlagAck)}, 1*time.Second); err != nil {
t.Fatalf("expected a RST-ACK packet but got none: %s", err)
diff --git a/test/packetimpact/tests/tcp_outside_the_window_test.go b/test/packetimpact/tests/tcp_outside_the_window_test.go
index 1b041932a..8909a348e 100644
--- a/test/packetimpact/tests/tcp_outside_the_window_test.go
+++ b/test/packetimpact/tests/tcp_outside_the_window_test.go
@@ -84,6 +84,24 @@ func TestTCPOutsideTheWindow(t *testing.T) {
if tt.expectACK && err != nil {
t.Fatalf("expected an ACK packet within %s but got none: %s", timeout, err)
}
+ // Data packets w/o SYN bits are always acked by Linux. Netstack ACK's data packets
+ // always right now. So only send a second segment and test for no ACK for packets
+ // with no data.
+ if tt.expectACK && tt.payload == nil {
+ // Sending another out-of-window segment immediately should not trigger
+ // an ACK if less than 500ms(default rate limit for out-of-window ACKs)
+ // has passed since the last ACK was sent.
+ t.Logf("sending another segment")
+ conn.Send(t, testbench.TCP{
+ Flags: testbench.Uint8(tt.tcpFlags),
+ SeqNum: testbench.Uint32(uint32(conn.LocalSeqNum(t).Add(windowSize))),
+ }, tt.payload...)
+ timeout := 3 * time.Second
+ gotACK, err := conn.Expect(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: localSeqNum}, timeout)
+ if err == nil {
+ t.Fatalf("expected no ACK packet but got one: %s", gotACK)
+ }
+ }
if !tt.expectACK && gotACK != nil {
t.Fatalf("expected no ACK packet within %s but got one: %s", timeout, gotACK)
}
diff --git a/test/packetimpact/tests/tcp_rack_test.go b/test/packetimpact/tests/tcp_rack_test.go
index 0a2381c97..fb2a4cc90 100644
--- a/test/packetimpact/tests/tcp_rack_test.go
+++ b/test/packetimpact/tests/tcp_rack_test.go
@@ -70,8 +70,11 @@ func closeSACKConnection(t *testing.T, dut testbench.DUT, conn testbench.TCPIPv4
func getRTTAndRTO(t *testing.T, dut testbench.DUT, acceptFd int32) (rtt, rto time.Duration) {
info := linux.TCPInfo{}
- ret := dut.GetSockOpt(t, acceptFd, unix.SOL_TCP, unix.TCP_INFO, int32(linux.SizeOfTCPInfo))
- binary.Unmarshal(ret, usermem.ByteOrder, &info)
+ infoBytes := dut.GetSockOpt(t, acceptFd, unix.SOL_TCP, unix.TCP_INFO, int32(linux.SizeOfTCPInfo))
+ if got, want := len(infoBytes), linux.SizeOfTCPInfo; got != want {
+ t.Fatalf("expected %T, got %d bytes want %d bytes", info, got, want)
+ }
+ binary.Unmarshal(infoBytes, usermem.ByteOrder, &info)
return time.Duration(info.RTT) * time.Microsecond, time.Duration(info.RTO) * time.Microsecond
}
@@ -219,3 +222,200 @@ func TestRACKTLPWithSACK(t *testing.T) {
}
closeSACKConnection(t, dut, conn, acceptFd, listenFd)
}
+
+// TestRACKWithoutReorder tests that without reordering RACK will retransmit the
+// lost packets after reorder timer expires.
+func TestRACKWithoutReorder(t *testing.T) {
+ dut, conn, acceptFd, listenFd := createSACKConnection(t)
+ seqNum1 := *conn.RemoteSeqNum(t)
+
+ // Send ACK for data packets to establish RTT.
+ sendAndReceive(t, dut, conn, numPktsForRTT, acceptFd, true /* sendACK */)
+ seqNum1.UpdateForward(seqnum.Size(numPktsForRTT * payloadSize))
+
+ // We are not sending ACK for these packets.
+ const numPkts = 4
+ sendAndReceive(t, dut, conn, numPkts, acceptFd, false /* sendACK */)
+
+ // SACK for [3,4] packets.
+ sackBlock := make([]byte, 40)
+ start := seqNum1.Add(seqnum.Size(2 * payloadSize))
+ end := start.Add(seqnum.Size(2 * payloadSize))
+ sbOff := 0
+ sbOff += header.EncodeNOP(sackBlock[sbOff:])
+ sbOff += header.EncodeNOP(sackBlock[sbOff:])
+ sbOff += header.EncodeSACKBlocks([]header.SACKBlock{{
+ start, end,
+ }}, sackBlock[sbOff:])
+ time.Sleep(simulatedRTT)
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: testbench.Uint32(uint32(seqNum1)), Options: sackBlock[:sbOff]})
+
+ // RACK marks #1 and #2 packets as lost and retransmits both after
+ // RTT + reorderWindow. The reorderWindow initially will be a small
+ // fraction of RTT.
+ rtt, _ := getRTTAndRTO(t, dut, acceptFd)
+ timeout := 2 * rtt
+ for i, sn := 0, seqNum1; i < 2; i++ {
+ if _, err := conn.Expect(t, testbench.TCP{SeqNum: testbench.Uint32(uint32(sn))}, timeout); err != nil {
+ t.Fatalf("expected payload was not received: %s", err)
+ }
+ sn.UpdateForward(seqnum.Size(payloadSize))
+ }
+ closeSACKConnection(t, dut, conn, acceptFd, listenFd)
+}
+
+// TestRACKWithReorder tests that RACK will retransmit segments when there is
+// reordering in the connection and reorder timer expires.
+func TestRACKWithReorder(t *testing.T) {
+ dut, conn, acceptFd, listenFd := createSACKConnection(t)
+ seqNum1 := *conn.RemoteSeqNum(t)
+
+ // Send ACK for data packets to establish RTT.
+ sendAndReceive(t, dut, conn, numPktsForRTT, acceptFd, true /* sendACK */)
+ seqNum1.UpdateForward(seqnum.Size(numPktsForRTT * payloadSize))
+
+ // We are not sending ACK for these packets.
+ const numPkts = 4
+ sendAndReceive(t, dut, conn, numPkts, acceptFd, false /* sendACK */)
+
+ time.Sleep(simulatedRTT)
+ // SACK in reverse order for the connection to detect reorder.
+ var start seqnum.Value
+ var end seqnum.Value
+ for i := 0; i < numPkts-1; i++ {
+ sackBlock := make([]byte, 40)
+ sbOff := 0
+ start = seqNum1.Add(seqnum.Size((numPkts - i - 1) * payloadSize))
+ end = start.Add(seqnum.Size((i + 1) * payloadSize))
+ sackBlock = make([]byte, 40)
+ sbOff = 0
+ sbOff += header.EncodeNOP(sackBlock[sbOff:])
+ sbOff += header.EncodeNOP(sackBlock[sbOff:])
+ sbOff += header.EncodeSACKBlocks([]header.SACKBlock{{
+ start, end,
+ }}, sackBlock[sbOff:])
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: testbench.Uint32(uint32(seqNum1)), Options: sackBlock[:sbOff]})
+ }
+
+ // Send a DSACK block indicating both original and retransmitted
+ // packets are received, RACK will increase the reordering window on
+ // every DSACK.
+ dsackBlock := make([]byte, 40)
+ dbOff := 0
+ start = seqNum1
+ end = start.Add(seqnum.Size(2 * payloadSize))
+ dbOff += header.EncodeNOP(dsackBlock[dbOff:])
+ dbOff += header.EncodeNOP(dsackBlock[dbOff:])
+ dbOff += header.EncodeSACKBlocks([]header.SACKBlock{{
+ start, end,
+ }}, dsackBlock[dbOff:])
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: testbench.Uint32(uint32(seqNum1 + numPkts*payloadSize)), Options: dsackBlock[:dbOff]})
+
+ seqNum1.UpdateForward(seqnum.Size(numPkts * payloadSize))
+ sendTime := time.Now()
+ sendAndReceive(t, dut, conn, numPkts, acceptFd, false /* sendACK */)
+
+ time.Sleep(simulatedRTT)
+ // Send SACK for [2-5] packets.
+ sackBlock := make([]byte, 40)
+ sbOff := 0
+ start = seqNum1.Add(seqnum.Size(payloadSize))
+ end = start.Add(seqnum.Size(3 * payloadSize))
+ sbOff += header.EncodeNOP(sackBlock[sbOff:])
+ sbOff += header.EncodeNOP(sackBlock[sbOff:])
+ sbOff += header.EncodeSACKBlocks([]header.SACKBlock{{
+ start, end,
+ }}, sackBlock[sbOff:])
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: testbench.Uint32(uint32(seqNum1)), Options: sackBlock[:sbOff]})
+
+ // Expect the retransmission of #1 packet after RTT+ReorderWindow.
+ if _, err := conn.Expect(t, testbench.TCP{SeqNum: testbench.Uint32(uint32(seqNum1))}, time.Second); err != nil {
+ t.Fatalf("expected payload was not received: %s", err)
+ }
+ rtt, _ := getRTTAndRTO(t, dut, acceptFd)
+ diff := time.Now().Sub(sendTime)
+ if diff < rtt {
+ t.Fatalf("expected payload was received too sonn, within RTT")
+ }
+
+ closeSACKConnection(t, dut, conn, acceptFd, listenFd)
+}
+
+// TestRACKWithLostRetransmission tests that RACK will not enter RTO when a
+// retransmitted segment is lost and enters fast recovery.
+func TestRACKWithLostRetransmission(t *testing.T) {
+ dut, conn, acceptFd, listenFd := createSACKConnection(t)
+ seqNum1 := *conn.RemoteSeqNum(t)
+
+ // Send ACK for data packets to establish RTT.
+ sendAndReceive(t, dut, conn, numPktsForRTT, acceptFd, true /* sendACK */)
+ seqNum1.UpdateForward(seqnum.Size(numPktsForRTT * payloadSize))
+
+ // We are not sending ACK for these packets.
+ const numPkts = 5
+ sendAndReceive(t, dut, conn, numPkts, acceptFd, false /* sendACK */)
+
+ // SACK for [2-5] packets.
+ sackBlock := make([]byte, 40)
+ start := seqNum1.Add(seqnum.Size(payloadSize))
+ end := start.Add(seqnum.Size(4 * payloadSize))
+ sbOff := 0
+ sbOff += header.EncodeNOP(sackBlock[sbOff:])
+ sbOff += header.EncodeNOP(sackBlock[sbOff:])
+ sbOff += header.EncodeSACKBlocks([]header.SACKBlock{{
+ start, end,
+ }}, sackBlock[sbOff:])
+ time.Sleep(simulatedRTT)
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: testbench.Uint32(uint32(seqNum1)), Options: sackBlock[:sbOff]})
+
+ // RACK marks #1 packet as lost and retransmits it after
+ // RTT + reorderWindow. The reorderWindow is bounded between a small
+ // fraction of RTT and 1 RTT.
+ rtt, _ := getRTTAndRTO(t, dut, acceptFd)
+ timeout := 2 * rtt
+ if _, err := conn.Expect(t, testbench.TCP{SeqNum: testbench.Uint32(uint32(seqNum1))}, timeout); err != nil {
+ t.Fatalf("expected payload was not received: %s", err)
+ }
+
+ // Send #6 packet.
+ payload := make([]byte, payloadSize)
+ dut.Send(t, acceptFd, payload, 0)
+ gotOne, err := conn.Expect(t, testbench.TCP{SeqNum: testbench.Uint32(uint32(seqNum1 + 5*payloadSize))}, time.Second)
+ if err != nil {
+ t.Fatalf("Expect #6: %s", err)
+ }
+ if gotOne == nil {
+ t.Fatalf("#6: expected a packet within a second but got none")
+ }
+
+ // SACK for [2-6] packets.
+ sackBlock1 := make([]byte, 40)
+ start = seqNum1.Add(seqnum.Size(payloadSize))
+ end = start.Add(seqnum.Size(5 * payloadSize))
+ sbOff1 := 0
+ sbOff1 += header.EncodeNOP(sackBlock1[sbOff1:])
+ sbOff1 += header.EncodeNOP(sackBlock1[sbOff1:])
+ sbOff1 += header.EncodeSACKBlocks([]header.SACKBlock{{
+ start, end,
+ }}, sackBlock1[sbOff1:])
+ time.Sleep(simulatedRTT)
+ conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: testbench.Uint32(uint32(seqNum1)), Options: sackBlock1[:sbOff1]})
+
+ // Expect re-retransmission of #1 packet without entering an RTO.
+ if _, err := conn.Expect(t, testbench.TCP{SeqNum: testbench.Uint32(uint32(seqNum1))}, timeout); err != nil {
+ t.Fatalf("expected payload was not received: %s", err)
+ }
+
+ // Check the congestion control state.
+ info := linux.TCPInfo{}
+ infoBytes := dut.GetSockOpt(t, acceptFd, unix.SOL_TCP, unix.TCP_INFO, int32(linux.SizeOfTCPInfo))
+ if got, want := len(infoBytes), linux.SizeOfTCPInfo; got != want {
+ t.Fatalf("expected %T, got %d bytes want %d bytes", info, got, want)
+ }
+ binary.Unmarshal(infoBytes, usermem.ByteOrder, &info)
+ if info.CaState != linux.TCP_CA_Recovery {
+ t.Fatalf("expected connection to be in fast recovery, want: %v got: %v", linux.TCP_CA_Recovery, info.CaState)
+ }
+
+ closeSACKConnection(t, dut, conn, acceptFd, listenFd)
+}
diff --git a/test/packetimpact/tests/tcp_zero_window_probe_retransmit_test.go b/test/packetimpact/tests/tcp_zero_window_probe_retransmit_test.go
index 1ab9ee1b2..b15b8fc25 100644
--- a/test/packetimpact/tests/tcp_zero_window_probe_retransmit_test.go
+++ b/test/packetimpact/tests/tcp_zero_window_probe_retransmit_test.go
@@ -66,33 +66,39 @@ func TestZeroWindowProbeRetransmit(t *testing.T) {
probeSeq := testbench.Uint32(uint32(*conn.RemoteSeqNum(t) - 1))
ackProbe := testbench.Uint32(uint32(*conn.RemoteSeqNum(t)))
- startProbeDuration := time.Second
- current := startProbeDuration
- first := time.Now()
// Ask the dut to send out data.
dut.Send(t, acceptFd, sampleData, 0)
+
+ var prev time.Duration
// Expect the dut to keep the connection alive as long as the remote is
// acknowledging the zero-window probes.
- for i := 0; i < 5; i++ {
+ for i := 1; i <= 5; i++ {
start := time.Now()
// Expect zero-window probe with a timeout which is a function of the typical
// first retransmission time. The retransmission times is supposed to
// exponentially increase.
- if _, err := conn.ExpectData(t, &testbench.TCP{SeqNum: probeSeq}, nil, 2*current); err != nil {
+ if _, err := conn.ExpectData(t, &testbench.TCP{SeqNum: probeSeq}, nil, time.Duration(i)*time.Second); err != nil {
t.Fatalf("expected a probe with sequence number %d: loop %d", probeSeq, i)
}
- if i == 0 {
- startProbeDuration = time.Now().Sub(first)
- current = 2 * startProbeDuration
+ if i == 1 {
+ // Skip the first probe as computing transmit time for that is
+ // non-deterministic because of the arbitrary time taken for
+ // the dut to receive a send command and issue a send.
continue
}
- // Check if the probes came at exponentially increasing intervals.
- if got, want := time.Since(start), current-startProbeDuration; got < want {
+
+ // Check if the time taken to receive the probe from the dut is
+ // increasing exponentially. To avoid flakes, use a correction
+ // factor for the expected duration which accounts for any
+ // scheduling non-determinism.
+ const timeCorrection = 200 * time.Millisecond
+ got := time.Since(start)
+ if want := (2 * prev) - timeCorrection; prev != 0 && got < want {
t.Errorf("got zero probe %d after %s, want >= %s", i, got, want)
}
+ prev = got
// Acknowledge the zero-window probes from the dut.
conn.Send(t, testbench.TCP{AckNum: ackProbe, Flags: testbench.Uint8(header.TCPFlagAck), WindowSize: testbench.Uint16(0)})
- current *= 2
}
// Advertize non-zero window.
conn.Send(t, testbench.TCP{AckNum: ackProbe, Flags: testbench.Uint8(header.TCPFlagAck)})
diff --git a/test/packetimpact/tests/udp_recv_mcast_bcast_test.go b/test/packetimpact/tests/udp_recv_mcast_bcast_test.go
deleted file mode 100644
index b29c07825..000000000
--- a/test/packetimpact/tests/udp_recv_mcast_bcast_test.go
+++ /dev/null
@@ -1,115 +0,0 @@
-// 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 udp_recv_mcast_bcast_test
-
-import (
- "context"
- "flag"
- "fmt"
- "net"
- "syscall"
- "testing"
-
- "github.com/google/go-cmp/cmp"
- "golang.org/x/sys/unix"
- "gvisor.dev/gvisor/pkg/tcpip"
- "gvisor.dev/gvisor/test/packetimpact/testbench"
-)
-
-func init() {
- testbench.Initialize(flag.CommandLine)
-}
-
-func TestUDPRecvMcastBcast(t *testing.T) {
- dut := testbench.NewDUT(t)
- subnetBcastAddr := broadcastAddr(dut.Net.RemoteIPv4, net.CIDRMask(dut.Net.IPv4PrefixLength, 32))
- for _, v := range []struct {
- bound, to net.IP
- }{
- {bound: net.IPv4zero, to: subnetBcastAddr},
- {bound: net.IPv4zero, to: net.IPv4bcast},
- {bound: net.IPv4zero, to: net.IPv4allsys},
-
- {bound: subnetBcastAddr, to: subnetBcastAddr},
-
- // FIXME(gvisor.dev/issue/4896): Previously by the time subnetBcastAddr is
- // created, IPv4PrefixLength is still 0 because genPseudoFlags is not called
- // yet, it was only called in NewDUT, so the test didn't do what the author
- // original intended to and becomes failing because we process all flags at
- // the very beginning.
- //
- // {bound: subnetBcastAddr, to: net.IPv4bcast},
-
- {bound: net.IPv4bcast, to: net.IPv4bcast},
- {bound: net.IPv4allsys, to: net.IPv4allsys},
- } {
- t.Run(fmt.Sprintf("bound=%s,to=%s", v.bound, v.to), func(t *testing.T) {
- boundFD, remotePort := dut.CreateBoundSocket(t, unix.SOCK_DGRAM, unix.IPPROTO_UDP, v.bound)
- defer dut.Close(t, boundFD)
- conn := dut.Net.NewUDPIPv4(t, testbench.UDP{DstPort: &remotePort}, testbench.UDP{SrcPort: &remotePort})
- defer conn.Close(t)
-
- payload := testbench.GenerateRandomPayload(t, 1<<10 /* 1 KiB */)
- conn.SendIP(
- t,
- testbench.IPv4{DstAddr: testbench.Address(tcpip.Address(v.to.To4()))},
- testbench.UDP{},
- &testbench.Payload{Bytes: payload},
- )
- got, want := dut.Recv(t, boundFD, int32(len(payload)+1), 0), payload
- if diff := cmp.Diff(want, got); diff != "" {
- t.Errorf("received payload does not match sent payload, diff (-want, +got):\n%s", diff)
- }
- })
- }
-}
-
-func TestUDPDoesntRecvMcastBcastOnUnicastAddr(t *testing.T) {
- dut := testbench.NewDUT(t)
- boundFD, remotePort := dut.CreateBoundSocket(t, unix.SOCK_DGRAM, unix.IPPROTO_UDP, dut.Net.RemoteIPv4)
- dut.SetSockOptTimeval(t, boundFD, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: 1, Usec: 0})
- defer dut.Close(t, boundFD)
- conn := dut.Net.NewUDPIPv4(t, testbench.UDP{DstPort: &remotePort}, testbench.UDP{SrcPort: &remotePort})
- defer conn.Close(t)
-
- for _, to := range []net.IP{
- broadcastAddr(dut.Net.RemoteIPv4, net.CIDRMask(dut.Net.IPv4PrefixLength, 32)),
- net.IPv4(255, 255, 255, 255),
- net.IPv4(224, 0, 0, 1),
- } {
- t.Run(fmt.Sprint("to=%s", to), func(t *testing.T) {
- payload := testbench.GenerateRandomPayload(t, 1<<10 /* 1 KiB */)
- conn.SendIP(
- t,
- testbench.IPv4{DstAddr: testbench.Address(tcpip.Address(to.To4()))},
- testbench.UDP{},
- &testbench.Payload{Bytes: payload},
- )
- ret, payload, errno := dut.RecvWithErrno(context.Background(), t, boundFD, 100, 0)
- if errno != syscall.EAGAIN || errno != syscall.EWOULDBLOCK {
- t.Errorf("Recv got unexpected result, ret=%d, payload=%q, errno=%s", ret, payload, errno)
- }
- })
- }
-}
-
-func broadcastAddr(ip net.IP, mask net.IPMask) net.IP {
- result := make(net.IP, net.IPv4len)
- ip4 := ip.To4()
- for i := range ip4 {
- result[i] = ip4[i] | ^mask[i]
- }
- return result
-}
diff --git a/test/packetimpact/tests/udp_send_recv_dgram_test.go b/test/packetimpact/tests/udp_send_recv_dgram_test.go
index 7ee2c8014..6e45cb143 100644
--- a/test/packetimpact/tests/udp_send_recv_dgram_test.go
+++ b/test/packetimpact/tests/udp_send_recv_dgram_test.go
@@ -15,13 +15,18 @@
package udp_send_recv_dgram_test
import (
+ "context"
"flag"
+ "fmt"
"net"
+ "syscall"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"golang.org/x/sys/unix"
+ "gvisor.dev/gvisor/pkg/tcpip"
+ "gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/test/packetimpact/testbench"
)
@@ -30,74 +35,295 @@ func init() {
}
type udpConn interface {
- Send(*testing.T, testbench.UDP, ...testbench.Layer)
- ExpectData(*testing.T, testbench.UDP, testbench.Payload, time.Duration) (testbench.Layers, error)
- Drain(*testing.T)
+ SrcPort(*testing.T) uint16
+ SendFrame(*testing.T, testbench.Layers, ...testbench.Layer)
+ ExpectFrame(*testing.T, testbench.Layers, time.Duration) (testbench.Layers, error)
Close(*testing.T)
}
+type testCase struct {
+ bindTo, sendTo net.IP
+ sendToBroadcast, bindToDevice, expectData bool
+}
+
func TestUDP(t *testing.T) {
dut := testbench.NewDUT(t)
+ subnetBcast := func() net.IP {
+ subnet := (&tcpip.AddressWithPrefix{
+ Address: tcpip.Address(dut.Net.RemoteIPv4.To4()),
+ PrefixLen: dut.Net.IPv4PrefixLength,
+ }).Subnet()
+ return net.IP(subnet.Broadcast())
+ }()
- for _, isIPv4 := range []bool{true, false} {
- ipVersionName := "IPv6"
- if isIPv4 {
- ipVersionName = "IPv4"
- }
- t.Run(ipVersionName, func(t *testing.T) {
- var addr net.IP
- if isIPv4 {
- addr = dut.Net.RemoteIPv4
- } else {
- addr = dut.Net.RemoteIPv6
+ t.Run("Send", func(t *testing.T) {
+ var testCases []testCase
+ // Test every valid combination of bound/unbound, broadcast/multicast/unicast
+ // bound/destination address, and bound/not-bound to device.
+ for _, bindTo := range []net.IP{
+ nil, // Do not bind.
+ net.IPv4zero,
+ net.IPv4bcast,
+ net.IPv4allsys,
+ subnetBcast,
+ dut.Net.RemoteIPv4,
+ dut.Net.RemoteIPv6,
+ } {
+ for _, sendTo := range []net.IP{
+ net.IPv4bcast,
+ net.IPv4allsys,
+ subnetBcast,
+ dut.Net.LocalIPv4,
+ dut.Net.LocalIPv6,
+ } {
+ // Cannot send to an IPv4 address from a socket bound to IPv6 (except for IPv4-mapped IPv6),
+ // and viceversa.
+ if bindTo != nil && ((bindTo.To4() == nil) != (sendTo.To4() == nil)) {
+ continue
+ }
+ for _, bindToDevice := range []bool{true, false} {
+ expectData := true
+ switch {
+ case bindTo.Equal(dut.Net.RemoteIPv4):
+ // If we're explicitly bound to an interface's unicast address,
+ // packets are always sent on that interface.
+ case bindToDevice:
+ // If we're explicitly bound to an interface, packets are always
+ // sent on that interface.
+ case !sendTo.Equal(net.IPv4bcast) && !sendTo.IsMulticast():
+ // If we're not sending to limited broadcast or multicast, the route table
+ // will be consulted and packets will be sent on the correct interface.
+ default:
+ expectData = false
+ }
+ testCases = append(
+ testCases,
+ testCase{
+ bindTo: bindTo,
+ sendTo: sendTo,
+ sendToBroadcast: sendTo.Equal(subnetBcast) || sendTo.Equal(net.IPv4bcast),
+ bindToDevice: bindToDevice,
+ expectData: expectData,
+ },
+ )
+ }
}
- boundFD, remotePort := dut.CreateBoundSocket(t, unix.SOCK_DGRAM, unix.IPPROTO_UDP, addr)
- defer dut.Close(t, boundFD)
-
- var conn udpConn
- var localAddr unix.Sockaddr
- if isIPv4 {
- v4Conn := dut.Net.NewUDPIPv4(t, testbench.UDP{DstPort: &remotePort}, testbench.UDP{SrcPort: &remotePort})
- localAddr = v4Conn.LocalAddr(t)
- conn = &v4Conn
- } else {
- v6Conn := dut.Net.NewUDPIPv6(t, testbench.UDP{DstPort: &remotePort}, testbench.UDP{SrcPort: &remotePort})
- localAddr = v6Conn.LocalAddr(t, dut.Net.RemoteDevID)
- conn = &v6Conn
- }
- defer conn.Close(t)
-
- testCases := []struct {
- name string
- payload []byte
- }{
- {"emptypayload", nil},
- {"small payload", []byte("hello world")},
- {"1kPayload", testbench.GenerateRandomPayload(t, 1<<10)},
- // Even though UDP allows larger dgrams we don't test it here as
- // they need to be fragmented and written out as individual
- // frames.
+ }
+ for _, tc := range testCases {
+ boundTestCaseName := "unbound"
+ if tc.bindTo != nil {
+ boundTestCaseName = fmt.Sprintf("bindTo=%s", tc.bindTo)
}
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- t.Run("Send", func(t *testing.T) {
- conn.Send(t, testbench.UDP{}, &testbench.Payload{Bytes: tc.payload})
- got, want := dut.Recv(t, boundFD, int32(len(tc.payload)+1), 0), tc.payload
- if diff := cmp.Diff(want, got); diff != "" {
- t.Fatalf("received payload does not match sent payload, diff (-want, +got):\n%s", diff)
+ t.Run(fmt.Sprintf("%s/sendTo=%s/bindToDevice=%t/expectData=%t", boundTestCaseName, tc.sendTo, tc.bindToDevice, tc.expectData), func(t *testing.T) {
+ runTestCase(
+ t,
+ dut,
+ tc,
+ func(t *testing.T, dut testbench.DUT, conn udpConn, socketFD int32, tc testCase, payload []byte, layers testbench.Layers) {
+ var destSockaddr unix.Sockaddr
+ if sendTo4 := tc.sendTo.To4(); sendTo4 != nil {
+ addr := unix.SockaddrInet4{
+ Port: int(conn.SrcPort(t)),
+ }
+ copy(addr.Addr[:], sendTo4)
+ destSockaddr = &addr
+ } else {
+ addr := unix.SockaddrInet6{
+ Port: int(conn.SrcPort(t)),
+ ZoneId: dut.Net.RemoteDevID,
+ }
+ copy(addr.Addr[:], tc.sendTo.To16())
+ destSockaddr = &addr
}
- })
- t.Run("Recv", func(t *testing.T) {
- conn.Drain(t)
- if got, want := int(dut.SendTo(t, boundFD, tc.payload, 0, localAddr)), len(tc.payload); got != want {
- t.Fatalf("short write got: %d, want: %d", got, want)
+ if got, want := dut.SendTo(t, socketFD, payload, 0, destSockaddr), len(payload); int(got) != want {
+ t.Fatalf("got dut.SendTo = %d, want %d", got, want)
}
- if _, err := conn.ExpectData(t, testbench.UDP{SrcPort: &remotePort}, testbench.Payload{Bytes: tc.payload}, time.Second); err != nil {
+ layers = append(layers, &testbench.Payload{
+ Bytes: payload,
+ })
+ _, err := conn.ExpectFrame(t, layers, time.Second)
+
+ if !tc.expectData && err == nil {
+ t.Fatal("received unexpected packet, socket is not bound to device")
+ }
+ if err != nil && tc.expectData {
t.Fatal(err)
}
- })
- })
+ },
+ )
+ })
+ }
+ })
+ t.Run("Recv", func(t *testing.T) {
+ // Test every valid combination of broadcast/multicast/unicast
+ // bound/destination address, and bound/not-bound to device.
+ var testCases []testCase
+ for _, addr := range []net.IP{
+ net.IPv4bcast,
+ net.IPv4allsys,
+ dut.Net.RemoteIPv4,
+ dut.Net.RemoteIPv6,
+ } {
+ for _, bindToDevice := range []bool{true, false} {
+ testCases = append(
+ testCases,
+ testCase{
+ bindTo: addr,
+ sendTo: addr,
+ sendToBroadcast: addr.Equal(subnetBcast) || addr.Equal(net.IPv4bcast),
+ bindToDevice: bindToDevice,
+ expectData: true,
+ },
+ )
}
- })
+ }
+ for _, bindTo := range []net.IP{
+ net.IPv4zero,
+ subnetBcast,
+ dut.Net.RemoteIPv4,
+ } {
+ for _, sendTo := range []net.IP{
+ subnetBcast,
+ net.IPv4bcast,
+ net.IPv4allsys,
+ } {
+ // TODO(gvisor.dev/issue/4896): Add bindTo=subnetBcast/sendTo=IPv4bcast
+ // and bindTo=subnetBcast/sendTo=IPv4allsys test cases.
+ if bindTo.Equal(subnetBcast) && (sendTo.Equal(net.IPv4bcast) || sendTo.IsMulticast()) {
+ continue
+ }
+ // Expect that a socket bound to a unicast address does not receive
+ // packets sent to an address other than the bound unicast address.
+ //
+ // Note: we cannot use net.IP.IsGlobalUnicast to test this condition
+ // because IsGlobalUnicast does not check whether the address is the
+ // subnet broadcast, and returns true in that case.
+ expectData := !bindTo.Equal(dut.Net.RemoteIPv4) || sendTo.Equal(dut.Net.RemoteIPv4)
+ for _, bindToDevice := range []bool{true, false} {
+ testCases = append(
+ testCases,
+ testCase{
+ bindTo: bindTo,
+ sendTo: sendTo,
+ sendToBroadcast: sendTo.Equal(subnetBcast) || sendTo.Equal(net.IPv4bcast),
+ bindToDevice: bindToDevice,
+ expectData: expectData,
+ },
+ )
+ }
+ }
+ }
+ for _, tc := range testCases {
+ t.Run(fmt.Sprintf("bindTo=%s/sendTo=%s/bindToDevice=%t/expectData=%t", tc.bindTo, tc.sendTo, tc.bindToDevice, tc.expectData), func(t *testing.T) {
+ runTestCase(
+ t,
+ dut,
+ tc,
+ func(t *testing.T, dut testbench.DUT, conn udpConn, socketFD int32, tc testCase, payload []byte, layers testbench.Layers) {
+ conn.SendFrame(t, layers, &testbench.Payload{Bytes: payload})
+
+ if tc.expectData {
+ got, want := dut.Recv(t, socketFD, int32(len(payload)+1), 0), payload
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("received payload does not match sent payload, diff (-want, +got):\n%s", diff)
+ }
+ } else {
+ // Expected receive error, set a short receive timeout.
+ dut.SetSockOptTimeval(
+ t,
+ socketFD,
+ unix.SOL_SOCKET,
+ unix.SO_RCVTIMEO,
+ &unix.Timeval{
+ Sec: 1,
+ Usec: 0,
+ },
+ )
+ ret, recvPayload, errno := dut.RecvWithErrno(context.Background(), t, socketFD, 100, 0)
+ if errno != syscall.EAGAIN || errno != syscall.EWOULDBLOCK {
+ t.Errorf("Recv got unexpected result, ret=%d, payload=%q, errno=%s", ret, recvPayload, errno)
+ }
+ }
+ },
+ )
+ })
+ }
+ })
+}
+
+func runTestCase(
+ t *testing.T,
+ dut testbench.DUT,
+ tc testCase,
+ runTc func(t *testing.T, dut testbench.DUT, conn udpConn, socketFD int32, tc testCase, payload []byte, layers testbench.Layers),
+) {
+ var (
+ socketFD int32
+ outgoingUDP, incomingUDP testbench.UDP
+ )
+ if tc.bindTo != nil {
+ var remotePort uint16
+ socketFD, remotePort = dut.CreateBoundSocket(t, unix.SOCK_DGRAM, unix.IPPROTO_UDP, tc.bindTo)
+ outgoingUDP.DstPort = &remotePort
+ incomingUDP.SrcPort = &remotePort
+ } else {
+ // An unbound socket will auto-bind to INNADDR_ANY and a random
+ // port on sendto.
+ socketFD = dut.Socket(t, unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP)
+ }
+ defer dut.Close(t, socketFD)
+ if tc.bindToDevice {
+ dut.SetSockOpt(t, socketFD, unix.SOL_SOCKET, unix.SO_BINDTODEVICE, []byte(dut.Net.RemoteDevName))
+ }
+
+ var ethernetLayer testbench.Ether
+ if tc.sendToBroadcast {
+ dut.SetSockOptInt(t, socketFD, unix.SOL_SOCKET, unix.SO_BROADCAST, 1)
+
+ // When sending to broadcast (subnet or limited), the expected ethernet
+ // address is also broadcast.
+ ethernetBroadcastAddress := header.EthernetBroadcastAddress
+ ethernetLayer.DstAddr = &ethernetBroadcastAddress
+ } else if tc.sendTo.IsMulticast() {
+ ethernetMulticastAddress := header.EthernetAddressFromMulticastIPv4Address(tcpip.Address(tc.sendTo.To4()))
+ ethernetLayer.DstAddr = &ethernetMulticastAddress
+ }
+ expectedLayers := testbench.Layers{&ethernetLayer}
+
+ var conn udpConn
+ if sendTo4 := tc.sendTo.To4(); sendTo4 != nil {
+ v4Conn := dut.Net.NewUDPIPv4(t, outgoingUDP, incomingUDP)
+ conn = &v4Conn
+ expectedLayers = append(
+ expectedLayers,
+ &testbench.IPv4{
+ DstAddr: testbench.Address(tcpip.Address(sendTo4)),
+ },
+ )
+ } else {
+ v6Conn := dut.Net.NewUDPIPv6(t, outgoingUDP, incomingUDP)
+ conn = &v6Conn
+ expectedLayers = append(
+ expectedLayers,
+ &testbench.IPv6{
+ DstAddr: testbench.Address(tcpip.Address(tc.sendTo)),
+ },
+ )
+ }
+ defer conn.Close(t)
+
+ expectedLayers = append(expectedLayers, &incomingUDP)
+ for _, v := range []struct {
+ name string
+ payload []byte
+ }{
+ {"emptypayload", nil},
+ {"small payload", []byte("hello world")},
+ {"1kPayload", testbench.GenerateRandomPayload(t, 1<<10)},
+ // Even though UDP allows larger dgrams we don't test it here as
+ // they need to be fragmented and written out as individual
+ // frames.
+ } {
+ runTc(t, dut, conn, socketFD, tc, v.payload, expectedLayers)
}
}
diff --git a/test/syscalls/BUILD b/test/syscalls/BUILD
index 6ee2b73c1..e43f30ba3 100644
--- a/test/syscalls/BUILD
+++ b/test/syscalls/BUILD
@@ -557,6 +557,10 @@ syscall_test(
)
syscall_test(
+ test = "//test/syscalls/linux:setgid_test",
+)
+
+syscall_test(
add_overlay = True,
test = "//test/syscalls/linux:splice_test",
)
diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD
index 0da295e2d..80e2837f8 100644
--- a/test/syscalls/linux/BUILD
+++ b/test/syscalls/linux/BUILD
@@ -482,7 +482,9 @@ cc_binary(
"//test/util:fs_util",
"@com_google_absl//absl/strings",
gtest,
+ "//test/util:logging",
"//test/util:mount_util",
+ "//test/util:multiprocess_util",
"//test/util:temp_path",
"//test/util:test_main",
"//test/util:test_util",
@@ -582,6 +584,7 @@ cc_binary(
"//test/util:eventfd_util",
"//test/util:file_descriptor",
gtest,
+ "//test/util:fs_util",
"//test/util:posix_error",
"//test/util:temp_path",
"//test/util:test_main",
@@ -671,6 +674,7 @@ cc_binary(
gtest,
"//test/util:logging",
"//test/util:memory_util",
+ "//test/util:multiprocess_util",
"//test/util:signal_util",
"//test/util:test_main",
"//test/util:test_util",
@@ -1379,6 +1383,7 @@ cc_binary(
"//test/util:file_descriptor",
"//test/util:fs_util",
gtest,
+ "//test/util:posix_error",
"//test/util:temp_path",
"//test/util:temp_umask",
"//test/util:test_main",
@@ -2142,6 +2147,24 @@ cc_binary(
)
cc_binary(
+ name = "setgid_test",
+ testonly = 1,
+ srcs = ["setgid.cc"],
+ linkstatic = 1,
+ deps = [
+ "//test/util:capability_util",
+ "//test/util:cleanup",
+ "//test/util:fs_util",
+ "//test/util:posix_error",
+ "//test/util:temp_path",
+ "//test/util:test_main",
+ "//test/util:test_util",
+ "@com_google_absl//absl/strings",
+ gtest,
+ ],
+)
+
+cc_binary(
name = "splice_test",
testonly = 1,
srcs = ["splice.cc"],
@@ -3826,6 +3849,8 @@ cc_binary(
"@com_google_absl//absl/flags:flag",
"@com_google_absl//absl/strings",
gtest,
+ "//test/util:cleanup",
+ "//test/util:multiprocess_util",
"//test/util:posix_error",
"//test/util:test_main",
"//test/util:test_util",
@@ -4081,6 +4106,7 @@ cc_binary(
"@com_google_absl//absl/strings",
"@com_google_absl//absl/strings:str_format",
gtest,
+ "//test/util:cleanup",
"//test/util:test_main",
"//test/util:test_util",
],
diff --git a/test/syscalls/linux/chmod.cc b/test/syscalls/linux/chmod.cc
index a06b5cfd6..8233df0f8 100644
--- a/test/syscalls/linux/chmod.cc
+++ b/test/syscalls/linux/chmod.cc
@@ -98,6 +98,42 @@ TEST(ChmodTest, FchmodatBadF) {
ASSERT_THAT(fchmodat(-1, "foo", 0444, 0), SyscallFailsWithErrno(EBADF));
}
+TEST(ChmodTest, FchmodFileWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_PATH));
+
+ ASSERT_THAT(fchmod(fd.get(), 0444), SyscallFailsWithErrno(EBADF));
+}
+
+TEST(ChmodTest, FchmodDirWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ const auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ const auto fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(dir.path(), O_DIRECTORY | O_PATH));
+
+ ASSERT_THAT(fchmod(fd.get(), 0444), SyscallFailsWithErrno(EBADF));
+}
+
+TEST(ChmodTest, FchmodatWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ // Drop capabilities that allow us to override file permissions.
+ ASSERT_NO_ERRNO(SetCapability(CAP_DAC_OVERRIDE, false));
+
+ auto temp_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+
+ const auto parent_fd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(GetAbsoluteTestTmpdir().c_str(), O_PATH | O_DIRECTORY));
+
+ ASSERT_THAT(
+ fchmodat(parent_fd.get(), std::string(Basename(temp_file.path())).c_str(),
+ 0444, 0),
+ SyscallSucceeds());
+
+ EXPECT_THAT(open(temp_file.path().c_str(), O_RDWR),
+ SyscallFailsWithErrno(EACCES));
+}
+
TEST(ChmodTest, FchmodatNotDir) {
ASSERT_THAT(fchmodat(-1, "", 0444, 0), SyscallFailsWithErrno(ENOENT));
}
diff --git a/test/syscalls/linux/chown.cc b/test/syscalls/linux/chown.cc
index 5530ad18f..ff0d39343 100644
--- a/test/syscalls/linux/chown.cc
+++ b/test/syscalls/linux/chown.cc
@@ -48,6 +48,36 @@ TEST(ChownTest, FchownatBadF) {
ASSERT_THAT(fchownat(-1, "fff", 0, 0, 0), SyscallFailsWithErrno(EBADF));
}
+TEST(ChownTest, FchownFileWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_PATH));
+
+ ASSERT_THAT(fchown(fd.get(), geteuid(), getegid()),
+ SyscallFailsWithErrno(EBADF));
+}
+
+TEST(ChownTest, FchownDirWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ const auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ const auto fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(dir.path(), O_DIRECTORY | O_PATH));
+
+ ASSERT_THAT(fchown(fd.get(), geteuid(), getegid()),
+ SyscallFailsWithErrno(EBADF));
+}
+
+TEST(ChownTest, FchownatWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ const auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileIn(dir.path()));
+ const auto dirfd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(dir.path(), O_DIRECTORY | O_PATH));
+ ASSERT_THAT(
+ fchownat(dirfd.get(), file.path().c_str(), geteuid(), getegid(), 0),
+ SyscallSucceeds());
+}
+
TEST(ChownTest, FchownatEmptyPath) {
const auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
const auto fd =
@@ -209,6 +239,14 @@ INSTANTIATE_TEST_SUITE_P(
owner, group, 0);
MaybeSave();
return errorFromReturn("fchownat-dirfd", rc);
+ },
+ [](const std::string& path, uid_t owner, gid_t group) -> PosixError {
+ ASSIGN_OR_RETURN_ERRNO(auto dirfd, Open(std::string(Dirname(path)),
+ O_DIRECTORY | O_PATH));
+ int rc = fchownat(dirfd.get(), std::string(Basename(path)).c_str(),
+ owner, group, 0);
+ MaybeSave();
+ return errorFromReturn("fchownat-opathdirfd", rc);
}));
} // namespace
diff --git a/test/syscalls/linux/chroot.cc b/test/syscalls/linux/chroot.cc
index 85ec013d5..fab79d300 100644
--- a/test/syscalls/linux/chroot.cc
+++ b/test/syscalls/linux/chroot.cc
@@ -32,7 +32,9 @@
#include "test/util/cleanup.h"
#include "test/util/file_descriptor.h"
#include "test/util/fs_util.h"
+#include "test/util/logging.h"
#include "test/util/mount_util.h"
+#include "test/util/multiprocess_util.h"
#include "test/util/temp_path.h"
#include "test/util/test_util.h"
@@ -47,17 +49,20 @@ namespace {
TEST(ChrootTest, Success) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT)));
- auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
- EXPECT_THAT(chroot(temp_dir.path().c_str()), SyscallSucceeds());
+ const auto rest = [] {
+ auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ TEST_CHECK_SUCCESS(chroot(temp_dir.path().c_str()));
+ };
+ EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}
TEST(ChrootTest, PermissionDenied) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT)));
- // CAP_DAC_READ_SEARCH and CAP_DAC_OVERRIDE may override Execute permission on
- // directories.
- ASSERT_NO_ERRNO(SetCapability(CAP_DAC_READ_SEARCH, false));
- ASSERT_NO_ERRNO(SetCapability(CAP_DAC_OVERRIDE, false));
+ // CAP_DAC_READ_SEARCH and CAP_DAC_OVERRIDE may override Execute permission
+ // on directories.
+ AutoCapability cap_search(CAP_DAC_READ_SEARCH, false);
+ AutoCapability cap_override(CAP_DAC_OVERRIDE, false);
auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(
TempPath::CreateDirWith(GetAbsoluteTestTmpdir(), 0666 /* mode */));
@@ -78,8 +83,10 @@ TEST(ChrootTest, NotExist) {
}
TEST(ChrootTest, WithoutCapability) {
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETPCAP)));
+
// Unset CAP_SYS_CHROOT.
- ASSERT_NO_ERRNO(SetCapability(CAP_SYS_CHROOT, false));
+ AutoCapability cap(CAP_SYS_CHROOT, false);
auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
EXPECT_THAT(chroot(temp_dir.path().c_str()), SyscallFailsWithErrno(EPERM));
@@ -97,51 +104,53 @@ TEST(ChrootTest, CreatesNewRoot) {
auto file_in_new_root =
ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileIn(new_root.path()));
- // chroot into new_root.
- ASSERT_THAT(chroot(new_root.path().c_str()), SyscallSucceeds());
-
- // getcwd should return "(unreachable)" followed by the initial_cwd.
- char cwd[1024];
- ASSERT_THAT(syscall(__NR_getcwd, cwd, sizeof(cwd)), SyscallSucceeds());
- std::string expected_cwd = "(unreachable)";
- expected_cwd += initial_cwd;
- EXPECT_STREQ(cwd, expected_cwd.c_str());
-
- // Should not be able to stat file by its full path.
- struct stat statbuf;
- EXPECT_THAT(stat(file_in_new_root.path().c_str(), &statbuf),
- SyscallFailsWithErrno(ENOENT));
-
- // Should be able to stat file at new rooted path.
- auto basename = std::string(Basename(file_in_new_root.path()));
- auto rootedFile = "/" + basename;
- ASSERT_THAT(stat(rootedFile.c_str(), &statbuf), SyscallSucceeds());
-
- // Should be able to stat cwd at '.' even though it's outside root.
- ASSERT_THAT(stat(".", &statbuf), SyscallSucceeds());
-
- // chdir into new root.
- ASSERT_THAT(chdir("/"), SyscallSucceeds());
-
- // getcwd should return "/".
- EXPECT_THAT(syscall(__NR_getcwd, cwd, sizeof(cwd)), SyscallSucceeds());
- EXPECT_STREQ(cwd, "/");
-
- // Statting '.', '..', '/', and '/..' all return the same dev and inode.
- struct stat statbuf_dot;
- ASSERT_THAT(stat(".", &statbuf_dot), SyscallSucceeds());
- struct stat statbuf_dotdot;
- ASSERT_THAT(stat("..", &statbuf_dotdot), SyscallSucceeds());
- EXPECT_EQ(statbuf_dot.st_dev, statbuf_dotdot.st_dev);
- EXPECT_EQ(statbuf_dot.st_ino, statbuf_dotdot.st_ino);
- struct stat statbuf_slash;
- ASSERT_THAT(stat("/", &statbuf_slash), SyscallSucceeds());
- EXPECT_EQ(statbuf_dot.st_dev, statbuf_slash.st_dev);
- EXPECT_EQ(statbuf_dot.st_ino, statbuf_slash.st_ino);
- struct stat statbuf_slashdotdot;
- ASSERT_THAT(stat("/..", &statbuf_slashdotdot), SyscallSucceeds());
- EXPECT_EQ(statbuf_dot.st_dev, statbuf_slashdotdot.st_dev);
- EXPECT_EQ(statbuf_dot.st_ino, statbuf_slashdotdot.st_ino);
+ const auto rest = [&] {
+ // chroot into new_root.
+ TEST_CHECK_SUCCESS(chroot(new_root.path().c_str()));
+
+ // getcwd should return "(unreachable)" followed by the initial_cwd.
+ char cwd[1024];
+ TEST_CHECK_SUCCESS(syscall(__NR_getcwd, cwd, sizeof(cwd)));
+ std::string expected_cwd = "(unreachable)";
+ expected_cwd += initial_cwd;
+ TEST_CHECK(strcmp(cwd, expected_cwd.c_str()) == 0);
+
+ // Should not be able to stat file by its full path.
+ struct stat statbuf;
+ TEST_CHECK_ERRNO(stat(file_in_new_root.path().c_str(), &statbuf), ENOENT);
+
+ // Should be able to stat file at new rooted path.
+ auto basename = std::string(Basename(file_in_new_root.path()));
+ auto rootedFile = "/" + basename;
+ TEST_CHECK_SUCCESS(stat(rootedFile.c_str(), &statbuf));
+
+ // Should be able to stat cwd at '.' even though it's outside root.
+ TEST_CHECK_SUCCESS(stat(".", &statbuf));
+
+ // chdir into new root.
+ TEST_CHECK_SUCCESS(chdir("/"));
+
+ // getcwd should return "/".
+ TEST_CHECK_SUCCESS(syscall(__NR_getcwd, cwd, sizeof(cwd)));
+ TEST_CHECK_SUCCESS(strcmp(cwd, "/") == 0);
+
+ // Statting '.', '..', '/', and '/..' all return the same dev and inode.
+ struct stat statbuf_dot;
+ TEST_CHECK_SUCCESS(stat(".", &statbuf_dot));
+ struct stat statbuf_dotdot;
+ TEST_CHECK_SUCCESS(stat("..", &statbuf_dotdot));
+ TEST_CHECK(statbuf_dot.st_dev == statbuf_dotdot.st_dev);
+ TEST_CHECK(statbuf_dot.st_ino == statbuf_dotdot.st_ino);
+ struct stat statbuf_slash;
+ TEST_CHECK_SUCCESS(stat("/", &statbuf_slash));
+ TEST_CHECK(statbuf_dot.st_dev == statbuf_slash.st_dev);
+ TEST_CHECK(statbuf_dot.st_ino == statbuf_slash.st_ino);
+ struct stat statbuf_slashdotdot;
+ TEST_CHECK_SUCCESS(stat("/..", &statbuf_slashdotdot));
+ TEST_CHECK(statbuf_dot.st_dev == statbuf_slashdotdot.st_dev);
+ TEST_CHECK(statbuf_dot.st_ino == statbuf_slashdotdot.st_ino);
+ };
+ EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}
TEST(ChrootTest, DotDotFromOpenFD) {
@@ -152,18 +161,20 @@ TEST(ChrootTest, DotDotFromOpenFD) {
Open(dir_outside_root.path(), O_RDONLY | O_DIRECTORY));
auto new_root = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
- // chroot into new_root.
- ASSERT_THAT(chroot(new_root.path().c_str()), SyscallSucceeds());
+ const auto rest = [&] {
+ // chroot into new_root.
+ TEST_CHECK_SUCCESS(chroot(new_root.path().c_str()));
- // openat on fd with path .. will succeed.
- int other_fd;
- ASSERT_THAT(other_fd = openat(fd.get(), "..", O_RDONLY), SyscallSucceeds());
- EXPECT_THAT(close(other_fd), SyscallSucceeds());
+ // openat on fd with path .. will succeed.
+ int other_fd;
+ TEST_CHECK_SUCCESS(other_fd = openat(fd.get(), "..", O_RDONLY));
+ TEST_CHECK_SUCCESS(close(other_fd));
- // getdents on fd should not error.
- char buf[1024];
- ASSERT_THAT(syscall(SYS_getdents64, fd.get(), buf, sizeof(buf)),
- SyscallSucceeds());
+ // getdents on fd should not error.
+ char buf[1024];
+ TEST_CHECK_SUCCESS(syscall(SYS_getdents64, fd.get(), buf, sizeof(buf)));
+ };
+ EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}
// Test that link resolution in a chroot can escape the root by following an
@@ -179,24 +190,27 @@ TEST(ChrootTest, ProcFdLinkResolutionInChroot) {
const FileDescriptor proc_fd = ASSERT_NO_ERRNO_AND_VALUE(
Open("/proc", O_DIRECTORY | O_RDONLY | O_CLOEXEC));
- auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
- ASSERT_THAT(chroot(temp_dir.path().c_str()), SyscallSucceeds());
-
- // Opening relative to an already open fd to a node outside the chroot works.
- const FileDescriptor proc_self_fd = ASSERT_NO_ERRNO_AND_VALUE(
- OpenAt(proc_fd.get(), "self/fd", O_DIRECTORY | O_RDONLY | O_CLOEXEC));
-
- // Proc fd symlinks can escape the chroot if the fd the symlink refers to
- // refers to an object outside the chroot.
- struct stat s = {};
- EXPECT_THAT(
- fstatat(proc_self_fd.get(), absl::StrCat(fd.get()).c_str(), &s, 0),
- SyscallSucceeds());
-
- // Try to stat the stdin fd. Internally, this is handled differently from a
- // proc fd entry pointing to a file, since stdin is backed by a host fd, and
- // isn't a walkable path on the filesystem inside the sandbox.
- EXPECT_THAT(fstatat(proc_self_fd.get(), "0", &s, 0), SyscallSucceeds());
+ const auto rest = [&] {
+ auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ TEST_CHECK_SUCCESS(chroot(temp_dir.path().c_str()));
+
+ // Opening relative to an already open fd to a node outside the chroot
+ // works.
+ const FileDescriptor proc_self_fd = TEST_CHECK_NO_ERRNO_AND_VALUE(
+ OpenAt(proc_fd.get(), "self/fd", O_DIRECTORY | O_RDONLY | O_CLOEXEC));
+
+ // Proc fd symlinks can escape the chroot if the fd the symlink refers to
+ // refers to an object outside the chroot.
+ struct stat s = {};
+ TEST_CHECK_SUCCESS(
+ fstatat(proc_self_fd.get(), absl::StrCat(fd.get()).c_str(), &s, 0));
+
+ // Try to stat the stdin fd. Internally, this is handled differently from a
+ // proc fd entry pointing to a file, since stdin is backed by a host fd, and
+ // isn't a walkable path on the filesystem inside the sandbox.
+ TEST_CHECK_SUCCESS(fstatat(proc_self_fd.get(), "0", &s, 0));
+ };
+ EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}
// This test will verify that when you hold a fd to proc before entering
@@ -209,28 +223,30 @@ TEST(ChrootTest, ProcMemSelfFdsNoEscapeProcOpen) {
const FileDescriptor proc =
ASSERT_NO_ERRNO_AND_VALUE(Open("/proc", O_RDONLY));
- // Create and enter a chroot directory.
- const auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
- ASSERT_THAT(chroot(temp_dir.path().c_str()), SyscallSucceeds());
-
- // Open a file inside the chroot at /foo.
- const FileDescriptor foo =
- ASSERT_NO_ERRNO_AND_VALUE(Open("/foo", O_CREAT | O_RDONLY, 0644));
-
- // Examine /proc/self/fd/{foo_fd} to see if it exposes the fact that we're
- // inside a chroot, the path should be /foo and NOT {chroot_dir}/foo.
- const std::string fd_path = absl::StrCat("self/fd/", foo.get());
- char buf[1024] = {};
- size_t bytes_read = 0;
- ASSERT_THAT(bytes_read =
- readlinkat(proc.get(), fd_path.c_str(), buf, sizeof(buf) - 1),
- SyscallSucceeds());
-
- // The link should resolve to something.
- ASSERT_GT(bytes_read, 0);
-
- // Assert that the link doesn't contain the chroot path and is only /foo.
- EXPECT_STREQ(buf, "/foo");
+ const auto rest = [&] {
+ // Create and enter a chroot directory.
+ const auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ TEST_CHECK_SUCCESS(chroot(temp_dir.path().c_str()));
+
+ // Open a file inside the chroot at /foo.
+ const FileDescriptor foo =
+ TEST_CHECK_NO_ERRNO_AND_VALUE(Open("/foo", O_CREAT | O_RDONLY, 0644));
+
+ // Examine /proc/self/fd/{foo_fd} to see if it exposes the fact that we're
+ // inside a chroot, the path should be /foo and NOT {chroot_dir}/foo.
+ const std::string fd_path = absl::StrCat("self/fd/", foo.get());
+ char buf[1024] = {};
+ size_t bytes_read = 0;
+ TEST_CHECK_SUCCESS(bytes_read = readlinkat(proc.get(), fd_path.c_str(), buf,
+ sizeof(buf) - 1));
+
+ // The link should resolve to something.
+ TEST_CHECK(bytes_read > 0);
+
+ // Assert that the link doesn't contain the chroot path and is only /foo.
+ TEST_CHECK(strcmp(buf, "/foo") == 0);
+ };
+ EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}
// This test will verify that a file inside a chroot when mmapped will not
@@ -242,39 +258,41 @@ TEST(ChrootTest, ProcMemSelfMapsNoEscapeProcOpen) {
const FileDescriptor proc =
ASSERT_NO_ERRNO_AND_VALUE(Open("/proc", O_RDONLY));
- // Create and enter a chroot directory.
- const auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
- ASSERT_THAT(chroot(temp_dir.path().c_str()), SyscallSucceeds());
-
- // Open a file inside the chroot at /foo.
- const FileDescriptor foo =
- ASSERT_NO_ERRNO_AND_VALUE(Open("/foo", O_CREAT | O_RDONLY, 0644));
-
- // Mmap the newly created file.
- void* foo_map = mmap(nullptr, kPageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE,
- foo.get(), 0);
- ASSERT_THAT(reinterpret_cast<int64_t>(foo_map), SyscallSucceeds());
-
- // Always unmap.
- auto cleanup_map = Cleanup(
- [&] { EXPECT_THAT(munmap(foo_map, kPageSize), SyscallSucceeds()); });
-
- // Examine /proc/self/maps to be sure that /foo doesn't appear to be
- // mapped with the full chroot path.
- const FileDescriptor maps =
- ASSERT_NO_ERRNO_AND_VALUE(OpenAt(proc.get(), "self/maps", O_RDONLY));
-
- size_t bytes_read = 0;
- char buf[8 * 1024] = {};
- ASSERT_THAT(bytes_read = ReadFd(maps.get(), buf, sizeof(buf)),
- SyscallSucceeds());
-
- // The maps file should have something.
- ASSERT_GT(bytes_read, 0);
-
- // Finally we want to make sure the maps don't contain the chroot path
- ASSERT_EQ(std::string(buf, bytes_read).find(temp_dir.path()),
- std::string::npos);
+ const auto rest = [&] {
+ // Create and enter a chroot directory.
+ const auto temp_dir = TEST_CHECK_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ TEST_CHECK_SUCCESS(chroot(temp_dir.path().c_str()));
+
+ // Open a file inside the chroot at /foo.
+ const FileDescriptor foo =
+ TEST_CHECK_NO_ERRNO_AND_VALUE(Open("/foo", O_CREAT | O_RDONLY, 0644));
+
+ // Mmap the newly created file.
+ void* foo_map = mmap(nullptr, kPageSize, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE, foo.get(), 0);
+ TEST_CHECK_SUCCESS(reinterpret_cast<int64_t>(foo_map));
+
+ // Always unmap.
+ auto cleanup_map =
+ Cleanup([&] { TEST_CHECK_SUCCESS(munmap(foo_map, kPageSize)); });
+
+ // Examine /proc/self/maps to be sure that /foo doesn't appear to be
+ // mapped with the full chroot path.
+ const FileDescriptor maps = TEST_CHECK_NO_ERRNO_AND_VALUE(
+ OpenAt(proc.get(), "self/maps", O_RDONLY));
+
+ size_t bytes_read = 0;
+ char buf[8 * 1024] = {};
+ TEST_CHECK_SUCCESS(bytes_read = ReadFd(maps.get(), buf, sizeof(buf)));
+
+ // The maps file should have something.
+ TEST_CHECK(bytes_read > 0);
+
+ // Finally we want to make sure the maps don't contain the chroot path
+ TEST_CHECK(std::string(buf, bytes_read).find(temp_dir.path()) ==
+ std::string::npos);
+ };
+ EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}
// Test that mounts outside the chroot will not appear in /proc/self/mounts or
@@ -283,81 +301,76 @@ TEST(ChrootTest, ProcMountsMountinfoNoEscape) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_CHROOT)));
- // We are going to create some mounts and then chroot. In order to be able to
- // unmount the mounts after the test run, we must chdir to the root and use
- // relative paths for all mounts. That way, as long as we never chdir into
- // the new root, we can access the mounts via relative paths and unmount them.
- ASSERT_THAT(chdir("/"), SyscallSucceeds());
-
- // Create nested tmpfs mounts. Note the use of relative paths in Mount calls.
+ // Create nested tmpfs mounts.
auto const outer_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
- auto const outer_mount = ASSERT_NO_ERRNO_AND_VALUE(Mount(
- "none", JoinPath(".", outer_dir.path()), "tmpfs", 0, "mode=0700", 0));
+ auto const outer_mount = ASSERT_NO_ERRNO_AND_VALUE(
+ Mount("none", outer_dir.path(), "tmpfs", 0, "mode=0700", 0));
auto const inner_dir =
ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(outer_dir.path()));
- auto const inner_mount = ASSERT_NO_ERRNO_AND_VALUE(Mount(
- "none", JoinPath(".", inner_dir.path()), "tmpfs", 0, "mode=0700", 0));
-
- // Filenames that will be checked for mounts, all relative to /proc dir.
- std::string paths[3] = {"mounts", "self/mounts", "self/mountinfo"};
-
- for (const std::string& path : paths) {
- // We should have both inner and outer mounts.
- const std::string contents =
- ASSERT_NO_ERRNO_AND_VALUE(GetContents(JoinPath("/proc", path)));
- EXPECT_THAT(contents, AllOf(HasSubstr(outer_dir.path()),
- HasSubstr(inner_dir.path())));
- // We better have at least two mounts: the mounts we created plus the root.
- std::vector<absl::string_view> submounts =
- absl::StrSplit(contents, '\n', absl::SkipWhitespace());
- EXPECT_GT(submounts.size(), 2);
- }
-
- // Get a FD to /proc before we enter the chroot.
- const FileDescriptor proc =
- ASSERT_NO_ERRNO_AND_VALUE(Open("/proc", O_RDONLY));
-
- // Chroot to outer mount.
- ASSERT_THAT(chroot(outer_dir.path().c_str()), SyscallSucceeds());
-
- for (const std::string& path : paths) {
- const FileDescriptor proc_file =
- ASSERT_NO_ERRNO_AND_VALUE(OpenAt(proc.get(), path, O_RDONLY));
-
- // Only two mounts visible from this chroot: the inner and outer. Both
- // paths should be relative to the new chroot.
- const std::string contents =
- ASSERT_NO_ERRNO_AND_VALUE(GetContentsFD(proc_file.get()));
- EXPECT_THAT(contents,
- AllOf(HasSubstr(absl::StrCat(Basename(inner_dir.path()))),
- Not(HasSubstr(outer_dir.path())),
- Not(HasSubstr(inner_dir.path()))));
- std::vector<absl::string_view> submounts =
- absl::StrSplit(contents, '\n', absl::SkipWhitespace());
- EXPECT_EQ(submounts.size(), 2);
- }
-
- // Chroot to inner mount. We must use an absolute path accessible to our
- // chroot.
- const std::string inner_dir_basename =
- absl::StrCat("/", Basename(inner_dir.path()));
- ASSERT_THAT(chroot(inner_dir_basename.c_str()), SyscallSucceeds());
-
- for (const std::string& path : paths) {
- const FileDescriptor proc_file =
- ASSERT_NO_ERRNO_AND_VALUE(OpenAt(proc.get(), path, O_RDONLY));
- const std::string contents =
- ASSERT_NO_ERRNO_AND_VALUE(GetContentsFD(proc_file.get()));
-
- // Only the inner mount visible from this chroot.
- std::vector<absl::string_view> submounts =
- absl::StrSplit(contents, '\n', absl::SkipWhitespace());
- EXPECT_EQ(submounts.size(), 1);
- }
-
- // Chroot back to ".".
- ASSERT_THAT(chroot("."), SyscallSucceeds());
+ auto const inner_mount = ASSERT_NO_ERRNO_AND_VALUE(
+ Mount("none", inner_dir.path(), "tmpfs", 0, "mode=0700", 0));
+
+ const auto rest = [&outer_dir, &inner_dir] {
+ // Filenames that will be checked for mounts, all relative to /proc dir.
+ std::string paths[3] = {"mounts", "self/mounts", "self/mountinfo"};
+
+ for (const std::string& path : paths) {
+ // We should have both inner and outer mounts.
+ const std::string contents =
+ TEST_CHECK_NO_ERRNO_AND_VALUE(GetContents(JoinPath("/proc", path)));
+ EXPECT_THAT(contents, AllOf(HasSubstr(outer_dir.path()),
+ HasSubstr(inner_dir.path())));
+ // We better have at least two mounts: the mounts we created plus the
+ // root.
+ std::vector<absl::string_view> submounts =
+ absl::StrSplit(contents, '\n', absl::SkipWhitespace());
+ TEST_CHECK(submounts.size() > 2);
+ }
+
+ // Get a FD to /proc before we enter the chroot.
+ const FileDescriptor proc =
+ TEST_CHECK_NO_ERRNO_AND_VALUE(Open("/proc", O_RDONLY));
+
+ // Chroot to outer mount.
+ TEST_CHECK_SUCCESS(chroot(outer_dir.path().c_str()));
+
+ for (const std::string& path : paths) {
+ const FileDescriptor proc_file =
+ TEST_CHECK_NO_ERRNO_AND_VALUE(OpenAt(proc.get(), path, O_RDONLY));
+
+ // Only two mounts visible from this chroot: the inner and outer. Both
+ // paths should be relative to the new chroot.
+ const std::string contents =
+ TEST_CHECK_NO_ERRNO_AND_VALUE(GetContentsFD(proc_file.get()));
+ EXPECT_THAT(contents,
+ AllOf(HasSubstr(absl::StrCat(Basename(inner_dir.path()))),
+ Not(HasSubstr(outer_dir.path())),
+ Not(HasSubstr(inner_dir.path()))));
+ std::vector<absl::string_view> submounts =
+ absl::StrSplit(contents, '\n', absl::SkipWhitespace());
+ TEST_CHECK(submounts.size() == 2);
+ }
+
+ // Chroot to inner mount. We must use an absolute path accessible to our
+ // chroot.
+ const std::string inner_dir_basename =
+ absl::StrCat("/", Basename(inner_dir.path()));
+ TEST_CHECK_SUCCESS(chroot(inner_dir_basename.c_str()));
+
+ for (const std::string& path : paths) {
+ const FileDescriptor proc_file =
+ TEST_CHECK_NO_ERRNO_AND_VALUE(OpenAt(proc.get(), path, O_RDONLY));
+ const std::string contents =
+ TEST_CHECK_NO_ERRNO_AND_VALUE(GetContentsFD(proc_file.get()));
+
+ // Only the inner mount visible from this chroot.
+ std::vector<absl::string_view> submounts =
+ absl::StrSplit(contents, '\n', absl::SkipWhitespace());
+ TEST_CHECK(submounts.size() == 1);
+ }
+ };
+ EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}
} // namespace
diff --git a/test/syscalls/linux/dup.cc b/test/syscalls/linux/dup.cc
index 4f773bc75..ba4e13fb9 100644
--- a/test/syscalls/linux/dup.cc
+++ b/test/syscalls/linux/dup.cc
@@ -18,6 +18,7 @@
#include "gtest/gtest.h"
#include "test/util/eventfd_util.h"
#include "test/util/file_descriptor.h"
+#include "test/util/fs_util.h"
#include "test/util/posix_error.h"
#include "test/util/temp_path.h"
#include "test/util/test_util.h"
@@ -44,14 +45,6 @@ PosixErrorOr<FileDescriptor> Dup3(const FileDescriptor& fd, int target_fd,
return FileDescriptor(new_fd);
}
-void CheckSameFile(const FileDescriptor& fd1, const FileDescriptor& fd2) {
- struct stat stat_result1, stat_result2;
- ASSERT_THAT(fstat(fd1.get(), &stat_result1), SyscallSucceeds());
- ASSERT_THAT(fstat(fd2.get(), &stat_result2), SyscallSucceeds());
- EXPECT_EQ(stat_result1.st_dev, stat_result2.st_dev);
- EXPECT_EQ(stat_result1.st_ino, stat_result2.st_ino);
-}
-
TEST(DupTest, Dup) {
auto f = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(f.path(), O_RDONLY));
@@ -59,7 +52,7 @@ TEST(DupTest, Dup) {
// Dup the descriptor and make sure it's the same file.
FileDescriptor nfd = ASSERT_NO_ERRNO_AND_VALUE(fd.Dup());
ASSERT_NE(fd.get(), nfd.get());
- CheckSameFile(fd, nfd);
+ ASSERT_NO_ERRNO(CheckSameFile(fd, nfd));
}
TEST(DupTest, DupClearsCloExec) {
@@ -70,10 +63,24 @@ TEST(DupTest, DupClearsCloExec) {
// Duplicate the descriptor. Ensure that it doesn't have FD_CLOEXEC set.
FileDescriptor nfd = ASSERT_NO_ERRNO_AND_VALUE(fd.Dup());
ASSERT_NE(fd.get(), nfd.get());
- CheckSameFile(fd, nfd);
+ ASSERT_NO_ERRNO(CheckSameFile(fd, nfd));
EXPECT_THAT(fcntl(nfd.get(), F_GETFD), SyscallSucceedsWithValue(0));
}
+TEST(DupTest, DupWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ auto f = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(f.path(), O_PATH));
+ int flags;
+ ASSERT_THAT(flags = fcntl(fd.get(), F_GETFL), SyscallSucceeds());
+
+ // Dup the descriptor and make sure it's the same file.
+ FileDescriptor nfd = ASSERT_NO_ERRNO_AND_VALUE(fd.Dup());
+ ASSERT_NE(fd.get(), nfd.get());
+ ASSERT_NO_ERRNO(CheckSameFile(fd, nfd));
+ EXPECT_THAT(fcntl(nfd.get(), F_GETFL), SyscallSucceedsWithValue(flags));
+}
+
TEST(DupTest, Dup2) {
auto f = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(f.path(), O_RDONLY));
@@ -82,13 +89,13 @@ TEST(DupTest, Dup2) {
FileDescriptor nfd = ASSERT_NO_ERRNO_AND_VALUE(fd.Dup());
ASSERT_NE(fd.get(), nfd.get());
- CheckSameFile(fd, nfd);
+ ASSERT_NO_ERRNO(CheckSameFile(fd, nfd));
// Dup over the file above.
int target_fd = nfd.release();
FileDescriptor nfd2 = ASSERT_NO_ERRNO_AND_VALUE(Dup2(fd, target_fd));
EXPECT_EQ(target_fd, nfd2.get());
- CheckSameFile(fd, nfd2);
+ ASSERT_NO_ERRNO(CheckSameFile(fd, nfd2));
}
TEST(DupTest, Dup2SameFD) {
@@ -99,6 +106,28 @@ TEST(DupTest, Dup2SameFD) {
ASSERT_THAT(dup2(fd.get(), fd.get()), SyscallSucceedsWithValue(fd.get()));
}
+TEST(DupTest, Dup2WithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ auto f = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(f.path(), O_PATH));
+ int flags;
+ ASSERT_THAT(flags = fcntl(fd.get(), F_GETFL), SyscallSucceeds());
+
+ // Regular dup once.
+ FileDescriptor nfd = ASSERT_NO_ERRNO_AND_VALUE(fd.Dup());
+
+ ASSERT_NE(fd.get(), nfd.get());
+ ASSERT_NO_ERRNO(CheckSameFile(fd, nfd));
+ EXPECT_THAT(fcntl(nfd.get(), F_GETFL), SyscallSucceedsWithValue(flags));
+
+ // Dup over the file above.
+ int target_fd = nfd.release();
+ FileDescriptor nfd2 = ASSERT_NO_ERRNO_AND_VALUE(Dup2(fd, target_fd));
+ EXPECT_EQ(target_fd, nfd2.get());
+ ASSERT_NO_ERRNO(CheckSameFile(fd, nfd2));
+ EXPECT_THAT(fcntl(nfd2.get(), F_GETFL), SyscallSucceedsWithValue(flags));
+}
+
TEST(DupTest, Dup3) {
auto f = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(f.path(), O_RDONLY));
@@ -106,16 +135,16 @@ TEST(DupTest, Dup3) {
// Regular dup once.
FileDescriptor nfd = ASSERT_NO_ERRNO_AND_VALUE(fd.Dup());
ASSERT_NE(fd.get(), nfd.get());
- CheckSameFile(fd, nfd);
+ ASSERT_NO_ERRNO(CheckSameFile(fd, nfd));
// Dup over the file above, check that it has no CLOEXEC.
nfd = ASSERT_NO_ERRNO_AND_VALUE(Dup3(fd, nfd.release(), 0));
- CheckSameFile(fd, nfd);
+ ASSERT_NO_ERRNO(CheckSameFile(fd, nfd));
EXPECT_THAT(fcntl(nfd.get(), F_GETFD), SyscallSucceedsWithValue(0));
// Dup over the file again, check that it does not CLOEXEC.
nfd = ASSERT_NO_ERRNO_AND_VALUE(Dup3(fd, nfd.release(), O_CLOEXEC));
- CheckSameFile(fd, nfd);
+ ASSERT_NO_ERRNO(CheckSameFile(fd, nfd));
EXPECT_THAT(fcntl(nfd.get(), F_GETFD), SyscallSucceedsWithValue(FD_CLOEXEC));
}
@@ -127,6 +156,32 @@ TEST(DupTest, Dup3FailsSameFD) {
ASSERT_THAT(dup3(fd.get(), fd.get(), 0), SyscallFailsWithErrno(EINVAL));
}
+TEST(DupTest, Dup3WithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ auto f = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(f.path(), O_PATH));
+ EXPECT_THAT(fcntl(fd.get(), F_GETFD), SyscallSucceedsWithValue(0));
+ int flags;
+ ASSERT_THAT(flags = fcntl(fd.get(), F_GETFL), SyscallSucceeds());
+
+ // Regular dup once.
+ FileDescriptor nfd = ASSERT_NO_ERRNO_AND_VALUE(fd.Dup());
+ ASSERT_NE(fd.get(), nfd.get());
+ ASSERT_NO_ERRNO(CheckSameFile(fd, nfd));
+
+ // Dup over the file above, check that it has no CLOEXEC.
+ nfd = ASSERT_NO_ERRNO_AND_VALUE(Dup3(fd, nfd.release(), 0));
+ ASSERT_NO_ERRNO(CheckSameFile(fd, nfd));
+ EXPECT_THAT(fcntl(nfd.get(), F_GETFD), SyscallSucceedsWithValue(0));
+ EXPECT_THAT(fcntl(nfd.get(), F_GETFL), SyscallSucceedsWithValue(flags));
+
+ // Dup over the file again, check that it does not CLOEXEC.
+ nfd = ASSERT_NO_ERRNO_AND_VALUE(Dup3(fd, nfd.release(), O_CLOEXEC));
+ ASSERT_NO_ERRNO(CheckSameFile(fd, nfd));
+ EXPECT_THAT(fcntl(nfd.get(), F_GETFD), SyscallSucceedsWithValue(FD_CLOEXEC));
+ EXPECT_THAT(fcntl(nfd.get(), F_GETFL), SyscallSucceedsWithValue(flags));
+}
+
} // namespace
} // namespace testing
diff --git a/test/syscalls/linux/fadvise64.cc b/test/syscalls/linux/fadvise64.cc
index 2af7aa6d9..ac24c4066 100644
--- a/test/syscalls/linux/fadvise64.cc
+++ b/test/syscalls/linux/fadvise64.cc
@@ -45,6 +45,17 @@ TEST(FAdvise64Test, Basic) {
SyscallSucceeds());
}
+TEST(FAdvise64Test, FAdvise64WithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ const auto fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_PATH));
+
+ ASSERT_THAT(syscall(__NR_fadvise64, fd.get(), 0, 10, POSIX_FADV_NORMAL),
+ SyscallFailsWithErrno(EBADF));
+ ASSERT_THAT(syscall(__NR_fadvise64, fd.get(), 0, 10, POSIX_FADV_NORMAL),
+ SyscallFailsWithErrno(EBADF));
+}
+
TEST(FAdvise64Test, InvalidArgs) {
auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
const auto fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDONLY));
diff --git a/test/syscalls/linux/fallocate.cc b/test/syscalls/linux/fallocate.cc
index edd23e063..5c839447e 100644
--- a/test/syscalls/linux/fallocate.cc
+++ b/test/syscalls/linux/fallocate.cc
@@ -108,6 +108,13 @@ TEST_F(AllocateTest, FallocateReadonly) {
EXPECT_THAT(fallocate(fd.get(), 0, 0, 10), SyscallFailsWithErrno(EBADF));
}
+TEST_F(AllocateTest, FallocateWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_PATH));
+ EXPECT_THAT(fallocate(fd.get(), 0, 0, 10), SyscallFailsWithErrno(EBADF));
+}
+
TEST_F(AllocateTest, FallocatePipe) {
int pipes[2];
EXPECT_THAT(pipe(pipes), SyscallSucceeds());
diff --git a/test/syscalls/linux/fchdir.cc b/test/syscalls/linux/fchdir.cc
index 08bcae1e8..c6675802d 100644
--- a/test/syscalls/linux/fchdir.cc
+++ b/test/syscalls/linux/fchdir.cc
@@ -71,6 +71,18 @@ TEST(FchdirTest, NotDir) {
EXPECT_THAT(close(fd), SyscallSucceeds());
}
+TEST(FchdirTest, FchdirWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(temp_dir.path(), O_PATH));
+ ASSERT_THAT(open(temp_dir.path().c_str(), O_DIRECTORY | O_PATH),
+ SyscallSucceeds());
+
+ EXPECT_THAT(fchdir(fd.get()), SyscallSucceeds());
+ // Change CWD to a permanent location as temp dirs will be cleaned up.
+ EXPECT_THAT(chdir("/"), SyscallSucceeds());
+}
+
} // namespace
} // namespace testing
diff --git a/test/syscalls/linux/fcntl.cc b/test/syscalls/linux/fcntl.cc
index 75a5c9f17..4fa6751ff 100644
--- a/test/syscalls/linux/fcntl.cc
+++ b/test/syscalls/linux/fcntl.cc
@@ -207,6 +207,41 @@ PosixErrorOr<Cleanup> SubprocessLock(std::string const& path, bool for_write,
return std::move(cleanup);
}
+TEST(FcntlTest, FcntlDupWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ auto f = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(f.path(), O_PATH));
+
+ int new_fd;
+ // Dup the descriptor and make sure it's the same file.
+ EXPECT_THAT(new_fd = fcntl(fd.get(), F_DUPFD, 0), SyscallSucceeds());
+
+ FileDescriptor nfd = FileDescriptor(new_fd);
+ ASSERT_NE(fd.get(), nfd.get());
+ ASSERT_NO_ERRNO(CheckSameFile(fd, nfd));
+ EXPECT_THAT(fcntl(nfd.get(), F_GETFL), SyscallSucceedsWithValue(O_PATH));
+}
+
+TEST(FcntlTest, SetFileStatusFlagWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ TempPath path = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(path.path(), O_PATH));
+
+ EXPECT_THAT(fcntl(fd.get(), F_SETFL, 0), SyscallFailsWithErrno(EBADF));
+}
+
+TEST(FcntlTest, BadFcntlsWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ TempPath path = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(path.path(), O_PATH));
+
+ EXPECT_THAT(fcntl(fd.get(), F_SETOWN, 0), SyscallFailsWithErrno(EBADF));
+ EXPECT_THAT(fcntl(fd.get(), F_GETOWN, 0), SyscallFailsWithErrno(EBADF));
+
+ EXPECT_THAT(fcntl(fd.get(), F_SETOWN_EX, 0), SyscallFailsWithErrno(EBADF));
+ EXPECT_THAT(fcntl(fd.get(), F_GETOWN_EX, 0), SyscallFailsWithErrno(EBADF));
+}
+
TEST(FcntlTest, SetCloExecBadFD) {
// Open an eventfd file descriptor with FD_CLOEXEC descriptor flag not set.
FileDescriptor f = ASSERT_NO_ERRNO_AND_VALUE(NewEventFD(0, 0));
@@ -226,6 +261,32 @@ TEST(FcntlTest, SetCloExec) {
ASSERT_THAT(fcntl(fd.get(), F_GETFD), SyscallSucceedsWithValue(FD_CLOEXEC));
}
+TEST(FcntlTest, SetCloExecWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ // Open a file descriptor with FD_CLOEXEC descriptor flag not set.
+ TempPath path = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(path.path(), O_PATH));
+ ASSERT_THAT(fcntl(fd.get(), F_GETFD), SyscallSucceedsWithValue(0));
+
+ // Set the FD_CLOEXEC flag.
+ ASSERT_THAT(fcntl(fd.get(), F_SETFD, FD_CLOEXEC), SyscallSucceeds());
+ ASSERT_THAT(fcntl(fd.get(), F_GETFD), SyscallSucceedsWithValue(FD_CLOEXEC));
+}
+
+TEST(FcntlTest, DupFDCloExecWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ // Open a file descriptor with FD_CLOEXEC descriptor flag not set.
+ TempPath path = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(path.path(), O_PATH));
+ int nfd;
+ ASSERT_THAT(nfd = fcntl(fd.get(), F_DUPFD_CLOEXEC, 0), SyscallSucceeds());
+ FileDescriptor dup_fd(nfd);
+
+ // Check for the FD_CLOEXEC flag.
+ ASSERT_THAT(fcntl(dup_fd.get(), F_GETFD),
+ SyscallSucceedsWithValue(FD_CLOEXEC));
+}
+
TEST(FcntlTest, ClearCloExec) {
// Open an eventfd file descriptor with FD_CLOEXEC descriptor flag set.
FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(NewEventFD(0, EFD_CLOEXEC));
@@ -267,6 +328,22 @@ TEST(FcntlTest, GetAllFlags) {
EXPECT_EQ(rflags, expected);
}
+// When O_PATH is specified in flags, flag bits other than O_CLOEXEC,
+// O_DIRECTORY, and O_NOFOLLOW are ignored.
+TEST(FcntlTest, GetOpathFlag) {
+ SKIP_IF(IsRunningWithVFS1());
+ TempPath path = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ int flags = O_RDWR | O_DIRECT | O_SYNC | O_NONBLOCK | O_APPEND | O_PATH |
+ O_NOFOLLOW | O_DIRECTORY;
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(path.path(), flags));
+
+ int expected = O_PATH | O_NOFOLLOW | O_DIRECTORY;
+
+ int rflags;
+ EXPECT_THAT(rflags = fcntl(fd.get(), F_GETFL), SyscallSucceeds());
+ EXPECT_EQ(rflags, expected);
+}
+
TEST(FcntlTest, SetFlags) {
TempPath path = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(path.path(), 0));
@@ -395,6 +472,22 @@ TEST_F(FcntlLockTest, SetLockBadOpenFlagsRead) {
EXPECT_THAT(fcntl(fd.get(), F_SETLK, &fl1), SyscallFailsWithErrno(EBADF));
}
+TEST_F(FcntlLockTest, SetLockWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_PATH));
+
+ struct flock fl0;
+ fl0.l_type = F_WRLCK;
+ fl0.l_whence = SEEK_SET;
+ fl0.l_start = 0;
+ fl0.l_len = 0; // Lock all file
+
+ // Expect that setting a write lock using a Opath file descriptor
+ // won't work.
+ EXPECT_THAT(fcntl(fd.get(), F_SETLK, &fl0), SyscallFailsWithErrno(EBADF));
+}
+
TEST_F(FcntlLockTest, SetLockUnlockOnNothing) {
auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
FileDescriptor fd =
diff --git a/test/syscalls/linux/getdents.cc b/test/syscalls/linux/getdents.cc
index 93c692dd6..2f2b14037 100644
--- a/test/syscalls/linux/getdents.cc
+++ b/test/syscalls/linux/getdents.cc
@@ -429,6 +429,32 @@ TYPED_TEST(GetdentsTest, NotDir) {
SyscallFailsWithErrno(ENOTDIR));
}
+// Test that getdents returns EBADF when called on an opath file.
+TYPED_TEST(GetdentsTest, OpathFile) {
+ SKIP_IF(IsRunningWithVFS1());
+
+ auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_PATH));
+
+ typename TestFixture::DirentBufferType dirents(256);
+ EXPECT_THAT(RetryEINTR(syscall)(this->SyscallNum(), fd.get(), dirents.Data(),
+ dirents.Size()),
+ SyscallFailsWithErrno(EBADF));
+}
+
+// Test that getdents returns EBADF when called on an opath directory.
+TYPED_TEST(GetdentsTest, OpathDirectory) {
+ SKIP_IF(IsRunningWithVFS1());
+
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(Open(dir.path(), O_PATH | O_DIRECTORY));
+
+ typename TestFixture::DirentBufferType dirents(256);
+ ASSERT_THAT(RetryEINTR(syscall)(this->SyscallNum(), fd.get(), dirents.Data(),
+ dirents.Size()),
+ SyscallFailsWithErrno(EBADF));
+}
+
// Test that SEEK_SET to 0 causes getdents to re-read the entries.
TYPED_TEST(GetdentsTest, SeekResetsCursor) {
// . and .. should be in an otherwise empty directory.
diff --git a/test/syscalls/linux/getrusage.cc b/test/syscalls/linux/getrusage.cc
index 0e51d42a8..e84cbfdc3 100644
--- a/test/syscalls/linux/getrusage.cc
+++ b/test/syscalls/linux/getrusage.cc
@@ -23,6 +23,7 @@
#include "absl/time/time.h"
#include "test/util/logging.h"
#include "test/util/memory_util.h"
+#include "test/util/multiprocess_util.h"
#include "test/util/signal_util.h"
#include "test/util/test_util.h"
@@ -93,59 +94,66 @@ TEST(GetrusageTest, Grandchild) {
// Verifies that processes ignoring SIGCHLD do not have updated child maxrss
// updated.
TEST(GetrusageTest, IgnoreSIGCHLD) {
- struct sigaction sa;
- sa.sa_handler = SIG_IGN;
- sa.sa_flags = 0;
- auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(ScopedSigaction(SIGCHLD, sa));
- pid_t pid = fork();
- if (pid == 0) {
+ const auto rest = [] {
+ struct sigaction sa;
+ sa.sa_handler = SIG_IGN;
+ sa.sa_flags = 0;
+ auto cleanup = TEST_CHECK_NO_ERRNO_AND_VALUE(ScopedSigaction(SIGCHLD, sa));
+ pid_t pid = fork();
+ if (pid == 0) {
+ struct rusage rusage_self;
+ TEST_PCHECK(getrusage(RUSAGE_SELF, &rusage_self) == 0);
+ // The child has consumed some memory.
+ TEST_CHECK(rusage_self.ru_maxrss != 0);
+ _exit(0);
+ }
+ TEST_CHECK_SUCCESS(pid);
+ int status;
+ TEST_CHECK_ERRNO(RetryEINTR(waitpid)(pid, &status, 0), ECHILD);
struct rusage rusage_self;
- TEST_PCHECK(getrusage(RUSAGE_SELF, &rusage_self) == 0);
- // The child has consumed some memory.
- TEST_CHECK(rusage_self.ru_maxrss != 0);
- _exit(0);
- }
- ASSERT_THAT(pid, SyscallSucceeds());
- int status;
- ASSERT_THAT(RetryEINTR(waitpid)(pid, &status, 0),
- SyscallFailsWithErrno(ECHILD));
- struct rusage rusage_self;
- ASSERT_THAT(getrusage(RUSAGE_SELF, &rusage_self), SyscallSucceeds());
- struct rusage rusage_children;
- ASSERT_THAT(getrusage(RUSAGE_CHILDREN, &rusage_children), SyscallSucceeds());
- // The parent has consumed some memory.
- EXPECT_GT(rusage_self.ru_maxrss, 0);
- // The child's maxrss should not have propagated up.
- EXPECT_EQ(rusage_children.ru_maxrss, 0);
+ TEST_CHECK_SUCCESS(getrusage(RUSAGE_SELF, &rusage_self));
+ struct rusage rusage_children;
+ TEST_CHECK_SUCCESS(getrusage(RUSAGE_CHILDREN, &rusage_children));
+ // The parent has consumed some memory.
+ TEST_CHECK(rusage_self.ru_maxrss > 0);
+ // The child's maxrss should not have propagated up.
+ TEST_CHECK(rusage_children.ru_maxrss == 0);
+ };
+ // Execute inside a forked process so that rusage_children is clean.
+ EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}
// Verifies that zombie processes do not update their parent's maxrss. Only
// reaped processes should do this.
TEST(GetrusageTest, IgnoreZombie) {
- pid_t pid = fork();
- if (pid == 0) {
+ const auto rest = [] {
+ pid_t pid = fork();
+ if (pid == 0) {
+ struct rusage rusage_self;
+ TEST_PCHECK(getrusage(RUSAGE_SELF, &rusage_self) == 0);
+ struct rusage rusage_children;
+ TEST_PCHECK(getrusage(RUSAGE_CHILDREN, &rusage_children) == 0);
+ // The child has consumed some memory.
+ TEST_CHECK(rusage_self.ru_maxrss != 0);
+ // The child has no children of its own.
+ TEST_CHECK(rusage_children.ru_maxrss == 0);
+ _exit(0);
+ }
+ TEST_CHECK_SUCCESS(pid);
+ // Give the child time to exit. Because we don't call wait, the child should
+ // remain a zombie.
+ absl::SleepFor(absl::Seconds(5));
struct rusage rusage_self;
- TEST_PCHECK(getrusage(RUSAGE_SELF, &rusage_self) == 0);
+ TEST_CHECK_SUCCESS(getrusage(RUSAGE_SELF, &rusage_self));
struct rusage rusage_children;
- TEST_PCHECK(getrusage(RUSAGE_CHILDREN, &rusage_children) == 0);
- // The child has consumed some memory.
- TEST_CHECK(rusage_self.ru_maxrss != 0);
- // The child has no children of its own.
+ TEST_CHECK_SUCCESS(getrusage(RUSAGE_CHILDREN, &rusage_children));
+ // The parent has consumed some memory.
+ TEST_CHECK(rusage_self.ru_maxrss > 0);
+ // The child has consumed some memory, but hasn't been reaped.
TEST_CHECK(rusage_children.ru_maxrss == 0);
- _exit(0);
- }
- ASSERT_THAT(pid, SyscallSucceeds());
- // Give the child time to exit. Because we don't call wait, the child should
- // remain a zombie.
- absl::SleepFor(absl::Seconds(5));
- struct rusage rusage_self;
- ASSERT_THAT(getrusage(RUSAGE_SELF, &rusage_self), SyscallSucceeds());
- struct rusage rusage_children;
- ASSERT_THAT(getrusage(RUSAGE_CHILDREN, &rusage_children), SyscallSucceeds());
- // The parent has consumed some memory.
- EXPECT_GT(rusage_self.ru_maxrss, 0);
- // The child has consumed some memory, but hasn't been reaped.
- EXPECT_EQ(rusage_children.ru_maxrss, 0);
+ };
+ // Execute inside a forked process so that rusage_children is clean.
+ EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}
TEST(GetrusageTest, Wait4) {
diff --git a/test/syscalls/linux/ioctl.cc b/test/syscalls/linux/ioctl.cc
index b0a07a064..9b16d1558 100644
--- a/test/syscalls/linux/ioctl.cc
+++ b/test/syscalls/linux/ioctl.cc
@@ -76,6 +76,19 @@ TEST_F(IoctlTest, InvalidControlNumber) {
EXPECT_THAT(ioctl(STDOUT_FILENO, 0), SyscallFailsWithErrno(ENOTTY));
}
+TEST_F(IoctlTest, IoctlWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ const FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open("/dev/null", O_PATH));
+
+ int set = 1;
+ EXPECT_THAT(ioctl(fd.get(), FIONBIO, &set), SyscallFailsWithErrno(EBADF));
+
+ EXPECT_THAT(ioctl(fd.get(), FIONCLEX), SyscallFailsWithErrno(EBADF));
+
+ EXPECT_THAT(ioctl(fd.get(), FIOCLEX), SyscallFailsWithErrno(EBADF));
+}
+
TEST_F(IoctlTest, FIONBIOSucceeds) {
EXPECT_FALSE(CheckNonBlocking(fd()));
int set = 1;
diff --git a/test/syscalls/linux/link.cc b/test/syscalls/linux/link.cc
index 544681168..4f9ca1a65 100644
--- a/test/syscalls/linux/link.cc
+++ b/test/syscalls/linux/link.cc
@@ -50,6 +50,8 @@ bool IsSameFile(const std::string& f1, const std::string& f2) {
return stat1.st_dev == stat2.st_dev && stat1.st_ino == stat2.st_ino;
}
+// TODO(b/178640646): Add test for linkat with AT_EMPTY_PATH
+
TEST(LinkTest, CanCreateLinkFile) {
auto oldfile = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
const std::string newname = NewTempAbsPath();
@@ -235,6 +237,59 @@ TEST(LinkTest, AbsPathsWithNonDirFDs) {
SyscallSucceeds());
}
+TEST(LinkTest, NewDirFDWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ auto oldfile = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ const std::string newname_parent = NewTempAbsPath();
+ const std::string newname_base = "child";
+ const std::string newname = JoinPath(newname_parent, newname_base);
+
+ // Create newname_parent directory, and get an FD.
+ EXPECT_THAT(mkdir(newname_parent.c_str(), 0777), SyscallSucceeds());
+ const FileDescriptor newname_parent_fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(newname_parent, O_DIRECTORY | O_PATH));
+
+ // Link newname to oldfile, using newname_parent_fd.
+ EXPECT_THAT(linkat(AT_FDCWD, oldfile.path().c_str(), newname_parent_fd.get(),
+ newname.c_str(), 0),
+ SyscallSucceeds());
+
+ EXPECT_TRUE(IsSameFile(oldfile.path(), newname));
+}
+
+TEST(LinkTest, RelPathsNonDirFDsWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ auto oldfile = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+
+ // Create a file that will be passed as the directory fd for old/new names.
+ TempPath path = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor file_fd = ASSERT_NO_ERRNO_AND_VALUE(Open(path.path(), O_PATH));
+
+ // Using file_fd as olddirfd will fail.
+ EXPECT_THAT(linkat(file_fd.get(), "foo", AT_FDCWD, "bar", 0),
+ SyscallFailsWithErrno(ENOTDIR));
+
+ // Using file_fd as newdirfd will fail.
+ EXPECT_THAT(linkat(AT_FDCWD, oldfile.path().c_str(), file_fd.get(), "bar", 0),
+ SyscallFailsWithErrno(ENOTDIR));
+}
+
+TEST(LinkTest, AbsPathsNonDirFDsWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+
+ auto oldfile = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ const std::string newname = NewTempAbsPath();
+
+ // Create a file that will be passed as the directory fd for old/new names.
+ TempPath path = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor file_fd = ASSERT_NO_ERRNO_AND_VALUE(Open(path.path(), O_PATH));
+
+ // Using file_fd as the dirfds is OK as long as paths are absolute.
+ EXPECT_THAT(linkat(file_fd.get(), oldfile.path().c_str(), file_fd.get(),
+ newname.c_str(), 0),
+ SyscallSucceeds());
+}
+
TEST(LinkTest, LinkDoesNotFollowSymlinks) {
// Create oldfile, and oldsymlink which points to it.
auto oldfile = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
diff --git a/test/syscalls/linux/madvise.cc b/test/syscalls/linux/madvise.cc
index 5a1973f60..6e714b12c 100644
--- a/test/syscalls/linux/madvise.cc
+++ b/test/syscalls/linux/madvise.cc
@@ -179,9 +179,9 @@ TEST(MadviseDontforkTest, DontforkShared) {
// First page is mapped in child and modifications are visible to parent
// via the shared mapping.
TEST_CHECK(IsMapped(ms1.addr()));
- ExpectAllMappingBytes(ms1, 2);
+ CheckAllMappingBytes(ms1, 2);
memset(ms1.ptr(), 1, kPageSize);
- ExpectAllMappingBytes(ms1, 1);
+ CheckAllMappingBytes(ms1, 1);
// Second page must not be mapped in child.
TEST_CHECK(!IsMapped(ms2.addr()));
@@ -222,9 +222,9 @@ TEST(MadviseDontforkTest, DontforkAnonPrivate) {
// page. The mapping is private so the modifications are not visible to
// the parent.
TEST_CHECK(IsMapped(mp1.addr()));
- ExpectAllMappingBytes(mp1, 1);
+ CheckAllMappingBytes(mp1, 1);
memset(mp1.ptr(), 11, kPageSize);
- ExpectAllMappingBytes(mp1, 11);
+ CheckAllMappingBytes(mp1, 11);
// Verify second page is not mapped.
TEST_CHECK(!IsMapped(mp2.addr()));
@@ -233,9 +233,9 @@ TEST(MadviseDontforkTest, DontforkAnonPrivate) {
// page. The mapping is private so the modifications are not visible to
// the parent.
TEST_CHECK(IsMapped(mp3.addr()));
- ExpectAllMappingBytes(mp3, 3);
+ CheckAllMappingBytes(mp3, 3);
memset(mp3.ptr(), 13, kPageSize);
- ExpectAllMappingBytes(mp3, 13);
+ CheckAllMappingBytes(mp3, 13);
};
EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
diff --git a/test/syscalls/linux/mmap.cc b/test/syscalls/linux/mmap.cc
index 83546830d..93a6d9cde 100644
--- a/test/syscalls/linux/mmap.cc
+++ b/test/syscalls/linux/mmap.cc
@@ -930,6 +930,18 @@ TEST_F(MMapFileTest, WriteSharedOnReadOnlyFd) {
SyscallFailsWithErrno(EACCES));
}
+// Mmap not allowed on O_PATH FDs.
+TEST_F(MMapFileTest, MmapFileWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ const FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_PATH));
+
+ uintptr_t addr;
+ EXPECT_THAT(addr = Map(0, kPageSize, PROT_READ, MAP_PRIVATE, fd.get(), 0),
+ SyscallFailsWithErrno(EBADF));
+}
+
// The FD must be readable.
TEST_P(MMapFileParamTest, WriteOnlyFd) {
const FileDescriptor fd =
diff --git a/test/syscalls/linux/open.cc b/test/syscalls/linux/open.cc
index fcd162ca2..e65ffee8f 100644
--- a/test/syscalls/linux/open.cc
+++ b/test/syscalls/linux/open.cc
@@ -45,7 +45,7 @@ namespace {
// * O_CREAT
// * O_DIRECTORY
// * O_NOFOLLOW
-// * O_PATH <- Will we ever support this?
+// * O_PATH
//
// Special operations on open:
// * O_EXCL
@@ -75,55 +75,52 @@ class OpenTest : public FileTest {
};
TEST_F(OpenTest, OTrunc) {
- auto dirpath = JoinPath(GetAbsoluteTestTmpdir(), "truncd");
- ASSERT_THAT(mkdir(dirpath.c_str(), 0777), SyscallSucceeds());
- ASSERT_THAT(open(dirpath.c_str(), O_TRUNC, 0666),
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ ASSERT_THAT(open(dir.path().c_str(), O_TRUNC, 0666),
SyscallFailsWithErrno(EISDIR));
}
TEST_F(OpenTest, OTruncAndReadOnlyDir) {
- auto dirpath = JoinPath(GetAbsoluteTestTmpdir(), "truncd");
- ASSERT_THAT(mkdir(dirpath.c_str(), 0777), SyscallSucceeds());
- ASSERT_THAT(open(dirpath.c_str(), O_TRUNC | O_RDONLY, 0666),
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ ASSERT_THAT(open(dir.path().c_str(), O_TRUNC | O_RDONLY, 0666),
SyscallFailsWithErrno(EISDIR));
}
TEST_F(OpenTest, OTruncAndReadOnlyFile) {
- auto dirpath = JoinPath(GetAbsoluteTestTmpdir(), "truncfile");
- const FileDescriptor existing =
- ASSERT_NO_ERRNO_AND_VALUE(Open(dirpath.c_str(), O_RDWR | O_CREAT, 0666));
- const FileDescriptor otrunc = ASSERT_NO_ERRNO_AND_VALUE(
- Open(dirpath.c_str(), O_TRUNC | O_RDONLY, 0666));
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ auto path = JoinPath(dir.path(), "foo");
+ EXPECT_NO_ERRNO(Open(path, O_RDWR | O_CREAT, 0666));
+ EXPECT_NO_ERRNO(Open(path, O_TRUNC | O_RDONLY, 0666));
}
TEST_F(OpenTest, OCreateDirectory) {
SKIP_IF(IsRunningWithVFS1());
- auto dirpath = GetAbsoluteTestTmpdir();
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
// Normal case: existing directory.
- ASSERT_THAT(open(dirpath.c_str(), O_RDWR | O_CREAT, 0666),
+ ASSERT_THAT(open(dir.path().c_str(), O_RDWR | O_CREAT, 0666),
SyscallFailsWithErrno(EISDIR));
// Trailing separator on existing directory.
- ASSERT_THAT(open(dirpath.append("/").c_str(), O_RDWR | O_CREAT, 0666),
+ ASSERT_THAT(open(dir.path().append("/").c_str(), O_RDWR | O_CREAT, 0666),
SyscallFailsWithErrno(EISDIR));
// Trailing separator on non-existing directory.
- ASSERT_THAT(open(JoinPath(dirpath, "non-existent").append("/").c_str(),
+ ASSERT_THAT(open(JoinPath(dir.path(), "non-existent").append("/").c_str(),
O_RDWR | O_CREAT, 0666),
SyscallFailsWithErrno(EISDIR));
// "." special case.
- ASSERT_THAT(open(JoinPath(dirpath, ".").c_str(), O_RDWR | O_CREAT, 0666),
+ ASSERT_THAT(open(JoinPath(dir.path(), ".").c_str(), O_RDWR | O_CREAT, 0666),
SyscallFailsWithErrno(EISDIR));
}
TEST_F(OpenTest, MustCreateExisting) {
- auto dirPath = GetAbsoluteTestTmpdir();
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
// Existing directory.
- ASSERT_THAT(open(dirPath.c_str(), O_RDWR | O_CREAT | O_EXCL, 0666),
+ ASSERT_THAT(open(dir.path().c_str(), O_RDWR | O_CREAT | O_EXCL, 0666),
SyscallFailsWithErrno(EEXIST));
// Existing file.
- auto newFile = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileIn(dirPath));
+ auto newFile = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileIn(dir.path()));
ASSERT_THAT(open(newFile.path().c_str(), O_RDWR | O_CREAT | O_EXCL, 0666),
SyscallFailsWithErrno(EEXIST));
}
@@ -206,7 +203,8 @@ TEST_F(OpenTest, AtAbsPath) {
}
TEST_F(OpenTest, OpenNoFollowSymlink) {
- const std::string link_path = JoinPath(GetAbsoluteTestTmpdir(), "link");
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ const std::string link_path = JoinPath(dir.path().c_str(), "link");
ASSERT_THAT(symlink(test_file_name_.c_str(), link_path.c_str()),
SyscallSucceeds());
auto cleanup = Cleanup([link_path]() {
@@ -227,8 +225,7 @@ TEST_F(OpenTest, OpenNoFollowStillFollowsLinksInPath) {
//
// We will then open tmp_folder/sym_folder/file with O_NOFOLLOW and it
// should succeed as O_NOFOLLOW only applies to the final path component.
- auto tmp_path =
- ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(GetAbsoluteTestTmpdir()));
+ auto tmp_path = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
auto sym_path = ASSERT_NO_ERRNO_AND_VALUE(
TempPath::CreateSymlinkTo(GetAbsoluteTestTmpdir(), tmp_path.path()));
auto file_path =
@@ -246,8 +243,7 @@ TEST_F(OpenTest, OpenNoFollowStillFollowsLinksInPath) {
//
// open("root/child/symlink/root/child/file")
TEST_F(OpenTest, SymlinkRecurse) {
- auto root =
- ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(GetAbsoluteTestTmpdir()));
+ auto root = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
auto child = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDirIn(root.path()));
auto symlink = ASSERT_NO_ERRNO_AND_VALUE(
TempPath::CreateSymlinkTo(child.path(), "../.."));
@@ -481,12 +477,8 @@ TEST_F(OpenTest, CanTruncateWithStrangePermissions) {
ASSERT_NO_ERRNO(SetCapability(CAP_DAC_READ_SEARCH, false));
const DisableSave ds; // Permissions are dropped.
std::string path = NewTempAbsPath();
- int fd;
// Create a file without user permissions.
- EXPECT_THAT( // SAVE_BELOW
- fd = open(path.c_str(), O_CREAT | O_TRUNC | O_WRONLY, 055),
- SyscallSucceeds());
- EXPECT_THAT(close(fd), SyscallSucceeds());
+ EXPECT_NO_ERRNO(Open(path, O_CREAT | O_TRUNC | O_WRONLY, 055));
// Cannot open file because we are owner and have no permissions set.
EXPECT_THAT(open(path.c_str(), O_RDONLY), SyscallFailsWithErrno(EACCES));
@@ -495,8 +487,7 @@ TEST_F(OpenTest, CanTruncateWithStrangePermissions) {
EXPECT_THAT(chmod(path.c_str(), 0755), SyscallSucceeds());
// Now we can open the file again.
- EXPECT_THAT(fd = open(path.c_str(), O_RDWR), SyscallSucceeds());
- EXPECT_THAT(close(fd), SyscallSucceeds());
+ EXPECT_NO_ERRNO(Open(path, O_RDWR));
}
TEST_F(OpenTest, OpenNonDirectoryWithTrailingSlash) {
@@ -517,6 +508,26 @@ TEST_F(OpenTest, OpenWithStrangeFlags) {
EXPECT_THAT(read(fd.get(), &c, 1), SyscallFailsWithErrno(EBADF));
}
+TEST_F(OpenTest, OpenWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ ASSERT_NO_ERRNO(SetCapability(CAP_DAC_OVERRIDE, false));
+ ASSERT_NO_ERRNO(SetCapability(CAP_DAC_READ_SEARCH, false));
+ const DisableSave ds; // Permissions are dropped.
+ std::string path = NewTempAbsPath();
+
+ // Create a file without user permissions.
+ const FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(path.c_str(), O_CREAT | O_TRUNC | O_WRONLY, 055));
+
+ // Cannot open file as read only because we are owner and have no permissions
+ // set.
+ EXPECT_THAT(open(path.c_str(), O_RDONLY), SyscallFailsWithErrno(EACCES));
+
+ // Can open file with O_PATH because don't need permissions on the object when
+ // opening with O_PATH.
+ ASSERT_NO_ERRNO(Open(path, O_PATH));
+}
+
} // namespace
} // namespace testing
diff --git a/test/syscalls/linux/open_create.cc b/test/syscalls/linux/open_create.cc
index 9d63782fb..f8fbea79e 100644
--- a/test/syscalls/linux/open_create.cc
+++ b/test/syscalls/linux/open_create.cc
@@ -22,6 +22,7 @@
#include "test/util/capability_util.h"
#include "test/util/file_descriptor.h"
#include "test/util/fs_util.h"
+#include "test/util/posix_error.h"
#include "test/util/temp_path.h"
#include "test/util/temp_umask.h"
#include "test/util/test_util.h"
@@ -31,85 +32,60 @@ namespace testing {
namespace {
TEST(CreateTest, TmpFile) {
- int fd;
- EXPECT_THAT(fd = open(JoinPath(GetAbsoluteTestTmpdir(), "a").c_str(),
- O_RDWR | O_CREAT, 0666),
- SyscallSucceeds());
- EXPECT_THAT(close(fd), SyscallSucceeds());
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ EXPECT_NO_ERRNO(Open(JoinPath(dir.path(), "a"), O_RDWR | O_CREAT, 0666));
}
TEST(CreateTest, ExistingFile) {
- int fd;
- EXPECT_THAT(
- fd = open(JoinPath(GetAbsoluteTestTmpdir(), "ExistingFile").c_str(),
- O_RDWR | O_CREAT, 0666),
- SyscallSucceeds());
- EXPECT_THAT(close(fd), SyscallSucceeds());
-
- EXPECT_THAT(
- fd = open(JoinPath(GetAbsoluteTestTmpdir(), "ExistingFile").c_str(),
- O_RDWR | O_CREAT, 0666),
- SyscallSucceeds());
- EXPECT_THAT(close(fd), SyscallSucceeds());
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ auto path = JoinPath(dir.path(), "ExistingFile");
+ EXPECT_NO_ERRNO(Open(path, O_RDWR | O_CREAT, 0666));
+ EXPECT_NO_ERRNO(Open(path, O_RDWR | O_CREAT, 0666));
}
TEST(CreateTest, CreateAtFile) {
- int dirfd;
- EXPECT_THAT(dirfd = open(GetAbsoluteTestTmpdir().c_str(), O_DIRECTORY, 0666),
- SyscallSucceeds());
- EXPECT_THAT(openat(dirfd, "CreateAtFile", O_RDWR | O_CREAT, 0666),
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ auto dirfd = ASSERT_NO_ERRNO_AND_VALUE(Open(dir.path(), O_DIRECTORY, 0666));
+ EXPECT_THAT(openat(dirfd.get(), "CreateAtFile", O_RDWR | O_CREAT, 0666),
SyscallSucceeds());
- EXPECT_THAT(close(dirfd), SyscallSucceeds());
}
TEST(CreateTest, HonorsUmask_NoRandomSave) {
const DisableSave ds; // file cannot be re-opened as writable.
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
TempUmask mask(0222);
- int fd;
- ASSERT_THAT(
- fd = open(JoinPath(GetAbsoluteTestTmpdir(), "UmaskedFile").c_str(),
- O_RDWR | O_CREAT, 0666),
- SyscallSucceeds());
+ auto fd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(JoinPath(dir.path(), "UmaskedFile"), O_RDWR | O_CREAT, 0666));
struct stat statbuf;
- ASSERT_THAT(fstat(fd, &statbuf), SyscallSucceeds());
+ ASSERT_THAT(fstat(fd.get(), &statbuf), SyscallSucceeds());
EXPECT_EQ(0444, statbuf.st_mode & 0777);
- EXPECT_THAT(close(fd), SyscallSucceeds());
}
TEST(CreateTest, CreateExclusively) {
- std::string filename = NewTempAbsPath();
-
- int fd;
- ASSERT_THAT(fd = open(filename.c_str(), O_CREAT | O_RDWR, 0644),
- SyscallSucceeds());
- EXPECT_THAT(close(fd), SyscallSucceeds());
-
- EXPECT_THAT(open(filename.c_str(), O_CREAT | O_EXCL | O_RDWR, 0644),
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ auto path = JoinPath(dir.path(), "foo");
+ EXPECT_NO_ERRNO(Open(path, O_CREAT | O_RDWR, 0644));
+ EXPECT_THAT(open(path.c_str(), O_CREAT | O_EXCL | O_RDWR, 0644),
SyscallFailsWithErrno(EEXIST));
}
TEST(CreateTest, CreatWithOTrunc) {
- std::string dirpath = JoinPath(GetAbsoluteTestTmpdir(), "truncd");
- ASSERT_THAT(mkdir(dirpath.c_str(), 0777), SyscallSucceeds());
- ASSERT_THAT(open(dirpath.c_str(), O_CREAT | O_TRUNC, 0666),
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ ASSERT_THAT(open(dir.path().c_str(), O_CREAT | O_TRUNC, 0666),
SyscallFailsWithErrno(EISDIR));
}
TEST(CreateTest, CreatDirWithOTruncAndReadOnly) {
- std::string dirpath = JoinPath(GetAbsoluteTestTmpdir(), "truncd");
- ASSERT_THAT(mkdir(dirpath.c_str(), 0777), SyscallSucceeds());
- ASSERT_THAT(open(dirpath.c_str(), O_CREAT | O_TRUNC | O_RDONLY, 0666),
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ ASSERT_THAT(open(dir.path().c_str(), O_CREAT | O_TRUNC | O_RDONLY, 0666),
SyscallFailsWithErrno(EISDIR));
}
TEST(CreateTest, CreatFileWithOTruncAndReadOnly) {
- std::string dirpath = JoinPath(GetAbsoluteTestTmpdir(), "truncfile");
- int dirfd;
- ASSERT_THAT(dirfd = open(dirpath.c_str(), O_RDWR | O_CREAT, 0666),
- SyscallSucceeds());
- ASSERT_THAT(open(dirpath.c_str(), O_CREAT | O_TRUNC | O_RDONLY, 0666),
- SyscallSucceeds());
- ASSERT_THAT(close(dirfd), SyscallSucceeds());
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ auto path = JoinPath(dir.path(), "foo");
+ ASSERT_NO_ERRNO(Open(path, O_RDWR | O_CREAT, 0666));
+ ASSERT_NO_ERRNO(Open(path, O_CREAT | O_TRUNC | O_RDONLY, 0666));
}
TEST(CreateTest, CreateFailsOnDirWithoutWritePerms) {
diff --git a/test/syscalls/linux/ping_socket.cc b/test/syscalls/linux/ping_socket.cc
index a9bfdb37b..999c8ab6b 100644
--- a/test/syscalls/linux/ping_socket.cc
+++ b/test/syscalls/linux/ping_socket.cc
@@ -31,51 +31,36 @@ namespace gvisor {
namespace testing {
namespace {
-class PingSocket : public ::testing::Test {
- protected:
- // Creates a socket to be used in tests.
- void SetUp() override;
-
- // Closes the socket created by SetUp().
- void TearDown() override;
-
- // The loopback address.
- struct sockaddr_in addr_;
-};
-
-void PingSocket::SetUp() {
- // On some hosts ping sockets are restricted to specific groups using the
- // sysctl "ping_group_range".
- int s = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
- if (s < 0 && errno == EPERM) {
- GTEST_SKIP();
- }
- close(s);
-
- addr_ = {};
- // Just a random port as the destination port number is irrelevant for ping
- // sockets.
- addr_.sin_port = 12345;
- addr_.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
- addr_.sin_family = AF_INET;
-}
-
-void PingSocket::TearDown() {}
-
// Test ICMP port exhaustion returns EAGAIN.
//
// We disable both random/cooperative S/R for this test as it makes way too many
// syscalls.
-TEST_F(PingSocket, ICMPPortExhaustion_NoRandomSave) {
+TEST(PingSocket, ICMPPortExhaustion_NoRandomSave) {
DisableSave ds;
+
+ {
+ auto s = Socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
+ if (!s.ok()) {
+ ASSERT_EQ(s.error().errno_value(), EACCES);
+ GTEST_SKIP();
+ }
+ }
+
+ const struct sockaddr_in addr = {
+ .sin_family = AF_INET,
+ .sin_addr =
+ {
+ .s_addr = htonl(INADDR_LOOPBACK),
+ },
+ };
+
std::vector<FileDescriptor> sockets;
constexpr int kSockets = 65536;
- addr_.sin_port = 0;
for (int i = 0; i < kSockets; i++) {
auto s =
ASSERT_NO_ERRNO_AND_VALUE(Socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP));
- int ret = connect(s.get(), reinterpret_cast<struct sockaddr*>(&addr_),
- sizeof(addr_));
+ int ret = connect(s.get(), reinterpret_cast<const struct sockaddr*>(&addr),
+ sizeof(addr));
if (ret == 0) {
sockets.push_back(std::move(s));
continue;
diff --git a/test/syscalls/linux/pread64.cc b/test/syscalls/linux/pread64.cc
index bcdbbb044..c74990ba1 100644
--- a/test/syscalls/linux/pread64.cc
+++ b/test/syscalls/linux/pread64.cc
@@ -77,6 +77,16 @@ TEST_F(Pread64Test, WriteOnlyNotReadable) {
EXPECT_THAT(pread64(fd.get(), buf, 1024, 0), SyscallFailsWithErrno(EBADF));
}
+TEST_F(Pread64Test, Pread64WithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ const FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_PATH));
+
+ char buf[1024];
+ EXPECT_THAT(pread64(fd.get(), buf, 1024, 0), SyscallFailsWithErrno(EBADF));
+}
+
TEST_F(Pread64Test, DirNotReadable) {
const FileDescriptor fd =
ASSERT_NO_ERRNO_AND_VALUE(Open(GetAbsoluteTestTmpdir(), O_RDONLY));
diff --git a/test/syscalls/linux/preadv.cc b/test/syscalls/linux/preadv.cc
index 5b0743fe9..1c40f0915 100644
--- a/test/syscalls/linux/preadv.cc
+++ b/test/syscalls/linux/preadv.cc
@@ -89,6 +89,20 @@ TEST(PreadvTest, MMConcurrencyStress) {
// The test passes if it neither deadlocks nor crashes the OS.
}
+// This test calls preadv with an O_PATH fd.
+TEST(PreadvTest, PreadvWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ const FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_PATH));
+
+ struct iovec iov;
+ iov.iov_base = nullptr;
+ iov.iov_len = 0;
+
+ EXPECT_THAT(preadv(fd.get(), &iov, 1, 0), SyscallFailsWithErrno(EBADF));
+}
+
} // namespace
} // namespace testing
diff --git a/test/syscalls/linux/preadv2.cc b/test/syscalls/linux/preadv2.cc
index 4a9acd7ae..cb58719c4 100644
--- a/test/syscalls/linux/preadv2.cc
+++ b/test/syscalls/linux/preadv2.cc
@@ -226,6 +226,24 @@ TEST(Preadv2Test, TestUnreadableFile) {
SyscallFailsWithErrno(EBADF));
}
+// This test calls preadv2 with a file opened with O_PATH.
+TEST(Preadv2Test, Preadv2WithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ SKIP_IF(preadv2(-1, nullptr, 0, 0, 0) < 0 && errno == ENOSYS);
+
+ const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ const FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_PATH));
+
+ auto iov = absl::make_unique<struct iovec[]>(1);
+ iov[0].iov_base = nullptr;
+ iov[0].iov_len = 0;
+
+ EXPECT_THAT(preadv2(fd.get(), iov.get(), /*iovcnt=*/1, /*offset=*/0,
+ /*flags=*/0),
+ SyscallFailsWithErrno(EBADF));
+}
+
// Calling preadv2 with a non-negative offset calls preadv. Calling preadv with
// an unseekable file is not allowed. A pipe is used for an unseekable file.
TEST(Preadv2Test, TestUnseekableFileInvalid) {
diff --git a/test/syscalls/linux/proc_net_unix.cc b/test/syscalls/linux/proc_net_unix.cc
index 662c6feb2..d61d94309 100644
--- a/test/syscalls/linux/proc_net_unix.cc
+++ b/test/syscalls/linux/proc_net_unix.cc
@@ -18,6 +18,7 @@
#include "absl/strings/str_join.h"
#include "absl/strings/str_split.h"
#include "test/syscalls/linux/unix_domain_socket_test_util.h"
+#include "test/util/cleanup.h"
#include "test/util/file_descriptor.h"
#include "test/util/fs_util.h"
#include "test/util/test_util.h"
@@ -341,6 +342,8 @@ TEST(ProcNetUnix, StreamSocketStateStateConnectedOnAccept) {
int clientfd;
ASSERT_THAT(clientfd = accept(sockets->first_fd(), nullptr, nullptr),
SyscallSucceeds());
+ auto cleanup = Cleanup(
+ [clientfd]() { ASSERT_THAT(close(clientfd), SyscallSucceeds()); });
// Find the entry for the accepted socket. UDS proc entries don't have a
// remote address, so we distinguish the accepted socket from the listen
diff --git a/test/syscalls/linux/pty.cc b/test/syscalls/linux/pty.cc
index 0b174e2be..85ff258df 100644
--- a/test/syscalls/linux/pty.cc
+++ b/test/syscalls/linux/pty.cc
@@ -1338,6 +1338,7 @@ TEST_F(JobControlTest, SetTTYDifferentSession) {
TEST_PCHECK(waitpid(grandchild, &gcwstatus, 0) == grandchild);
TEST_PCHECK(gcwstatus == 0);
});
+ ASSERT_NO_ERRNO(res);
}
TEST_F(JobControlTest, ReleaseTTY) {
@@ -1515,7 +1516,8 @@ TEST_F(JobControlTest, GetForegroundProcessGroupNonControlling) {
// - creates a child process in a new process group
// - sets that child as the foreground process group
// - kills its child and sets itself as the foreground process group.
-TEST_F(JobControlTest, SetForegroundProcessGroup) {
+// TODO(gvisor.dev/issue/5357): Fix and enable.
+TEST_F(JobControlTest, DISABLED_SetForegroundProcessGroup) {
auto res = RunInChild([=]() {
TEST_PCHECK(!ioctl(replica_.get(), TIOCSCTTY, 0));
@@ -1557,6 +1559,7 @@ TEST_F(JobControlTest, SetForegroundProcessGroup) {
TEST_PCHECK(pgid = getpgid(0) == 0);
TEST_PCHECK(!tcsetpgrp(replica_.get(), pgid));
});
+ ASSERT_NO_ERRNO(res);
}
TEST_F(JobControlTest, SetForegroundProcessGroupWrongTTY) {
@@ -1576,8 +1579,9 @@ TEST_F(JobControlTest, SetForegroundProcessGroupNegPgid) {
ASSERT_NO_ERRNO(ret);
}
-TEST_F(JobControlTest, SetForegroundProcessGroupEmptyProcessGroup) {
- auto ret = RunInChild([=]() {
+// TODO(gvisor.dev/issue/5357): Fix and enable.
+TEST_F(JobControlTest, DISABLED_SetForegroundProcessGroupEmptyProcessGroup) {
+ auto res = RunInChild([=]() {
TEST_PCHECK(!ioctl(replica_.get(), TIOCSCTTY, 0));
// Create a new process, put it in a new process group, make that group the
@@ -1595,6 +1599,7 @@ TEST_F(JobControlTest, SetForegroundProcessGroupEmptyProcessGroup) {
TEST_PCHECK(ioctl(replica_.get(), TIOCSPGRP, &grandchild) != 0 &&
errno == ESRCH);
});
+ ASSERT_NO_ERRNO(res);
}
TEST_F(JobControlTest, SetForegroundProcessGroupDifferentSession) {
diff --git a/test/syscalls/linux/pwrite64.cc b/test/syscalls/linux/pwrite64.cc
index e69794910..1b2f25363 100644
--- a/test/syscalls/linux/pwrite64.cc
+++ b/test/syscalls/linux/pwrite64.cc
@@ -77,6 +77,17 @@ TEST_F(Pwrite64, Overflow) {
EXPECT_THAT(close(fd), SyscallSucceeds());
}
+TEST_F(Pwrite64, Pwrite64WithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ const FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_PATH));
+
+ std::vector<char> buf(1);
+ EXPECT_THAT(PwriteFd(fd.get(), buf.data(), 1, 0),
+ SyscallFailsWithErrno(EBADF));
+}
+
} // namespace
} // namespace testing
diff --git a/test/syscalls/linux/pwritev2.cc b/test/syscalls/linux/pwritev2.cc
index 63b686c62..00aed61b4 100644
--- a/test/syscalls/linux/pwritev2.cc
+++ b/test/syscalls/linux/pwritev2.cc
@@ -283,6 +283,23 @@ TEST(Pwritev2Test, ReadOnlyFile) {
SyscallFailsWithErrno(EBADF));
}
+TEST(Pwritev2Test, Pwritev2WithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ SKIP_IF(pwritev2(-1, nullptr, 0, 0, 0) < 0 && errno == ENOSYS);
+
+ const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ const FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_PATH));
+
+ char buf[16];
+ struct iovec iov;
+ iov.iov_base = buf;
+ iov.iov_len = sizeof(buf);
+
+ EXPECT_THAT(pwritev2(fd.get(), &iov, /*iovcnt=*/1, /*offset=*/0, /*flags=*/0),
+ SyscallFailsWithErrno(EBADF));
+}
+
// This test calls pwritev2 with an invalid flag.
TEST(Pwritev2Test, InvalidFlag) {
SKIP_IF(pwritev2(-1, nullptr, 0, 0, 0) < 0 && errno == ENOSYS);
diff --git a/test/syscalls/linux/read.cc b/test/syscalls/linux/read.cc
index 2633ba31b..98d5e432d 100644
--- a/test/syscalls/linux/read.cc
+++ b/test/syscalls/linux/read.cc
@@ -112,6 +112,15 @@ TEST_F(ReadTest, ReadDirectoryFails) {
EXPECT_THAT(ReadFd(file.get(), buf.data(), 1), SyscallFailsWithErrno(EISDIR));
}
+TEST_F(ReadTest, ReadWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ const FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_PATH));
+ std::vector<char> buf(1);
+ EXPECT_THAT(ReadFd(fd.get(), buf.data(), 1), SyscallFailsWithErrno(EBADF));
+}
+
} // namespace
} // namespace testing
diff --git a/test/syscalls/linux/readv.cc b/test/syscalls/linux/readv.cc
index baaf9f757..86808d255 100644
--- a/test/syscalls/linux/readv.cc
+++ b/test/syscalls/linux/readv.cc
@@ -251,6 +251,20 @@ TEST_F(ReadvTest, IovecOutsideTaskAddressRangeInNonemptyArray) {
SyscallFailsWithErrno(EFAULT));
}
+TEST_F(ReadvTest, ReadvWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ char buffer[1024];
+ struct iovec iov[1];
+ iov[0].iov_base = buffer;
+ iov[0].iov_len = 1024;
+
+ TempPath tmpfile = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(tmpfile.path().c_str(), O_PATH));
+
+ ASSERT_THAT(readv(fd.get(), iov, 1), SyscallFailsWithErrno(EBADF));
+}
+
// This test depends on the maximum extent of a single readv() syscall, so
// we can't tolerate interruption from saving.
TEST(ReadvTestNoFixture, TruncatedAtMax_NoRandomSave) {
diff --git a/test/syscalls/linux/setgid.cc b/test/syscalls/linux/setgid.cc
new file mode 100644
index 000000000..bfd91ba4f
--- /dev/null
+++ b/test/syscalls/linux/setgid.cc
@@ -0,0 +1,370 @@
+// 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 <limits.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "gtest/gtest.h"
+#include "test/util/capability_util.h"
+#include "test/util/cleanup.h"
+#include "test/util/fs_util.h"
+#include "test/util/posix_error.h"
+#include "test/util/temp_path.h"
+#include "test/util/test_util.h"
+
+namespace gvisor {
+namespace testing {
+
+namespace {
+
+constexpr int kDirmodeMask = 07777;
+constexpr int kDirmodeSgid = S_ISGID | 0777;
+constexpr int kDirmodeNoExec = S_ISGID | 0767;
+constexpr int kDirmodeNoSgid = 0777;
+
+// Sets effective GID and returns a Cleanup that restores the original.
+PosixErrorOr<Cleanup> Setegid(gid_t egid) {
+ gid_t old_gid = getegid();
+ if (setegid(egid) < 0) {
+ return PosixError(errno, absl::StrFormat("setegid(%d)", egid));
+ }
+ return Cleanup(
+ [old_gid]() { EXPECT_THAT(setegid(old_gid), SyscallSucceeds()); });
+}
+
+// Returns a pair of groups that the user is a member of.
+PosixErrorOr<std::pair<gid_t, gid_t>> Groups() {
+ // See whether the user is a member of at least 2 groups.
+ std::vector<gid_t> groups(64);
+ for (; groups.size() <= NGROUPS_MAX; groups.resize(groups.size() * 2)) {
+ int ngroups = getgroups(groups.size(), groups.data());
+ if (ngroups < 0 && errno == EINVAL) {
+ // Need a larger list.
+ continue;
+ }
+ if (ngroups < 0) {
+ return PosixError(errno, absl::StrFormat("getgroups(%d, %p)",
+ groups.size(), groups.data()));
+ }
+ if (ngroups >= 2) {
+ return std::pair<gid_t, gid_t>(groups[0], groups[1]);
+ }
+ // There aren't enough groups.
+ break;
+ }
+
+ // If we're root in the root user namespace, we can set our GID to whatever we
+ // want. Try that before giving up.
+ constexpr gid_t kGID1 = 1111;
+ constexpr gid_t kGID2 = 2222;
+ auto cleanup1 = Setegid(kGID1);
+ if (!cleanup1.ok()) {
+ return cleanup1.error();
+ }
+ auto cleanup2 = Setegid(kGID2);
+ if (!cleanup2.ok()) {
+ return cleanup2.error();
+ }
+ return std::pair<gid_t, gid_t>(kGID1, kGID2);
+}
+
+class SetgidDirTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ original_gid_ = getegid();
+
+ // TODO(b/175325250): Enable when setgid directories are supported.
+ SKIP_IF(IsRunningOnGvisor());
+ SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETGID)));
+
+ temp_dir_ = ASSERT_NO_ERRNO_AND_VALUE(
+ TempPath::CreateDirWith(GetAbsoluteTestTmpdir(), 0777 /* mode */));
+ groups_ = ASSERT_NO_ERRNO_AND_VALUE(Groups());
+ }
+
+ void TearDown() override {
+ ASSERT_THAT(setegid(original_gid_), SyscallSucceeds());
+ }
+
+ void MkdirAsGid(gid_t gid, const std::string& path, mode_t mode) {
+ auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(Setegid(gid));
+ ASSERT_THAT(mkdir(path.c_str(), mode), SyscallSucceeds());
+ }
+
+ PosixErrorOr<struct stat> Stat(const std::string& path) {
+ struct stat stats;
+ if (stat(path.c_str(), &stats) < 0) {
+ return PosixError(errno, absl::StrFormat("stat(%s, _)", path));
+ }
+ return stats;
+ }
+
+ PosixErrorOr<struct stat> Stat(const FileDescriptor& fd) {
+ struct stat stats;
+ if (fstat(fd.get(), &stats) < 0) {
+ return PosixError(errno, "fstat(_, _)");
+ }
+ return stats;
+ }
+
+ TempPath temp_dir_;
+ std::pair<gid_t, gid_t> groups_;
+ gid_t original_gid_;
+};
+
+// The control test. Files created with a given GID are owned by that group.
+TEST_F(SetgidDirTest, Control) {
+ // Set group to G1 and create a directory.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, 0777));
+
+ // Set group to G2, create a file in g1owned, and confirm that G2 owns it.
+ ASSERT_THAT(setegid(groups_.second), SyscallSucceeds());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(JoinPath(g1owned, "g2owned").c_str(), O_CREAT | O_RDWR, 0777));
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
+ EXPECT_EQ(stats.st_gid, groups_.second);
+}
+
+// Setgid directories cause created files to inherit GID.
+TEST_F(SetgidDirTest, CreateFile) {
+ // Set group to G1, create a directory, and enable setgid.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeSgid));
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeSgid), SyscallSucceeds());
+
+ // Set group to G2, create a file, and confirm that G1 owns it.
+ ASSERT_THAT(setegid(groups_.second), SyscallSucceeds());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(JoinPath(g1owned, "g2created").c_str(), O_CREAT | O_RDWR, 0666));
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
+ EXPECT_EQ(stats.st_gid, groups_.first);
+}
+
+// Setgid directories cause created directories to inherit GID.
+TEST_F(SetgidDirTest, CreateDir) {
+ // Set group to G1, create a directory, and enable setgid.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeSgid));
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeSgid), SyscallSucceeds());
+
+ // Set group to G2, create a directory, confirm that G1 owns it, and that the
+ // setgid bit is enabled.
+ auto g2created = JoinPath(g1owned, "g2created");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.second, g2created, 0666));
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(g2created));
+ EXPECT_EQ(stats.st_gid, groups_.first);
+ EXPECT_EQ(stats.st_mode & S_ISGID, S_ISGID);
+}
+
+// Setgid directories with group execution disabled still cause GID inheritance.
+TEST_F(SetgidDirTest, NoGroupExec) {
+ // Set group to G1, create a directory, and enable setgid.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeNoExec));
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeNoExec), SyscallSucceeds());
+
+ // Set group to G2, create a directory, confirm that G2 owns it, and that the
+ // setgid bit is enabled.
+ auto g2created = JoinPath(g1owned, "g2created");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.second, g2created, 0666));
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(g2created));
+ EXPECT_EQ(stats.st_gid, groups_.first);
+ EXPECT_EQ(stats.st_mode & S_ISGID, S_ISGID);
+}
+
+// Setting the setgid bit on directories with an existing file does not change
+// the file's group.
+TEST_F(SetgidDirTest, OldFile) {
+ // Set group to G1 and create a directory.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeNoSgid));
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeNoSgid), SyscallSucceeds());
+
+ // Set group to G2, create a file, confirm that G2 owns it.
+ ASSERT_THAT(setegid(groups_.second), SyscallSucceeds());
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(JoinPath(g1owned, "g2created").c_str(), O_CREAT | O_RDWR, 0666));
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
+ EXPECT_EQ(stats.st_gid, groups_.second);
+
+ // Enable setgid.
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeSgid), SyscallSucceeds());
+
+ // Confirm that the file's group is still G2.
+ stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
+ EXPECT_EQ(stats.st_gid, groups_.second);
+}
+
+// Setting the setgid bit on directories with an existing subdirectory does not
+// change the subdirectory's group.
+TEST_F(SetgidDirTest, OldDir) {
+ // Set group to G1, create a directory, and enable setgid.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeNoSgid));
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeNoSgid), SyscallSucceeds());
+
+ // Set group to G2, create a directory, confirm that G2 owns it.
+ ASSERT_THAT(setegid(groups_.second), SyscallSucceeds());
+ auto g2created = JoinPath(g1owned, "g2created");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.second, g2created, 0666));
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(g2created));
+ EXPECT_EQ(stats.st_gid, groups_.second);
+
+ // Enable setgid.
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeSgid), SyscallSucceeds());
+
+ // Confirm that the file's group is still G2.
+ stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(g2created));
+ EXPECT_EQ(stats.st_gid, groups_.second);
+}
+
+// Chowning a file clears the setgid and setuid bits.
+TEST_F(SetgidDirTest, ChownFileClears) {
+ // Set group to G1, create a directory, and enable setgid.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeMask));
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeMask), SyscallSucceeds());
+
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(JoinPath(g1owned, "newfile").c_str(), O_CREAT | O_RDWR, 0666));
+ ASSERT_THAT(fchmod(fd.get(), 0777 | S_ISUID | S_ISGID), SyscallSucceeds());
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
+ EXPECT_EQ(stats.st_gid, groups_.first);
+ EXPECT_EQ(stats.st_mode & (S_ISUID | S_ISGID), S_ISUID | S_ISGID);
+
+ // Change the owning group.
+ ASSERT_THAT(fchown(fd.get(), -1, groups_.second), SyscallSucceeds());
+
+ // The setgid and setuid bits should be cleared.
+ stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
+ EXPECT_EQ(stats.st_gid, groups_.second);
+ EXPECT_EQ(stats.st_mode & (S_ISUID | S_ISGID), 0);
+}
+
+// Chowning a file with setgid enabled, but not the group exec bit, does not
+// clear the setgid bit. Such files are mandatory locked.
+TEST_F(SetgidDirTest, ChownNoExecFileDoesNotClear) {
+ // Set group to G1, create a directory, and enable setgid.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeNoExec));
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeNoExec), SyscallSucceeds());
+
+ FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(JoinPath(g1owned, "newdir").c_str(), O_CREAT | O_RDWR, 0666));
+ ASSERT_THAT(fchmod(fd.get(), 0766 | S_ISUID | S_ISGID), SyscallSucceeds());
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
+ EXPECT_EQ(stats.st_gid, groups_.first);
+ EXPECT_EQ(stats.st_mode & (S_ISUID | S_ISGID), S_ISUID | S_ISGID);
+
+ // Change the owning group.
+ ASSERT_THAT(fchown(fd.get(), -1, groups_.second), SyscallSucceeds());
+
+ // Only the setuid bit is cleared.
+ stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(fd));
+ EXPECT_EQ(stats.st_gid, groups_.second);
+ EXPECT_EQ(stats.st_mode & (S_ISUID | S_ISGID), S_ISGID);
+}
+
+// Chowning a directory with setgid enabled does not clear the bit.
+TEST_F(SetgidDirTest, ChownDirDoesNotClear) {
+ // Set group to G1, create a directory, and enable setgid.
+ auto g1owned = JoinPath(temp_dir_.path(), "g1owned/");
+ ASSERT_NO_FATAL_FAILURE(MkdirAsGid(groups_.first, g1owned, kDirmodeMask));
+ ASSERT_THAT(chmod(g1owned.c_str(), kDirmodeMask), SyscallSucceeds());
+
+ // Change the owning group.
+ ASSERT_THAT(chown(g1owned.c_str(), -1, groups_.second), SyscallSucceeds());
+
+ struct stat stats = ASSERT_NO_ERRNO_AND_VALUE(Stat(g1owned));
+ EXPECT_EQ(stats.st_gid, groups_.second);
+ EXPECT_EQ(stats.st_mode & kDirmodeMask, kDirmodeMask);
+}
+
+struct FileModeTestcase {
+ std::string name;
+ mode_t mode;
+ mode_t result_mode;
+
+ FileModeTestcase(const std::string& name, mode_t mode, mode_t result_mode)
+ : name(name), mode(mode), result_mode(result_mode) {}
+};
+
+class FileModeTest : public ::testing::TestWithParam<FileModeTestcase> {};
+
+TEST_P(FileModeTest, WriteToFile) {
+ // TODO(b/175325250): Enable when setgid directories are supported.
+ SKIP_IF(IsRunningOnGvisor());
+
+ auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(
+ TempPath::CreateDirWith(GetAbsoluteTestTmpdir(), 0777 /* mode */));
+ auto path = JoinPath(temp_dir.path(), GetParam().name);
+ FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(path.c_str(), O_CREAT | O_RDWR, 0666));
+ ASSERT_THAT(fchmod(fd.get(), GetParam().mode), SyscallSucceeds());
+ struct stat stats;
+ ASSERT_THAT(fstat(fd.get(), &stats), SyscallSucceeds());
+ EXPECT_EQ(stats.st_mode & kDirmodeMask, GetParam().mode);
+
+ // For security reasons, writing to the file clears the SUID bit, and clears
+ // the SGID bit when the group executable bit is unset (which is not a true
+ // SGID binary).
+ constexpr char kInput = 'M';
+ ASSERT_THAT(write(fd.get(), &kInput, sizeof(kInput)),
+ SyscallSucceedsWithValue(sizeof(kInput)));
+
+ ASSERT_THAT(fstat(fd.get(), &stats), SyscallSucceeds());
+ EXPECT_EQ(stats.st_mode & kDirmodeMask, GetParam().result_mode);
+}
+
+TEST_P(FileModeTest, TruncateFile) {
+ // TODO(b/175325250): Enable when setgid directories are supported.
+ SKIP_IF(IsRunningOnGvisor());
+
+ auto temp_dir = ASSERT_NO_ERRNO_AND_VALUE(
+ TempPath::CreateDirWith(GetAbsoluteTestTmpdir(), 0777 /* mode */));
+ auto path = JoinPath(temp_dir.path(), GetParam().name);
+ FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(path.c_str(), O_CREAT | O_RDWR, 0666));
+ ASSERT_THAT(fchmod(fd.get(), GetParam().mode), SyscallSucceeds());
+ struct stat stats;
+ ASSERT_THAT(fstat(fd.get(), &stats), SyscallSucceeds());
+ EXPECT_EQ(stats.st_mode & kDirmodeMask, GetParam().mode);
+
+ // For security reasons, truncating the file clears the SUID bit, and clears
+ // the SGID bit when the group executable bit is unset (which is not a true
+ // SGID binary).
+ ASSERT_THAT(ftruncate(fd.get(), 0), SyscallSucceeds());
+
+ ASSERT_THAT(fstat(fd.get(), &stats), SyscallSucceeds());
+ EXPECT_EQ(stats.st_mode & kDirmodeMask, GetParam().result_mode);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ FileModes, FileModeTest,
+ ::testing::ValuesIn<FileModeTestcase>(
+ {FileModeTestcase("normal file", 0777, 0777),
+ FileModeTestcase("setuid", S_ISUID | 0777, 00777),
+ FileModeTestcase("setgid", S_ISGID | 0777, 00777),
+ FileModeTestcase("setuid and setgid", S_ISUID | S_ISGID | 0777, 00777),
+ FileModeTestcase("setgid without exec", S_ISGID | 0767,
+ S_ISGID | 0767),
+ FileModeTestcase("setuid and setgid without exec",
+ S_ISGID | S_ISUID | 0767, S_ISGID | 0767)}));
+
+} // namespace
+
+} // namespace testing
+} // namespace gvisor
diff --git a/test/syscalls/linux/shm.cc b/test/syscalls/linux/shm.cc
index 6aabd79e7..baf794152 100644
--- a/test/syscalls/linux/shm.cc
+++ b/test/syscalls/linux/shm.cc
@@ -372,18 +372,18 @@ TEST(ShmDeathTest, SegmentNotAccessibleAfterDetach) {
SetupGvisorDeathTest();
const auto rest = [&] {
- ShmSegment shm = ASSERT_NO_ERRNO_AND_VALUE(
+ ShmSegment shm = TEST_CHECK_NO_ERRNO_AND_VALUE(
Shmget(IPC_PRIVATE, kAllocSize, IPC_CREAT | 0777));
- char* addr = ASSERT_NO_ERRNO_AND_VALUE(Shmat(shm.id(), nullptr, 0));
+ char* addr = TEST_CHECK_NO_ERRNO_AND_VALUE(Shmat(shm.id(), nullptr, 0));
// Mark the segment as destroyed so it's automatically cleaned up when we
// crash below. We can't rely on the standard cleanup since the destructor
// will not run after the SIGSEGV. Note that this doesn't destroy the
// segment immediately since we're still attached to it.
- ASSERT_NO_ERRNO(shm.Rmid());
+ TEST_CHECK_NO_ERRNO(shm.Rmid());
addr[0] = 'x';
- ASSERT_NO_ERRNO(Shmdt(addr));
+ TEST_CHECK_NO_ERRNO(Shmdt(addr));
// This access should cause a SIGSEGV.
addr[0] = 'x';
diff --git a/test/syscalls/linux/socket_ip_tcp_generic.cc b/test/syscalls/linux/socket_ip_tcp_generic.cc
index 831d96262..579e824cd 100644
--- a/test/syscalls/linux/socket_ip_tcp_generic.cc
+++ b/test/syscalls/linux/socket_ip_tcp_generic.cc
@@ -65,6 +65,37 @@ TEST_P(TCPSocketPairTest, ZeroTcpInfoSucceeds) {
SyscallSucceeds());
}
+// Copied from include/net/tcp.h.
+constexpr int TCP_CA_OPEN = 0;
+
+TEST_P(TCPSocketPairTest, CheckTcpInfoFields) {
+ auto sockets = ASSERT_NO_ERRNO_AND_VALUE(NewSocketPair());
+
+ char buf[10] = {};
+ ASSERT_THAT(RetryEINTR(send)(sockets->first_fd(), buf, sizeof(buf), 0),
+ SyscallSucceedsWithValue(sizeof(buf)));
+
+ // Wait until second_fd sees the data and then recv it.
+ struct pollfd poll_fd = {sockets->second_fd(), POLLIN, 0};
+ constexpr int kPollTimeoutMs = 2000; // Wait up to 2 seconds for the data.
+ ASSERT_THAT(RetryEINTR(poll)(&poll_fd, 1, kPollTimeoutMs),
+ SyscallSucceedsWithValue(1));
+
+ ASSERT_THAT(RetryEINTR(recv)(sockets->second_fd(), buf, sizeof(buf), 0),
+ SyscallSucceedsWithValue(sizeof(buf)));
+
+ struct tcp_info opt = {};
+ socklen_t optLen = sizeof(opt);
+ ASSERT_THAT(getsockopt(sockets->first_fd(), SOL_TCP, TCP_INFO, &opt, &optLen),
+ SyscallSucceeds());
+ ASSERT_EQ(optLen, sizeof(opt));
+
+ // Validates the received tcp_info fields.
+ EXPECT_EQ(opt.tcpi_ca_state, TCP_CA_OPEN);
+ EXPECT_GT(opt.tcpi_snd_cwnd, 0);
+ EXPECT_GT(opt.tcpi_rto, 0);
+}
+
// This test validates that an RST is sent instead of a FIN when data is
// unread on calls to close(2).
TEST_P(TCPSocketPairTest, RSTSentOnCloseWithUnreadData) {
diff --git a/test/syscalls/linux/stat.cc b/test/syscalls/linux/stat.cc
index 6e7142a42..72f888659 100644
--- a/test/syscalls/linux/stat.cc
+++ b/test/syscalls/linux/stat.cc
@@ -221,6 +221,43 @@ TEST_F(StatTest, TrailingSlashNotCleanedReturnsENOTDIR) {
EXPECT_THAT(lstat(bad_path.c_str(), &buf), SyscallFailsWithErrno(ENOTDIR));
}
+TEST_F(StatTest, FstatFileWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ struct stat st;
+ TempPath tmpfile = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(tmpfile.path().c_str(), O_PATH));
+
+ // Stat the directory.
+ ASSERT_THAT(fstat(fd.get(), &st), SyscallSucceeds());
+}
+
+TEST_F(StatTest, FstatDirWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ struct stat st;
+ TempPath tmpdir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ FileDescriptor dirfd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(tmpdir.path().c_str(), O_PATH | O_DIRECTORY));
+
+ // Stat the directory.
+ ASSERT_THAT(fstat(dirfd.get(), &st), SyscallSucceeds());
+}
+
+// fstatat with an O_PATH fd
+TEST_F(StatTest, FstatatDirWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ TempPath tmpdir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ FileDescriptor dirfd = ASSERT_NO_ERRNO_AND_VALUE(
+ Open(tmpdir.path().c_str(), O_PATH | O_DIRECTORY));
+ TempPath tmpfile = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+
+ struct stat st = {};
+ EXPECT_THAT(fstatat(dirfd.get(), tmpfile.path().c_str(), &st, 0),
+ SyscallSucceeds());
+ EXPECT_FALSE(S_ISDIR(st.st_mode));
+ EXPECT_TRUE(S_ISREG(st.st_mode));
+}
+
// Test fstatating a symlink directory.
TEST_F(StatTest, FstatatSymlinkDir) {
// Create a directory and symlink to it.
diff --git a/test/syscalls/linux/statfs.cc b/test/syscalls/linux/statfs.cc
index f0fb166bd..d4ea8e026 100644
--- a/test/syscalls/linux/statfs.cc
+++ b/test/syscalls/linux/statfs.cc
@@ -64,6 +64,16 @@ TEST(FstatfsTest, InternalTmpfs) {
EXPECT_THAT(fstatfs(fd.get(), &st), SyscallSucceeds());
}
+TEST(FstatfsTest, CanStatFileWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ auto temp_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ const FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(temp_file.path(), O_PATH));
+
+ struct statfs st;
+ EXPECT_THAT(fstatfs(fd.get(), &st), SyscallSucceeds());
+}
+
TEST(FstatfsTest, InternalDevShm) {
auto temp_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
const FileDescriptor fd =
diff --git a/test/syscalls/linux/symlink.cc b/test/syscalls/linux/symlink.cc
index 4d9eba7f0..ea219a091 100644
--- a/test/syscalls/linux/symlink.cc
+++ b/test/syscalls/linux/symlink.cc
@@ -269,6 +269,36 @@ TEST(SymlinkTest, SymlinkAtDegradedPermissions_NoRandomSave) {
EXPECT_THAT(close(dirfd), SyscallSucceeds());
}
+TEST(SymlinkTest, SymlinkAtDirWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ const std::string filepath = NewTempAbsPathInDir(dir.path());
+ const std::string base = std::string(Basename(filepath));
+ FileDescriptor dirfd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(dir.path().c_str(), O_DIRECTORY | O_PATH));
+
+ EXPECT_THAT(symlinkat("/dangling", dirfd.get(), base.c_str()),
+ SyscallSucceeds());
+}
+
+TEST(SymlinkTest, ReadlinkAtDirWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ auto dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir());
+ const std::string filepath = NewTempAbsPathInDir(dir.path());
+ const std::string base = std::string(Basename(filepath));
+ ASSERT_THAT(symlink("/dangling", filepath.c_str()), SyscallSucceeds());
+
+ FileDescriptor dirfd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(dir.path().c_str(), O_DIRECTORY | O_PATH));
+
+ std::vector<char> buf(1024);
+ int linksize;
+ EXPECT_THAT(
+ linksize = readlinkat(dirfd.get(), base.c_str(), buf.data(), 1024),
+ SyscallSucceeds());
+ EXPECT_EQ(0, strncmp("/dangling", buf.data(), linksize));
+}
+
TEST(SymlinkTest, ReadlinkAtDegradedPermissions_NoRandomSave) {
// Drop capabilities that allow us to override file and directory permissions.
ASSERT_NO_ERRNO(SetCapability(CAP_DAC_OVERRIDE, false));
diff --git a/test/syscalls/linux/sync.cc b/test/syscalls/linux/sync.cc
index 8aa2525a9..84a2c4ed7 100644
--- a/test/syscalls/linux/sync.cc
+++ b/test/syscalls/linux/sync.cc
@@ -49,10 +49,20 @@ TEST(SyncTest, SyncFromPipe) {
EXPECT_THAT(close(pipes[1]), SyscallSucceeds());
}
-TEST(SyncTest, CannotSyncFileSytemAtBadFd) {
+TEST(SyncTest, CannotSyncFileSystemAtBadFd) {
EXPECT_THAT(syncfs(-1), SyscallFailsWithErrno(EBADF));
}
+TEST(SyncTest, CannotSyncFileSystemAtOpathFD) {
+ SKIP_IF(IsRunningWithVFS1());
+
+ const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith(
+ GetAbsoluteTestTmpdir(), "", TempPath::kDefaultFileMode));
+ const FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_PATH));
+
+ EXPECT_THAT(syncfs(fd.get()), SyscallFailsWithErrno(EBADF));
+}
} // namespace
} // namespace testing
diff --git a/test/syscalls/linux/tcp_socket.cc b/test/syscalls/linux/tcp_socket.cc
index 9028ab024..f56c50e61 100644
--- a/test/syscalls/linux/tcp_socket.cc
+++ b/test/syscalls/linux/tcp_socket.cc
@@ -1168,6 +1168,42 @@ TEST_P(SimpleTcpSocketTest, SelfConnectSendRecv_NoRandomSave) {
EXPECT_EQ(read_bytes, kBufSz);
}
+TEST_P(SimpleTcpSocketTest, SelfConnectSend_NoRandomSave) {
+ // Initialize address to the loopback one.
+ sockaddr_storage addr =
+ ASSERT_NO_ERRNO_AND_VALUE(InetLoopbackAddr(GetParam()));
+ socklen_t addrlen = sizeof(addr);
+
+ const FileDescriptor s =
+ ASSERT_NO_ERRNO_AND_VALUE(Socket(GetParam(), SOCK_STREAM, IPPROTO_TCP));
+
+ constexpr int max_seg = 256;
+ ASSERT_THAT(
+ setsockopt(s.get(), SOL_TCP, TCP_MAXSEG, &max_seg, sizeof(max_seg)),
+ SyscallSucceeds());
+
+ ASSERT_THAT(bind(s.get(), reinterpret_cast<struct sockaddr*>(&addr), addrlen),
+ SyscallSucceeds());
+ // Get the bound port.
+ ASSERT_THAT(
+ getsockname(s.get(), reinterpret_cast<struct sockaddr*>(&addr), &addrlen),
+ SyscallSucceeds());
+ ASSERT_THAT(RetryEINTR(connect)(
+ s.get(), reinterpret_cast<struct sockaddr*>(&addr), addrlen),
+ SyscallSucceeds());
+
+ std::vector<char> writebuf(512 << 10); // 512 KiB.
+
+ // Try to send the whole thing.
+ int n;
+ ASSERT_THAT(n = SendFd(s.get(), writebuf.data(), writebuf.size(), 0),
+ SyscallSucceeds());
+
+ // We should have written the whole thing.
+ EXPECT_EQ(n, writebuf.size());
+ EXPECT_THAT(shutdown(s.get(), SHUT_WR), SyscallSucceedsWithValue(0));
+}
+
TEST_P(SimpleTcpSocketTest, NonBlockingConnect) {
const FileDescriptor listener =
ASSERT_NO_ERRNO_AND_VALUE(Socket(GetParam(), SOCK_STREAM, IPPROTO_TCP));
diff --git a/test/syscalls/linux/truncate.cc b/test/syscalls/linux/truncate.cc
index bfc95ed38..17832c47d 100644
--- a/test/syscalls/linux/truncate.cc
+++ b/test/syscalls/linux/truncate.cc
@@ -196,6 +196,16 @@ TEST(TruncateTest, FtruncateNonWriteable) {
EXPECT_THAT(ftruncate(fd.get(), 0), SyscallFailsWithErrno(EINVAL));
}
+TEST(TruncateTest, FtruncateWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ auto temp_file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith(
+ GetAbsoluteTestTmpdir(), absl::string_view(), 0555 /* mode */));
+ const FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(temp_file.path(), O_PATH));
+ EXPECT_THAT(ftruncate(fd.get(), 0), AnyOf(SyscallFailsWithErrno(EBADF),
+ 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) {
diff --git a/test/syscalls/linux/uidgid.cc b/test/syscalls/linux/uidgid.cc
index 64d6d0b8f..4139a18d8 100644
--- a/test/syscalls/linux/uidgid.cc
+++ b/test/syscalls/linux/uidgid.cc
@@ -23,6 +23,8 @@
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "test/util/capability_util.h"
+#include "test/util/cleanup.h"
+#include "test/util/multiprocess_util.h"
#include "test/util/posix_error.h"
#include "test/util/test_util.h"
#include "test/util/thread_util.h"
@@ -33,6 +35,16 @@ ABSL_FLAG(int32_t, scratch_uid2, 65533, "second scratch UID");
ABSL_FLAG(int32_t, scratch_gid1, 65534, "first scratch GID");
ABSL_FLAG(int32_t, scratch_gid2, 65533, "second scratch GID");
+// Force use of syscall instead of glibc set*id() wrappers because we want to
+// apply to the current task only. libc sets all threads in a process because
+// "POSIX requires that all threads in a process share the same credentials."
+#define setuid USE_SYSCALL_INSTEAD
+#define setgid USE_SYSCALL_INSTEAD
+#define setreuid USE_SYSCALL_INSTEAD
+#define setregid USE_SYSCALL_INSTEAD
+#define setresuid USE_SYSCALL_INSTEAD
+#define setresgid USE_SYSCALL_INSTEAD
+
using ::testing::UnorderedElementsAreArray;
namespace gvisor {
@@ -137,21 +149,31 @@ TEST(UidGidRootTest, Setuid) {
TEST(UidGidRootTest, Setgid) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(IsRoot()));
- EXPECT_THAT(setgid(-1), SyscallFailsWithErrno(EINVAL));
+ EXPECT_THAT(syscall(SYS_setgid, -1), SyscallFailsWithErrno(EINVAL));
- const gid_t gid = absl::GetFlag(FLAGS_scratch_gid1);
- ASSERT_THAT(setgid(gid), SyscallSucceeds());
- EXPECT_NO_ERRNO(CheckGIDs(gid, gid, gid));
+ ScopedThread([&] {
+ const gid_t gid = absl::GetFlag(FLAGS_scratch_gid1);
+ EXPECT_THAT(syscall(SYS_setgid, gid), SyscallSucceeds());
+ EXPECT_NO_ERRNO(CheckGIDs(gid, gid, gid));
+ });
}
TEST(UidGidRootTest, SetgidNotFromThreadGroupLeader) {
+#pragma push_macro("allow_setgid")
+#undef setgid
+
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(IsRoot()));
+ int old_gid = getgid();
+ auto clean = Cleanup([old_gid] { setgid(old_gid); });
+
const gid_t gid = absl::GetFlag(FLAGS_scratch_gid1);
// NOTE(b/64676707): Do setgid in a separate thread so that we can test if
// info.si_pid is set correctly.
ScopedThread([gid] { ASSERT_THAT(setgid(gid), SyscallSucceeds()); });
EXPECT_NO_ERRNO(CheckGIDs(gid, gid, gid));
+
+#pragma pop_macro("allow_setgid")
}
TEST(UidGidRootTest, Setreuid) {
@@ -159,27 +181,25 @@ TEST(UidGidRootTest, Setreuid) {
// "Supplying a value of -1 for either the real or effective user ID forces
// the system to leave that ID unchanged." - setreuid(2)
- EXPECT_THAT(setreuid(-1, -1), SyscallSucceeds());
+ EXPECT_THAT(syscall(SYS_setreuid, -1, -1), SyscallSucceeds());
+
EXPECT_NO_ERRNO(CheckUIDs(0, 0, 0));
// Do setuid in a separate thread so that after finishing this test, the
- // process can still open files the test harness created before starting this
- // test. Otherwise, the files are created by root (UID before the test), but
- // cannot be opened by the `uid` set below after the test. After calling
- // setuid(non-zero-UID), there is no way to get root privileges back.
+ // process can still open files the test harness created before starting
+ // this test. Otherwise, the files are created by root (UID before the
+ // test), but cannot be opened by the `uid` set below after the test. After
+ // calling setuid(non-zero-UID), there is no way to get root privileges
+ // back.
ScopedThread([&] {
const uid_t ruid = absl::GetFlag(FLAGS_scratch_uid1);
const uid_t euid = absl::GetFlag(FLAGS_scratch_uid2);
- // Use syscall instead of glibc setuid wrapper because we want this setuid
- // call to only apply to this task. posix threads, however, require that all
- // threads have the same UIDs, so using the setuid wrapper sets all threads'
- // real UID.
EXPECT_THAT(syscall(SYS_setreuid, ruid, euid), SyscallSucceeds());
// "If the real user ID is set or the effective user ID is set to a value
- // not equal to the previous real user ID, the saved set-user-ID will be set
- // to the new effective user ID." - setreuid(2)
+ // not equal to the previous real user ID, the saved set-user-ID will be
+ // set to the new effective user ID." - setreuid(2)
EXPECT_NO_ERRNO(CheckUIDs(ruid, euid, euid));
});
}
@@ -187,13 +207,15 @@ TEST(UidGidRootTest, Setreuid) {
TEST(UidGidRootTest, Setregid) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(IsRoot()));
- EXPECT_THAT(setregid(-1, -1), SyscallSucceeds());
+ EXPECT_THAT(syscall(SYS_setregid, -1, -1), SyscallSucceeds());
EXPECT_NO_ERRNO(CheckGIDs(0, 0, 0));
- const gid_t rgid = absl::GetFlag(FLAGS_scratch_gid1);
- const gid_t egid = absl::GetFlag(FLAGS_scratch_gid2);
- ASSERT_THAT(setregid(rgid, egid), SyscallSucceeds());
- EXPECT_NO_ERRNO(CheckGIDs(rgid, egid, egid));
+ ScopedThread([&] {
+ const gid_t rgid = absl::GetFlag(FLAGS_scratch_gid1);
+ const gid_t egid = absl::GetFlag(FLAGS_scratch_gid2);
+ ASSERT_THAT(syscall(SYS_setregid, rgid, egid), SyscallSucceeds());
+ EXPECT_NO_ERRNO(CheckGIDs(rgid, egid, egid));
+ });
}
TEST(UidGidRootTest, Setresuid) {
@@ -201,23 +223,24 @@ TEST(UidGidRootTest, Setresuid) {
// "If one of the arguments equals -1, the corresponding value is not
// changed." - setresuid(2)
- EXPECT_THAT(setresuid(-1, -1, -1), SyscallSucceeds());
+ EXPECT_THAT(syscall(SYS_setresuid, -1, -1, -1), SyscallSucceeds());
EXPECT_NO_ERRNO(CheckUIDs(0, 0, 0));
// Do setuid in a separate thread so that after finishing this test, the
- // process can still open files the test harness created before starting this
- // test. Otherwise, the files are created by root (UID before the test), but
- // cannot be opened by the `uid` set below after the test. After calling
- // setuid(non-zero-UID), there is no way to get root privileges back.
+ // process can still open files the test harness created before starting
+ // this test. Otherwise, the files are created by root (UID before the
+ // test), but cannot be opened by the `uid` set below after the test. After
+ // calling setuid(non-zero-UID), there is no way to get root privileges
+ // back.
ScopedThread([&] {
const uid_t ruid = 12345;
const uid_t euid = 23456;
const uid_t suid = 34567;
// Use syscall instead of glibc setuid wrapper because we want this setuid
- // call to only apply to this task. posix threads, however, require that all
- // threads have the same UIDs, so using the setuid wrapper sets all threads'
- // real UID.
+ // call to only apply to this task. posix threads, however, require that
+ // all threads have the same UIDs, so using the setuid wrapper sets all
+ // threads' real UID.
EXPECT_THAT(syscall(SYS_setresuid, ruid, euid, suid), SyscallSucceeds());
EXPECT_NO_ERRNO(CheckUIDs(ruid, euid, suid));
});
@@ -226,14 +249,16 @@ TEST(UidGidRootTest, Setresuid) {
TEST(UidGidRootTest, Setresgid) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(IsRoot()));
- EXPECT_THAT(setresgid(-1, -1, -1), SyscallSucceeds());
+ EXPECT_THAT(syscall(SYS_setresgid, -1, -1, -1), SyscallSucceeds());
EXPECT_NO_ERRNO(CheckGIDs(0, 0, 0));
- const gid_t rgid = 12345;
- const gid_t egid = 23456;
- const gid_t sgid = 34567;
- ASSERT_THAT(setresgid(rgid, egid, sgid), SyscallSucceeds());
- EXPECT_NO_ERRNO(CheckGIDs(rgid, egid, sgid));
+ ScopedThread([&] {
+ const gid_t rgid = 12345;
+ const gid_t egid = 23456;
+ const gid_t sgid = 34567;
+ ASSERT_THAT(syscall(SYS_setresgid, rgid, egid, sgid), SyscallSucceeds());
+ EXPECT_NO_ERRNO(CheckGIDs(rgid, egid, sgid));
+ });
}
TEST(UidGidRootTest, Setgroups) {
@@ -254,14 +279,14 @@ TEST(UidGidRootTest, Setuid_prlimit) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(IsRoot()));
// Do seteuid in a separate thread so that after finishing this test, the
- // process can still open files the test harness created before starting this
- // test. Otherwise, the files are created by root (UID before the test), but
- // cannot be opened by the `uid` set below after the test.
+ // process can still open files the test harness created before starting
+ // this test. Otherwise, the files are created by root (UID before the
+ // test), but cannot be opened by the `uid` set below after the test.
ScopedThread([&] {
- // Use syscall instead of glibc setuid wrapper because we want this seteuid
- // call to only apply to this task. POSIX threads, however, require that all
- // threads have the same UIDs, so using the seteuid wrapper sets all
- // threads' UID.
+ // Use syscall instead of glibc setuid wrapper because we want this
+ // seteuid call to only apply to this task. POSIX threads, however,
+ // require that all threads have the same UIDs, so using the seteuid
+ // wrapper sets all threads' UID.
EXPECT_THAT(syscall(SYS_setreuid, -1, 65534), SyscallSucceeds());
// Despite the UID change, we should be able to get our own limits.
diff --git a/test/syscalls/linux/write.cc b/test/syscalls/linux/write.cc
index 77bcfbb8a..740992d0a 100644
--- a/test/syscalls/linux/write.cc
+++ b/test/syscalls/linux/write.cc
@@ -218,6 +218,44 @@ TEST_F(WriteTest, PwriteNoChangeOffset) {
EXPECT_THAT(lseek(fd, 0, SEEK_CUR), SyscallSucceedsWithValue(bytes_total));
}
+TEST_F(WriteTest, WriteWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ TempPath tmpfile = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor f =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(tmpfile.path().c_str(), O_PATH));
+ int fd = f.get();
+
+ EXPECT_THAT(WriteBytes(fd, 1024), SyscallFailsWithErrno(EBADF));
+}
+
+TEST_F(WriteTest, WritevWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ TempPath tmpfile = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor f =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(tmpfile.path().c_str(), O_PATH));
+ int fd = f.get();
+
+ char buf[16];
+ struct iovec iov;
+ iov.iov_base = buf;
+ iov.iov_len = sizeof(buf);
+
+ EXPECT_THAT(writev(fd, &iov, /*__count=*/1), SyscallFailsWithErrno(EBADF));
+}
+
+TEST_F(WriteTest, PwriteWithOpath) {
+ SKIP_IF(IsRunningWithVFS1());
+ TempPath tmpfile = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile());
+ FileDescriptor f =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(tmpfile.path().c_str(), O_PATH));
+ int fd = f.get();
+
+ const std::string data = "hello world\n";
+
+ EXPECT_THAT(pwrite(fd, data.data(), data.size(), 0),
+ SyscallFailsWithErrno(EBADF));
+}
+
} // namespace
} // namespace testing
diff --git a/test/syscalls/linux/xattr.cc b/test/syscalls/linux/xattr.cc
index bd3f829c4..a953a55fe 100644
--- a/test/syscalls/linux/xattr.cc
+++ b/test/syscalls/linux/xattr.cc
@@ -607,6 +607,27 @@ TEST_F(XattrTest, XattrWithFD) {
EXPECT_THAT(fremovexattr(fd.get(), name), SyscallSucceeds());
}
+TEST_F(XattrTest, XattrWithOPath) {
+ SKIP_IF(IsRunningWithVFS1());
+ const FileDescriptor fd =
+ ASSERT_NO_ERRNO_AND_VALUE(Open(test_file_name_.c_str(), O_PATH));
+ const char name[] = "user.test";
+ int val = 1234;
+ size_t size = sizeof(val);
+ EXPECT_THAT(fsetxattr(fd.get(), name, &val, size, /*flags=*/0),
+ SyscallFailsWithErrno(EBADF));
+
+ int buf;
+ EXPECT_THAT(fgetxattr(fd.get(), name, &buf, size),
+ SyscallFailsWithErrno(EBADF));
+
+ char list[sizeof(name)];
+ EXPECT_THAT(flistxattr(fd.get(), list, sizeof(list)),
+ SyscallFailsWithErrno(EBADF));
+
+ EXPECT_THAT(fremovexattr(fd.get(), name), SyscallFailsWithErrno(EBADF));
+}
+
TEST_F(XattrTest, TrustedNamespaceWithCapSysAdmin) {
// Trusted namespace not supported in VFS1.
SKIP_IF(IsRunningWithVFS1());
diff --git a/test/util/capability_util.h b/test/util/capability_util.h
index bb9ea1fe5..a03bc7e05 100644
--- a/test/util/capability_util.h
+++ b/test/util/capability_util.h
@@ -96,6 +96,19 @@ inline PosixError DropPermittedCapability(int cap) {
PosixErrorOr<bool> CanCreateUserNamespace();
+class AutoCapability {
+ public:
+ AutoCapability(int cap, bool set) : cap_(cap), set_(set) {
+ EXPECT_NO_ERRNO(SetCapability(cap_, set_));
+ }
+
+ ~AutoCapability() { EXPECT_NO_ERRNO(SetCapability(cap_, !set_)); }
+
+ private:
+ int cap_;
+ bool set_;
+};
+
} // namespace testing
} // namespace gvisor
#endif // GVISOR_TEST_UTIL_CAPABILITY_UTIL_H_
diff --git a/test/util/fs_util.cc b/test/util/fs_util.cc
index b16055dd8..5f1ce0d8a 100644
--- a/test/util/fs_util.cc
+++ b/test/util/fs_util.cc
@@ -663,5 +663,21 @@ PosixErrorOr<bool> IsOverlayfs(const std::string& path) {
return stat.f_type == OVERLAYFS_SUPER_MAGIC;
}
+PosixError CheckSameFile(const FileDescriptor& fd1, const FileDescriptor& fd2) {
+ struct stat stat_result1, stat_result2;
+ int res = fstat(fd1.get(), &stat_result1);
+ if (res < 0) {
+ return PosixError(errno, absl::StrCat("fstat ", fd1.get()));
+ }
+
+ res = fstat(fd2.get(), &stat_result2);
+ if (res < 0) {
+ return PosixError(errno, absl::StrCat("fstat ", fd2.get()));
+ }
+ EXPECT_EQ(stat_result1.st_dev, stat_result2.st_dev);
+ EXPECT_EQ(stat_result1.st_ino, stat_result2.st_ino);
+
+ return NoError();
+}
} // namespace testing
} // namespace gvisor
diff --git a/test/util/fs_util.h b/test/util/fs_util.h
index c99cf5eb7..2190c3bca 100644
--- a/test/util/fs_util.h
+++ b/test/util/fs_util.h
@@ -191,6 +191,8 @@ PosixErrorOr<bool> IsTmpfs(const std::string& path);
// IsOverlayfs returns true if the file at path is backed by overlayfs.
PosixErrorOr<bool> IsOverlayfs(const std::string& path);
+PosixError CheckSameFile(const FileDescriptor& fd1, const FileDescriptor& fd2);
+
namespace internal {
// Not part of the public API.
std::string JoinPathImpl(std::initializer_list<absl::string_view> paths);
diff --git a/test/util/logging.cc b/test/util/logging.cc
index 5d5e76c46..5fadb076b 100644
--- a/test/util/logging.cc
+++ b/test/util/logging.cc
@@ -69,9 +69,7 @@ int WriteNumber(int fd, uint32_t val) {
} // namespace
void CheckFailure(const char* cond, size_t cond_size, const char* msg,
- size_t msg_size, bool include_errno) {
- int saved_errno = errno;
-
+ size_t msg_size, int errno_value) {
constexpr char kCheckFailure[] = "Check failed: ";
Write(2, kCheckFailure, sizeof(kCheckFailure) - 1);
Write(2, cond, cond_size);
@@ -81,10 +79,10 @@ void CheckFailure(const char* cond, size_t cond_size, const char* msg,
Write(2, msg, msg_size);
}
- if (include_errno) {
+ if (errno_value != 0) {
constexpr char kErrnoMessage[] = " (errno ";
Write(2, kErrnoMessage, sizeof(kErrnoMessage) - 1);
- WriteNumber(2, saved_errno);
+ WriteNumber(2, errno_value);
Write(2, ")", 1);
}
diff --git a/test/util/logging.h b/test/util/logging.h
index 589166fab..5c17f1233 100644
--- a/test/util/logging.h
+++ b/test/util/logging.h
@@ -21,7 +21,7 @@ namespace gvisor {
namespace testing {
void CheckFailure(const char* cond, size_t cond_size, const char* msg,
- size_t msg_size, bool include_errno);
+ size_t msg_size, int errno_value);
// If cond is false, aborts the current process.
//
@@ -30,7 +30,7 @@ void CheckFailure(const char* cond, size_t cond_size, const char* msg,
do { \
if (!(cond)) { \
::gvisor::testing::CheckFailure(#cond, sizeof(#cond) - 1, nullptr, \
- 0, false); \
+ 0, 0); \
} \
} while (0)
@@ -41,7 +41,7 @@ void CheckFailure(const char* cond, size_t cond_size, const char* msg,
do { \
if (!(cond)) { \
::gvisor::testing::CheckFailure(#cond, sizeof(#cond) - 1, msg, \
- sizeof(msg) - 1, false); \
+ sizeof(msg) - 1, 0); \
} \
} while (0)
@@ -52,7 +52,7 @@ void CheckFailure(const char* cond, size_t cond_size, const char* msg,
do { \
if (!(cond)) { \
::gvisor::testing::CheckFailure(#cond, sizeof(#cond) - 1, nullptr, \
- 0, true); \
+ 0, errno); \
} \
} while (0)
@@ -63,10 +63,54 @@ void CheckFailure(const char* cond, size_t cond_size, const char* msg,
do { \
if (!(cond)) { \
::gvisor::testing::CheckFailure(#cond, sizeof(#cond) - 1, msg, \
- sizeof(msg) - 1, true); \
+ sizeof(msg) - 1, errno); \
} \
} while (0)
+// expr must return PosixErrorOr<T>. The current process is aborted if
+// !PosixError<T>.ok().
+//
+// This macro is async-signal-safe.
+#define TEST_CHECK_NO_ERRNO(expr) \
+ ({ \
+ auto _expr_result = (expr); \
+ if (!_expr_result.ok()) { \
+ ::gvisor::testing::CheckFailure( \
+ #expr, sizeof(#expr) - 1, nullptr, 0, \
+ _expr_result.error().errno_value()); \
+ } \
+ })
+
+// expr must return PosixErrorOr<T>. The current process is aborted if
+// !PosixError<T>.ok(). Otherwise, PosixErrorOr<T> value is returned.
+//
+// This macro is async-signal-safe.
+#define TEST_CHECK_NO_ERRNO_AND_VALUE(expr) \
+ ({ \
+ auto _expr_result = (expr); \
+ if (!_expr_result.ok()) { \
+ ::gvisor::testing::CheckFailure( \
+ #expr, sizeof(#expr) - 1, nullptr, 0, \
+ _expr_result.error().errno_value()); \
+ } \
+ std::move(_expr_result).ValueOrDie(); \
+ })
+
+// cond must be greater or equal than 0. Used to test result of syscalls.
+//
+// This macro is async-signal-safe.
+#define TEST_CHECK_SUCCESS(cond) TEST_PCHECK((cond) >= 0)
+
+// cond must be -1 and errno must match errno_value. Used to test errors from
+// syscalls.
+//
+// This macro is async-signal-safe.
+#define TEST_CHECK_ERRNO(cond, errno_value) \
+ do { \
+ TEST_PCHECK((cond) == -1); \
+ TEST_PCHECK_MSG(errno == (errno_value), #cond " expected " #errno_value); \
+ } while (0)
+
} // namespace testing
} // namespace gvisor
diff --git a/test/util/multiprocess_util.h b/test/util/multiprocess_util.h
index 2f3bf4a6f..840fde4ee 100644
--- a/test/util/multiprocess_util.h
+++ b/test/util/multiprocess_util.h
@@ -123,7 +123,8 @@ inline PosixErrorOr<Cleanup> ForkAndExecveat(int32_t dirfd,
// Calls fn in a forked subprocess and returns the exit status of the
// subprocess.
//
-// fn must be async-signal-safe.
+// fn must be async-signal-safe. Use of ASSERT/EXPECT functions is prohibited.
+// Use TEST_CHECK variants instead.
PosixErrorOr<int> InForkedProcess(const std::function<void()>& fn);
} // namespace testing
diff --git a/test/util/posix_error.cc b/test/util/posix_error.cc
index deed0c05b..8522e4c81 100644
--- a/test/util/posix_error.cc
+++ b/test/util/posix_error.cc
@@ -50,7 +50,7 @@ std::string PosixError::ToString() const {
ret = absl::StrCat("PosixError(errno=", errno_, " ", res, ")");
#endif
- if (!msg_.empty()) {
+ if (strnlen(msg_, sizeof(msg_)) > 0) {
ret.append(" ");
ret.append(msg_);
}
diff --git a/test/util/posix_error.h b/test/util/posix_error.h
index b634a7f78..27557ad44 100644
--- a/test/util/posix_error.h
+++ b/test/util/posix_error.h
@@ -26,12 +26,18 @@
namespace gvisor {
namespace testing {
+// PosixError must be async-signal-safe.
class ABSL_MUST_USE_RESULT PosixError {
public:
PosixError() {}
+
explicit PosixError(int errno_value) : errno_(errno_value) {}
- PosixError(int errno_value, std::string msg)
- : errno_(errno_value), msg_(std::move(msg)) {}
+
+ PosixError(int errno_value, std::string_view msg) : errno_(errno_value) {
+ // Check that `msg` will fit, leaving room for '\0' at the end.
+ TEST_CHECK(msg.size() < sizeof(msg_));
+ msg.copy(msg_, msg.size());
+ }
PosixError(PosixError&& other) = default;
PosixError& operator=(PosixError&& other) = default;
@@ -45,7 +51,7 @@ class ABSL_MUST_USE_RESULT PosixError {
const PosixError& error() const { return *this; }
int errno_value() const { return errno_; }
- std::string message() const { return msg_; }
+ const char* message() const { return msg_; }
// ToString produces a full string representation of this posix error
// including the printable representation of the errno and the error message.
@@ -58,7 +64,7 @@ class ABSL_MUST_USE_RESULT PosixError {
private:
int errno_ = 0;
- std::string msg_;
+ char msg_[1024] = {};
};
template <typename T>