diff options
Diffstat (limited to 'test')
71 files changed, 2565 insertions, 390 deletions
diff --git a/test/fsstress/fsstress_test.go b/test/fsstress/fsstress_test.go index 63f692ca9..300c21ceb 100644 --- a/test/fsstress/fsstress_test.go +++ b/test/fsstress/fsstress_test.go @@ -41,7 +41,7 @@ func fsstress(t *testing.T, dir string) { image = "basic/fsstress" ) seed := strconv.FormatUint(uint64(rand.Uint32()), 10) - args := []string{"-d", dir, "-n", operations, "-p", processes, "-seed", seed, "-X"} + args := []string{"-d", dir, "-n", operations, "-p", processes, "-s", seed, "-X"} t.Logf("Repro: docker run --rm --runtime=runsc %s %s", image, strings.Join(args, "")) out, err := d.Run(ctx, dockerutil.RunOpts{Image: image}, args...) if err != nil { diff --git a/test/iptables/BUILD b/test/iptables/BUILD index 66453772a..ae4bba847 100644 --- a/test/iptables/BUILD +++ b/test/iptables/BUILD @@ -15,7 +15,9 @@ go_library( ], visibility = ["//test/iptables:__subpackages__"], deps = [ + "//pkg/binary", "//pkg/test/testutil", + "//pkg/usermem", ], ) diff --git a/test/iptables/filter_input.go b/test/iptables/filter_input.go index 37a1a6694..c47660026 100644 --- a/test/iptables/filter_input.go +++ b/test/iptables/filter_input.go @@ -51,6 +51,12 @@ func init() { RegisterTestCase(FilterInputInvertDestination{}) RegisterTestCase(FilterInputSource{}) RegisterTestCase(FilterInputInvertSource{}) + RegisterTestCase(FilterInputInterfaceAccept{}) + RegisterTestCase(FilterInputInterfaceDrop{}) + RegisterTestCase(FilterInputInterface{}) + RegisterTestCase(FilterInputInterfaceBeginsWith{}) + RegisterTestCase(FilterInputInterfaceInvertDrop{}) + RegisterTestCase(FilterInputInterfaceInvertAccept{}) } // FilterInputDropUDP tests that we can drop UDP traffic. @@ -744,3 +750,195 @@ func (FilterInputInvertSource) ContainerAction(ctx context.Context, ip net.IP, i func (FilterInputInvertSource) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error { return sendUDPLoop(ctx, ip, acceptPort) } + +// FilterInputInterfaceAccept tests that packets are accepted from interface +// matching the iptables rule. +type FilterInputInterfaceAccept struct{ localCase } + +var _ TestCase = FilterInputInterfaceAccept{} + +// Name implements TestCase.Name. +func (FilterInputInterfaceAccept) Name() string { + return "FilterInputInterfaceAccept" +} + +// ContainerAction implements TestCase.ContainerAction. +func (FilterInputInterfaceAccept) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error { + ifname, ok := getInterfaceName() + if !ok { + return fmt.Errorf("no interface is present, except loopback") + } + if err := filterTable(ipv6, "-A", "INPUT", "-p", "udp", "-i", ifname, "-j", "ACCEPT"); err != nil { + return err + } + if err := listenUDP(ctx, acceptPort); err != nil { + return fmt.Errorf("packets on port %d should be allowed, but encountered an error: %w", acceptPort, err) + } + + return nil +} + +// LocalAction implements TestCase.LocalAction. +func (FilterInputInterfaceAccept) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error { + return sendUDPLoop(ctx, ip, acceptPort) +} + +// FilterInputInterfaceDrop tests that packets are dropped from interface +// matching the iptables rule. +type FilterInputInterfaceDrop struct{ localCase } + +var _ TestCase = FilterInputInterfaceDrop{} + +// Name implements TestCase.Name. +func (FilterInputInterfaceDrop) Name() string { + return "FilterInputInterfaceDrop" +} + +// ContainerAction implements TestCase.ContainerAction. +func (FilterInputInterfaceDrop) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error { + ifname, ok := getInterfaceName() + if !ok { + return fmt.Errorf("no interface is present, except loopback") + } + if err := filterTable(ipv6, "-A", "INPUT", "-p", "udp", "-i", ifname, "-j", "DROP"); err != nil { + return err + } + timedCtx, cancel := context.WithTimeout(ctx, NegativeTimeout) + defer cancel() + if err := listenUDP(timedCtx, acceptPort); err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return nil + } + return fmt.Errorf("error reading: %w", err) + } + return fmt.Errorf("packets should have been dropped, but got a packet") +} + +// LocalAction implements TestCase.LocalAction. +func (FilterInputInterfaceDrop) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error { + return sendUDPLoop(ctx, ip, acceptPort) +} + +// FilterInputInterface tests that packets are not dropped from interface which +// is not matching the interface name in the iptables rule. +type FilterInputInterface struct{ localCase } + +var _ TestCase = FilterInputInterface{} + +// Name implements TestCase.Name. +func (FilterInputInterface) Name() string { + return "FilterInputInterface" +} + +// ContainerAction implements TestCase.ContainerAction. +func (FilterInputInterface) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error { + if err := filterTable(ipv6, "-A", "INPUT", "-p", "udp", "-i", "lo", "-j", "DROP"); err != nil { + return err + } + if err := listenUDP(ctx, acceptPort); err != nil { + return fmt.Errorf("packets on port %d should be allowed, but encountered an error: %w", acceptPort, err) + } + return nil +} + +// LocalAction implements TestCase.LocalAction. +func (FilterInputInterface) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error { + return sendUDPLoop(ctx, ip, acceptPort) +} + +// FilterInputInterfaceBeginsWith tests that packets are dropped from an +// interface which begins with the given interface name. +type FilterInputInterfaceBeginsWith struct{ localCase } + +var _ TestCase = FilterInputInterfaceBeginsWith{} + +// Name implements TestCase.Name. +func (FilterInputInterfaceBeginsWith) Name() string { + return "FilterInputInterfaceBeginsWith" +} + +// ContainerAction implements TestCase.ContainerAction. +func (FilterInputInterfaceBeginsWith) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error { + if err := filterTable(ipv6, "-A", "INPUT", "-p", "udp", "-i", "e+", "-j", "DROP"); err != nil { + return err + } + timedCtx, cancel := context.WithTimeout(ctx, NegativeTimeout) + defer cancel() + if err := listenUDP(timedCtx, acceptPort); err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return nil + } + return fmt.Errorf("error reading: %w", err) + } + return fmt.Errorf("packets should have been dropped, but got a packet") +} + +// LocalAction implements TestCase.LocalAction. +func (FilterInputInterfaceBeginsWith) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error { + return sendUDPLoop(ctx, ip, acceptPort) +} + +// FilterInputInterfaceInvertDrop tests that we selectively drop packets from +// interface not matching the interface name. +type FilterInputInterfaceInvertDrop struct{ baseCase } + +var _ TestCase = FilterInputInterfaceInvertDrop{} + +// Name implements TestCase.Name. +func (FilterInputInterfaceInvertDrop) Name() string { + return "FilterInputInterfaceInvertDrop" +} + +// ContainerAction implements TestCase.ContainerAction. +func (FilterInputInterfaceInvertDrop) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error { + if err := filterTable(ipv6, "-A", "INPUT", "-p", "tcp", "!", "-i", "lo", "-j", "DROP"); err != nil { + return err + } + timedCtx, cancel := context.WithTimeout(ctx, NegativeTimeout) + defer cancel() + if err := listenTCP(timedCtx, acceptPort); err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return nil + } + return fmt.Errorf("error reading: %w", err) + } + return fmt.Errorf("connection on port %d should not be accepted, but was accepted", acceptPort) +} + +// LocalAction implements TestCase.LocalAction. +func (FilterInputInterfaceInvertDrop) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error { + timedCtx, cancel := context.WithTimeout(ctx, NegativeTimeout) + defer cancel() + if err := connectTCP(timedCtx, ip, acceptPort); err != nil { + var operr *net.OpError + if errors.As(err, &operr) && operr.Timeout() { + return nil + } + return fmt.Errorf("error connecting: %w", err) + } + return fmt.Errorf("connection destined to port %d should not be accepted, but was accepted", acceptPort) +} + +// FilterInputInterfaceInvertAccept tests that we can selectively accept packets +// not matching the specific incoming interface. +type FilterInputInterfaceInvertAccept struct{ baseCase } + +var _ TestCase = FilterInputInterfaceInvertAccept{} + +// Name implements TestCase.Name. +func (FilterInputInterfaceInvertAccept) Name() string { + return "FilterInputInterfaceInvertAccept" +} + +// ContainerAction implements TestCase.ContainerAction. +func (FilterInputInterfaceInvertAccept) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error { + if err := filterTable(ipv6, "-A", "INPUT", "-p", "tcp", "!", "-i", "lo", "-j", "ACCEPT"); err != nil { + return err + } + return listenTCP(ctx, acceptPort) +} + +// LocalAction implements TestCase.LocalAction. +func (FilterInputInterfaceInvertAccept) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error { + return connectTCP(ctx, ip, acceptPort) +} diff --git a/test/iptables/iptables_test.go b/test/iptables/iptables_test.go index 4733146c0..ef92e3fff 100644 --- a/test/iptables/iptables_test.go +++ b/test/iptables/iptables_test.go @@ -392,6 +392,30 @@ func TestInputInvertSource(t *testing.T) { singleTest(t, FilterInputInvertSource{}) } +func TestInputInterfaceAccept(t *testing.T) { + singleTest(t, FilterInputInterfaceAccept{}) +} + +func TestInputInterfaceDrop(t *testing.T) { + singleTest(t, FilterInputInterfaceDrop{}) +} + +func TestInputInterface(t *testing.T) { + singleTest(t, FilterInputInterface{}) +} + +func TestInputInterfaceBeginsWith(t *testing.T) { + singleTest(t, FilterInputInterfaceBeginsWith{}) +} + +func TestInputInterfaceInvertDrop(t *testing.T) { + singleTest(t, FilterInputInterfaceInvertDrop{}) +} + +func TestInputInterfaceInvertAccept(t *testing.T) { + singleTest(t, FilterInputInterfaceInvertAccept{}) +} + func TestFilterAddrs(t *testing.T) { tcs := []struct { ipv6 bool @@ -424,3 +448,11 @@ func TestNATPreOriginalDst(t *testing.T) { func TestNATOutOriginalDst(t *testing.T) { singleTest(t, NATOutOriginalDst{}) } + +func TestNATPreRECVORIGDSTADDR(t *testing.T) { + singleTest(t, NATPreRECVORIGDSTADDR{}) +} + +func TestNATOutRECVORIGDSTADDR(t *testing.T) { + singleTest(t, NATOutRECVORIGDSTADDR{}) +} diff --git a/test/iptables/iptables_util.go b/test/iptables/iptables_util.go index a6ec5cca3..4cd770a65 100644 --- a/test/iptables/iptables_util.go +++ b/test/iptables/iptables_util.go @@ -171,7 +171,7 @@ func connectTCP(ctx context.Context, ip net.IP, port int) error { return err } if err := testutil.PollContext(ctx, callback); err != nil { - return fmt.Errorf("timed out waiting to connect IP on port %v, most recent error: %v", port, err) + return fmt.Errorf("timed out waiting to connect IP on port %v, most recent error: %w", port, err) } return nil diff --git a/test/iptables/nat.go b/test/iptables/nat.go index 495241482..c3874240f 100644 --- a/test/iptables/nat.go +++ b/test/iptables/nat.go @@ -20,6 +20,9 @@ import ( "fmt" "net" "syscall" + + "gvisor.dev/gvisor/pkg/binary" + "gvisor.dev/gvisor/pkg/usermem" ) const redirectPort = 42 @@ -43,6 +46,8 @@ func init() { RegisterTestCase(NATLoopbackSkipsPrerouting{}) RegisterTestCase(NATPreOriginalDst{}) RegisterTestCase(NATOutOriginalDst{}) + RegisterTestCase(NATPreRECVORIGDSTADDR{}) + RegisterTestCase(NATOutRECVORIGDSTADDR{}) } // NATPreRedirectUDPPort tests that packets are redirected to different port. @@ -538,9 +543,9 @@ func (NATOutOriginalDst) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) } func listenForRedirectedConn(ctx context.Context, ipv6 bool, originalDsts []net.IP) error { - // The net package doesn't give guarantee access to the connection's + // The net package doesn't give guaranteed access to the connection's // underlying FD, and thus we cannot call getsockopt. We have to use - // traditional syscalls for SO_ORIGINAL_DST. + // traditional syscalls. // Create the listening socket, bind, listen, and accept. family := syscall.AF_INET @@ -609,36 +614,14 @@ func listenForRedirectedConn(ctx context.Context, ipv6 bool, originalDsts []net. if err != nil { return err } - // The original destination could be any of our IPs. - for _, dst := range originalDsts { - want := syscall.RawSockaddrInet6{ - Family: syscall.AF_INET6, - Port: htons(dropPort), - } - copy(want.Addr[:], dst.To16()) - if got == want { - return nil - } - } - return fmt.Errorf("SO_ORIGINAL_DST returned %+v, but wanted one of %+v (note: port numbers are in network byte order)", got, originalDsts) + return addrMatches6(got, originalDsts, dropPort) } got, err := originalDestination4(connFD) if err != nil { return err } - // The original destination could be any of our IPs. - for _, dst := range originalDsts { - want := syscall.RawSockaddrInet4{ - Family: syscall.AF_INET, - Port: htons(dropPort), - } - copy(want.Addr[:], dst.To4()) - if got == want { - return nil - } - } - return fmt.Errorf("SO_ORIGINAL_DST returned %+v, but wanted one of %+v (note: port numbers are in network byte order)", got, originalDsts) + return addrMatches4(got, originalDsts, dropPort) } // loopbackTests runs an iptables rule and ensures that packets sent to @@ -662,3 +645,233 @@ func loopbackTest(ctx context.Context, ipv6 bool, dest net.IP, args ...string) e return err } } + +// NATPreRECVORIGDSTADDR tests that IP{V6}_RECVORIGDSTADDR gets the post-NAT +// address on the PREROUTING chain. +type NATPreRECVORIGDSTADDR struct{ containerCase } + +// Name implements TestCase.Name. +func (NATPreRECVORIGDSTADDR) Name() string { + return "NATPreRECVORIGDSTADDR" +} + +// ContainerAction implements TestCase.ContainerAction. +func (NATPreRECVORIGDSTADDR) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error { + if err := natTable(ipv6, "-A", "PREROUTING", "-p", "udp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", redirectPort)); err != nil { + return err + } + + if err := recvWithRECVORIGDSTADDR(ctx, ipv6, nil, redirectPort); err != nil { + return err + } + + return nil +} + +// LocalAction implements TestCase.LocalAction. +func (NATPreRECVORIGDSTADDR) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error { + return sendUDPLoop(ctx, ip, acceptPort) +} + +// NATOutRECVORIGDSTADDR tests that IP{V6}_RECVORIGDSTADDR gets the post-NAT +// address on the OUTPUT chain. +type NATOutRECVORIGDSTADDR struct{ containerCase } + +// Name implements TestCase.Name. +func (NATOutRECVORIGDSTADDR) Name() string { + return "NATOutRECVORIGDSTADDR" +} + +// ContainerAction implements TestCase.ContainerAction. +func (NATOutRECVORIGDSTADDR) ContainerAction(ctx context.Context, ip net.IP, ipv6 bool) error { + if err := natTable(ipv6, "-A", "OUTPUT", "-p", "udp", "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", redirectPort)); err != nil { + return err + } + + sendCh := make(chan error) + go func() { + // Packets will be sent to a non-container IP and redirected + // back to the container. + sendCh <- sendUDPLoop(ctx, ip, acceptPort) + }() + + expectedIP := &net.IP{127, 0, 0, 1} + if ipv6 { + expectedIP = &net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + } + if err := recvWithRECVORIGDSTADDR(ctx, ipv6, expectedIP, redirectPort); err != nil { + return err + } + + select { + case err := <-sendCh: + return err + default: + return nil + } +} + +// LocalAction implements TestCase.LocalAction. +func (NATOutRECVORIGDSTADDR) LocalAction(ctx context.Context, ip net.IP, ipv6 bool) error { + // No-op. + return nil +} + +func recvWithRECVORIGDSTADDR(ctx context.Context, ipv6 bool, expectedDst *net.IP, port uint16) error { + // The net package doesn't give guaranteed access to a connection's + // underlying FD, and thus we cannot call getsockopt. We have to use + // traditional syscalls for IP_RECVORIGDSTADDR. + + // Create the listening socket. + var ( + family = syscall.AF_INET + level = syscall.SOL_IP + option = syscall.IP_RECVORIGDSTADDR + bindAddr syscall.Sockaddr = &syscall.SockaddrInet4{ + Port: int(port), + Addr: [4]byte{0, 0, 0, 0}, // INADDR_ANY + } + ) + if ipv6 { + family = syscall.AF_INET6 + level = syscall.SOL_IPV6 + option = 74 // IPV6_RECVORIGDSTADDR, which is missing from the syscall package. + bindAddr = &syscall.SockaddrInet6{ + Port: int(port), + Addr: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // in6addr_any + } + } + sockfd, err := syscall.Socket(family, syscall.SOCK_DGRAM, 0) + if err != nil { + return fmt.Errorf("failed Socket(%d, %d, 0): %w", family, syscall.SOCK_DGRAM, err) + } + defer syscall.Close(sockfd) + + if err := syscall.Bind(sockfd, bindAddr); err != nil { + return fmt.Errorf("failed Bind(%d, %+v): %v", sockfd, bindAddr, err) + } + + // Enable IP_RECVORIGDSTADDR. + if err := syscall.SetsockoptInt(sockfd, level, option, 1); err != nil { + return fmt.Errorf("failed SetsockoptByte(%d, %d, %d, 1): %v", sockfd, level, option, err) + } + + addrCh := make(chan interface{}) + errCh := make(chan error) + go func() { + var addr interface{} + var err error + if ipv6 { + addr, err = recvOrigDstAddr6(sockfd) + } else { + addr, err = recvOrigDstAddr4(sockfd) + } + if err != nil { + errCh <- err + } else { + addrCh <- addr + } + }() + + // Wait to receive a packet. + var addr interface{} + select { + case <-ctx.Done(): + return ctx.Err() + case err := <-errCh: + return err + case addr = <-addrCh: + } + + // Get a list of local IPs to verify that the packet now appears to have + // been sent to us. + var localAddrs []net.IP + if expectedDst != nil { + localAddrs = []net.IP{*expectedDst} + } else { + localAddrs, err = getInterfaceAddrs(ipv6) + if err != nil { + return fmt.Errorf("failed to get local interfaces: %w", err) + } + } + + // Verify that the address has the post-NAT port and address. + if ipv6 { + return addrMatches6(addr.(syscall.RawSockaddrInet6), localAddrs, redirectPort) + } + return addrMatches4(addr.(syscall.RawSockaddrInet4), localAddrs, redirectPort) +} + +func recvOrigDstAddr4(sockfd int) (syscall.RawSockaddrInet4, error) { + buf, err := recvOrigDstAddr(sockfd, syscall.SOL_IP, syscall.SizeofSockaddrInet4) + if err != nil { + return syscall.RawSockaddrInet4{}, err + } + var addr syscall.RawSockaddrInet4 + binary.Unmarshal(buf, usermem.ByteOrder, &addr) + return addr, nil +} + +func recvOrigDstAddr6(sockfd int) (syscall.RawSockaddrInet6, error) { + buf, err := recvOrigDstAddr(sockfd, syscall.SOL_IP, syscall.SizeofSockaddrInet6) + if err != nil { + return syscall.RawSockaddrInet6{}, err + } + var addr syscall.RawSockaddrInet6 + binary.Unmarshal(buf, usermem.ByteOrder, &addr) + return addr, nil +} + +func recvOrigDstAddr(sockfd int, level uintptr, addrSize int) ([]byte, error) { + buf := make([]byte, 64) + oob := make([]byte, syscall.CmsgSpace(addrSize)) + for { + _, oobn, _, _, err := syscall.Recvmsg( + sockfd, + buf, // Message buffer. + oob, // Out-of-band buffer. + 0) // Flags. + if errors.Is(err, syscall.EINTR) { + continue + } + if err != nil { + return nil, fmt.Errorf("failed when calling Recvmsg: %w", err) + } + oob = oob[:oobn] + + // Parse out the control message. + msgs, err := syscall.ParseSocketControlMessage(oob) + if err != nil { + return nil, fmt.Errorf("failed to parse control message: %w", err) + } + return msgs[0].Data, nil + } +} + +func addrMatches4(got syscall.RawSockaddrInet4, wantAddrs []net.IP, port uint16) error { + for _, wantAddr := range wantAddrs { + want := syscall.RawSockaddrInet4{ + Family: syscall.AF_INET, + Port: htons(port), + } + copy(want.Addr[:], wantAddr.To4()) + if got == want { + return nil + } + } + return fmt.Errorf("got %+v, but wanted one of %+v (note: port numbers are in network byte order)", got, wantAddrs) +} + +func addrMatches6(got syscall.RawSockaddrInet6, wantAddrs []net.IP, port uint16) error { + for _, wantAddr := range wantAddrs { + want := syscall.RawSockaddrInet6{ + Family: syscall.AF_INET6, + Port: htons(port), + } + copy(want.Addr[:], wantAddr.To16()) + if got == want { + return nil + } + } + return fmt.Errorf("got %+v, but wanted one of %+v (note: port numbers are in network byte order)", got, wantAddrs) +} 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 c6c95546a..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( @@ -277,6 +274,13 @@ ALL_TESTS = [ PacketimpactTestInfo( name = "tcp_rcv_buf_space", ), + PacketimpactTestInfo( + 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 b1b3c578b..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", @@ -376,6 +366,33 @@ packetimpact_testbench( ], ) +packetimpact_testbench( + name = "tcp_rack", + srcs = ["tcp_rack_test.go"], + deps = [ + "//pkg/abi/linux", + "//pkg/binary", + "//pkg/tcpip/header", + "//pkg/tcpip/seqnum", + "//pkg/usermem", + "//test/packetimpact/testbench", + "@org_golang_x_sys//unix:go_default_library", + ], +) + +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..b66e8f609 --- /dev/null +++ b/test/packetimpact/tests/tcp_info_test.go @@ -0,0 +1,103 @@ +// 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)) + 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)) + 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_rack_test.go b/test/packetimpact/tests/tcp_rack_test.go new file mode 100644 index 000000000..0a2381c97 --- /dev/null +++ b/test/packetimpact/tests/tcp_rack_test.go @@ -0,0 +1,221 @@ +// 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_rack_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/tcpip/seqnum" + "gvisor.dev/gvisor/pkg/usermem" + "gvisor.dev/gvisor/test/packetimpact/testbench" +) + +func init() { + testbench.Initialize(flag.CommandLine) +} + +const ( + // payloadSize is the size used to send packets. + payloadSize = header.TCPDefaultMSS + + // simulatedRTT is the time delay between packets sent and acked to + // increase the RTT. + simulatedRTT = 30 * time.Millisecond + + // numPktsForRTT is the number of packets sent and acked to establish + // RTT. + numPktsForRTT = 10 +) + +func createSACKConnection(t *testing.T) (testbench.DUT, testbench.TCPIPv4, int32, int32) { + dut := testbench.NewDUT(t) + listenFd, remotePort := dut.CreateListener(t, unix.SOCK_STREAM, unix.IPPROTO_TCP, 1) + conn := dut.Net.NewTCPIPv4(t, testbench.TCP{DstPort: &remotePort}, testbench.TCP{SrcPort: &remotePort}) + + // Enable SACK. + opts := make([]byte, 40) + optsOff := 0 + optsOff += header.EncodeNOP(opts[optsOff:]) + optsOff += header.EncodeNOP(opts[optsOff:]) + optsOff += header.EncodeSACKPermittedOption(opts[optsOff:]) + + conn.ConnectWithOptions(t, opts[:optsOff]) + acceptFd, _ := dut.Accept(t, listenFd) + return dut, conn, acceptFd, listenFd +} + +func closeSACKConnection(t *testing.T, dut testbench.DUT, conn testbench.TCPIPv4, acceptFd, listenFd int32) { + dut.Close(t, acceptFd) + dut.Close(t, listenFd) + conn.Close(t) +} + +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) + return time.Duration(info.RTT) * time.Microsecond, time.Duration(info.RTO) * time.Microsecond +} + +func sendAndReceive(t *testing.T, dut testbench.DUT, conn testbench.TCPIPv4, numPkts int, acceptFd int32, sendACK bool) time.Time { + seqNum1 := *conn.RemoteSeqNum(t) + payload := make([]byte, payloadSize) + var lastSent time.Time + for i, sn := 0, seqNum1; i < numPkts; i++ { + lastSent = time.Now() + dut.Send(t, acceptFd, payload, 0) + gotOne, err := conn.Expect(t, testbench.TCP{SeqNum: testbench.Uint32(uint32(sn))}, time.Second) + if err != nil { + t.Fatalf("Expect #%d: %s", i+1, err) + continue + } + if gotOne == nil { + t.Fatalf("#%d: expected a packet within a second but got none", i+1) + } + sn.UpdateForward(seqnum.Size(payloadSize)) + + if sendACK { + time.Sleep(simulatedRTT) + conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: testbench.Uint32(uint32(sn))}) + } + } + return lastSent +} + +// TestRACKTLPAllPacketsLost tests TLP when an entire flight of data is lost. +func TestRACKTLPAllPacketsLost(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 + lastSent := sendAndReceive(t, dut, conn, numPkts, acceptFd, false /* sendACK */) + + // Probe Timeout (PTO) should be two times RTT. Check that the last + // packet is retransmitted after probe timeout. + rtt, _ := getRTTAndRTO(t, dut, acceptFd) + pto := rtt * 2 + // We expect the 5th packet (the last unacknowledged packet) to be + // retransmitted. + tlpProbe := testbench.Uint32(uint32(seqNum1) + uint32((numPkts-1)*payloadSize)) + if _, err := conn.Expect(t, testbench.TCP{SeqNum: tlpProbe}, time.Second); err != nil { + t.Fatalf("expected payload was not received: %s %v %v", err, rtt, pto) + } + diff := time.Now().Sub(lastSent) + if diff < pto { + t.Fatalf("expected payload was received before the probe timeout, got: %v, want: %v", diff, pto) + } + closeSACKConnection(t, dut, conn, acceptFd, listenFd) +} + +// TestRACKTLPLost tests TLP when there are tail losses. +// See: https://tools.ietf.org/html/draft-ietf-tcpm-rack-08#section-7.4 +func TestRACKTLPLost(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 = 10 + lastSent := sendAndReceive(t, dut, conn, numPkts, acceptFd, false /* sendACK */) + + // Cumulative ACK for #[1-5] packets. + ackNum := seqNum1.Add(seqnum.Size(6 * payloadSize)) + conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: testbench.Uint32(uint32(ackNum))}) + + // Probe Timeout (PTO) should be two times RTT. Check that the last + // packet is retransmitted after probe timeout. + rtt, _ := getRTTAndRTO(t, dut, acceptFd) + pto := rtt * 2 + // We expect the 10th packet (the last unacknowledged packet) to be + // retransmitted. + tlpProbe := testbench.Uint32(uint32(seqNum1) + uint32((numPkts-1)*payloadSize)) + if _, err := conn.Expect(t, testbench.TCP{SeqNum: tlpProbe}, time.Second); err != nil { + t.Fatalf("expected payload was not received: %s", err) + } + diff := time.Now().Sub(lastSent) + if diff < pto { + t.Fatalf("expected payload was received before the probe timeout, got: %v, want: %v", diff, pto) + } + closeSACKConnection(t, dut, conn, acceptFd, listenFd) +} + +// TestRACKTLPWithSACK tests TLP by acknowledging out of order packets. +// See: https://tools.ietf.org/html/draft-ietf-tcpm-rack-08#section-8.1 +func TestRACKTLPWithSACK(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 = 3 + lastSent := sendAndReceive(t, dut, conn, numPkts, acceptFd, false /* sendACK */) + + // SACK for #2 packet. + sackBlock := make([]byte, 40) + start := seqNum1.Add(seqnum.Size(payloadSize)) + end := start.Add(seqnum.Size(payloadSize)) + 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]}) + + // RACK marks #1 packet as lost and retransmits it. + 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) + } + + // ACK for #1 packet. + conn.Send(t, testbench.TCP{Flags: testbench.Uint8(header.TCPFlagAck), AckNum: testbench.Uint32(uint32(end))}) + + // Probe Timeout (PTO) should be two times RTT. TLP will trigger for #3 + // packet. RACK adds an additional timeout of 200ms if the number of + // outstanding packets is equal to 1. + rtt, rto := getRTTAndRTO(t, dut, acceptFd) + pto := rtt*2 + (200 * time.Millisecond) + if rto < pto { + pto = rto + } + // We expect the 3rd packet (the last unacknowledged packet) to be + // retransmitted. + tlpProbe := testbench.Uint32(uint32(seqNum1) + uint32((numPkts-1)*payloadSize)) + if _, err := conn.Expect(t, testbench.TCP{SeqNum: tlpProbe}, time.Second); err != nil { + t.Fatalf("expected payload was not received: %s", err) + } + diff := time.Now().Sub(lastSent) + if diff < pto { + t.Fatalf("expected payload was received before the probe timeout, got: %v, want: %v", diff, pto) + } + 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 = ðernetBroadcastAddress + } else if tc.sendTo.IsMulticast() { + ethernetMulticastAddress := header.EthernetAddressFromMulticastIPv4Address(tcpip.Address(tc.sendTo.To4())) + ethernetLayer.DstAddr = ðernetMulticastAddress + } + expectedLayers := testbench.Layers{ðernetLayer} + + 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 0da35f7be..6ee2b73c1 100644 --- a/test/syscalls/BUILD +++ b/test/syscalls/BUILD @@ -569,7 +569,7 @@ syscall_test( # syscall_test(vfs2="True",test = "//test/syscalls/linux:sigaltstack_test") syscall_test( - test = "//test/syscalls/linux:sigiret_test", + test = "//test/syscalls/linux:sigreturn_test", ) syscall_test( diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index d184712e3..2b4b6f348 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -582,6 +582,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", @@ -797,6 +798,7 @@ cc_binary( linkstatic = 1, deps = [ ":socket_test_util", + "//test/util:capability_util", "//test/util:cleanup", "//test/util:eventfd_util", "//test/util:file_descriptor", @@ -807,6 +809,7 @@ cc_binary( "@com_google_absl//absl/strings", "@com_google_absl//absl/time", gtest, + "//test/util:memory_util", "//test/util:multiprocess_util", "//test/util:posix_error", "//test/util:save_util", @@ -978,6 +981,7 @@ cc_binary( "//test/util:epoll_util", "//test/util:file_descriptor", "//test/util:fs_util", + "//test/util:multiprocess_util", "//test/util:posix_error", "//test/util:temp_path", "//test/util:test_main", @@ -2191,11 +2195,11 @@ cc_binary( ) cc_binary( - name = "sigiret_test", + name = "sigreturn_test", testonly = 1, srcs = select_arch( - amd64 = ["sigiret.cc"], - arm64 = [], + amd64 = ["sigreturn_amd64.cc"], + arm64 = ["sigreturn_arm64.cc"], ), linkstatic = 1, deps = [ 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/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 4b581045b..4fa6751ff 100644 --- a/test/syscalls/linux/fcntl.cc +++ b/test/syscalls/linux/fcntl.cc @@ -15,6 +15,7 @@ #include <fcntl.h> #include <signal.h> #include <sys/epoll.h> +#include <sys/mman.h> #include <sys/types.h> #include <syscall.h> #include <unistd.h> @@ -35,10 +36,12 @@ #include "absl/time/clock.h" #include "absl/time/time.h" #include "test/syscalls/linux/socket_test_util.h" +#include "test/util/capability_util.h" #include "test/util/cleanup.h" #include "test/util/eventfd_util.h" #include "test/util/file_descriptor.h" #include "test/util/fs_util.h" +#include "test/util/memory_util.h" #include "test/util/multiprocess_util.h" #include "test/util/posix_error.h" #include "test/util/save_util.h" @@ -204,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)); @@ -223,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)); @@ -264,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)); @@ -392,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 = @@ -1642,6 +1738,202 @@ TEST(FcntlTest, SetFlSetOwnSetSigDoNotRace) { } } +TEST_F(FcntlLockTest, GetLockOnNothing) { + SKIP_IF(IsRunningWithVFS1()); + auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); + FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDWR, 0666)); + + struct flock fl; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 40; + ASSERT_THAT(fcntl(fd.get(), F_GETLK, &fl), SyscallSucceeds()); + ASSERT_TRUE(fl.l_type == F_UNLCK); +} + +TEST_F(FcntlLockTest, GetLockOnLockSameProcess) { + SKIP_IF(IsRunningWithVFS1()); + auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); + FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDWR, 0666)); + + struct flock fl; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 40; + ASSERT_THAT(fcntl(fd.get(), F_SETLK, &fl), SyscallSucceeds()); + ASSERT_THAT(fcntl(fd.get(), F_GETLK, &fl), SyscallSucceeds()); + ASSERT_TRUE(fl.l_type == F_UNLCK); + + fl.l_type = F_WRLCK; + ASSERT_THAT(fcntl(fd.get(), F_SETLK, &fl), SyscallSucceeds()); + ASSERT_THAT(fcntl(fd.get(), F_GETLK, &fl), SyscallSucceeds()); + ASSERT_TRUE(fl.l_type == F_UNLCK); +} + +TEST_F(FcntlLockTest, GetReadLockOnReadLock) { + SKIP_IF(IsRunningWithVFS1()); + auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); + FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDWR, 0666)); + + struct flock fl; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 40; + ASSERT_THAT(fcntl(fd.get(), F_SETLK, &fl), SyscallSucceeds()); + + pid_t child_pid = fork(); + if (child_pid == 0) { + TEST_CHECK(fcntl(fd.get(), F_GETLK, &fl) >= 0); + TEST_CHECK(fl.l_type == F_UNLCK); + _exit(0); + } + int status; + ASSERT_THAT(waitpid(child_pid, &status, 0), + SyscallSucceedsWithValue(child_pid)); + ASSERT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); +} + +TEST_F(FcntlLockTest, GetReadLockOnWriteLock) { + SKIP_IF(IsRunningWithVFS1()); + auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); + FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDWR, 0666)); + + struct flock fl; + fl.l_type = F_WRLCK; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 40; + ASSERT_THAT(fcntl(fd.get(), F_SETLK, &fl), SyscallSucceeds()); + + fl.l_type = F_RDLCK; + pid_t child_pid = fork(); + if (child_pid == 0) { + TEST_CHECK(fcntl(fd.get(), F_GETLK, &fl) >= 0); + TEST_CHECK(fl.l_type == F_WRLCK); + TEST_CHECK(fl.l_pid == getppid()); + _exit(0); + } + + int status; + ASSERT_THAT(waitpid(child_pid, &status, 0), + SyscallSucceedsWithValue(child_pid)); + ASSERT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); +} + +TEST_F(FcntlLockTest, GetWriteLockOnReadLock) { + SKIP_IF(IsRunningWithVFS1()); + auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); + FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDWR, 0666)); + + struct flock fl; + fl.l_type = F_RDLCK; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 40; + ASSERT_THAT(fcntl(fd.get(), F_SETLK, &fl), SyscallSucceeds()); + + fl.l_type = F_WRLCK; + pid_t child_pid = fork(); + if (child_pid == 0) { + TEST_CHECK(fcntl(fd.get(), F_GETLK, &fl) >= 0); + TEST_CHECK(fl.l_type == F_RDLCK); + TEST_CHECK(fl.l_pid == getppid()); + _exit(0); + } + + int status; + ASSERT_THAT(waitpid(child_pid, &status, 0), + SyscallSucceedsWithValue(child_pid)); + ASSERT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); +} + +TEST_F(FcntlLockTest, GetWriteLockOnWriteLock) { + SKIP_IF(IsRunningWithVFS1()); + auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); + FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDWR, 0666)); + + struct flock fl; + fl.l_type = F_WRLCK; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 40; + ASSERT_THAT(fcntl(fd.get(), F_SETLK, &fl), SyscallSucceeds()); + + pid_t child_pid = fork(); + if (child_pid == 0) { + TEST_CHECK(fcntl(fd.get(), F_GETLK, &fl) >= 0); + TEST_CHECK(fl.l_type == F_WRLCK); + TEST_CHECK(fl.l_pid == getppid()); + _exit(0); + } + + int status; + ASSERT_THAT(waitpid(child_pid, &status, 0), + SyscallSucceedsWithValue(child_pid)); + ASSERT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); +} + +// Tests that the pid returned from F_GETLK is relative to the caller's PID +// namespace. +TEST_F(FcntlLockTest, GetLockRespectsPIDNamespace) { + SKIP_IF(IsRunningWithVFS1()); + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN))); + auto file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); + std::string filename = file.path(); + const FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(Open(filename, O_RDWR, 0666)); + + // Lock in the parent process. + struct flock fl; + fl.l_type = F_WRLCK; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 40; + ASSERT_THAT(fcntl(fd.get(), F_SETLK, &fl), SyscallSucceeds()); + + auto child_getlk = [](void* filename) { + int fd = open((char*)filename, O_RDWR, 0666); + TEST_CHECK(fd >= 0); + + struct flock fl; + fl.l_type = F_WRLCK; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 40; + TEST_CHECK(fcntl(fd, F_GETLK, &fl) >= 0); + TEST_CHECK(fl.l_type == F_WRLCK); + // Parent PID should be 0 in the child PID namespace. + TEST_CHECK(fl.l_pid == 0); + close(fd); + return 0; + }; + + // Set up child process in a new PID namespace. + constexpr int kStackSize = 4096; + Mapping stack = ASSERT_NO_ERRNO_AND_VALUE( + Mmap(nullptr, kStackSize, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0)); + pid_t child_pid; + ASSERT_THAT( + child_pid = clone(child_getlk, (char*)stack.ptr() + stack.len(), + CLONE_NEWPID | SIGCHLD, (void*)filename.c_str()), + SyscallSucceeds()); + + int status; + ASSERT_THAT(waitpid(child_pid, &status, 0), + SyscallSucceedsWithValue(child_pid)); + ASSERT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); +} + } // namespace } // namespace testing 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/inotify.cc b/test/syscalls/linux/inotify.cc index 8137f0e29..a88c89e20 100644 --- a/test/syscalls/linux/inotify.cc +++ b/test/syscalls/linux/inotify.cc @@ -36,6 +36,7 @@ #include "test/util/epoll_util.h" #include "test/util/file_descriptor.h" #include "test/util/fs_util.h" +#include "test/util/multiprocess_util.h" #include "test/util/posix_error.h" #include "test/util/temp_path.h" #include "test/util/test_util.h" @@ -315,8 +316,7 @@ PosixErrorOr<std::vector<Event>> DrainEvents(int fd) { } PosixErrorOr<FileDescriptor> InotifyInit1(int flags) { - int fd; - EXPECT_THAT(fd = inotify_init1(flags), SyscallSucceeds()); + int fd = inotify_init1(flags); if (fd < 0) { return PosixError(errno, "inotify_init1() failed"); } @@ -325,9 +325,7 @@ PosixErrorOr<FileDescriptor> InotifyInit1(int flags) { PosixErrorOr<int> InotifyAddWatch(int fd, const std::string& path, uint32_t mask) { - int wd; - EXPECT_THAT(wd = inotify_add_watch(fd, path.c_str(), mask), - SyscallSucceeds()); + int wd = inotify_add_watch(fd, path.c_str(), mask); if (wd < 0) { return PosixError(errno, "inotify_add_watch() failed"); } @@ -784,6 +782,38 @@ TEST(Inotify, MoveWatchedTargetGeneratesEvents) { EXPECT_EQ(events[0].cookie, events[1].cookie); } +// Tests that close events are only emitted when a file description drops its +// last reference. +TEST(Inotify, DupFD) { + SKIP_IF(IsRunningWithVFS1()); + + const TempPath file = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); + const FileDescriptor inotify_fd = + ASSERT_NO_ERRNO_AND_VALUE(InotifyInit1(IN_NONBLOCK)); + + const int wd = ASSERT_NO_ERRNO_AND_VALUE( + InotifyAddWatch(inotify_fd.get(), file.path(), IN_ALL_EVENTS)); + + FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(Open(file.path(), O_RDONLY)); + FileDescriptor fd2 = ASSERT_NO_ERRNO_AND_VALUE(fd.Dup()); + + std::vector<Event> events = + ASSERT_NO_ERRNO_AND_VALUE(DrainEvents(inotify_fd.get())); + EXPECT_THAT(events, Are({ + Event(IN_OPEN, wd), + })); + + fd.reset(); + events = ASSERT_NO_ERRNO_AND_VALUE(DrainEvents(inotify_fd.get())); + EXPECT_THAT(events, Are({})); + + fd2.reset(); + events = ASSERT_NO_ERRNO_AND_VALUE(DrainEvents(inotify_fd.get())); + EXPECT_THAT(events, Are({ + Event(IN_CLOSE_NOWRITE, wd), + })); +} + TEST(Inotify, CoalesceEvents) { const TempPath root = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); const FileDescriptor fd = @@ -1779,11 +1809,9 @@ TEST(Inotify, Sendfile) { EXPECT_THAT(out_events, Are({Event(IN_MODIFY, out_wd)})); } -// On Linux, inotify behavior is not very consistent with splice(2). We try our -// best to emulate Linux for very basic calls to splice. TEST(Inotify, SpliceOnWatchTarget) { - int pipes[2]; - ASSERT_THAT(pipe2(pipes, O_NONBLOCK), SyscallSucceeds()); + int pipefds[2]; + ASSERT_THAT(pipe2(pipefds, O_NONBLOCK), SyscallSucceeds()); const TempPath dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); const FileDescriptor inotify_fd = @@ -1798,15 +1826,20 @@ TEST(Inotify, SpliceOnWatchTarget) { const int file_wd = ASSERT_NO_ERRNO_AND_VALUE( InotifyAddWatch(inotify_fd.get(), file.path(), IN_ALL_EVENTS)); - EXPECT_THAT(splice(fd.get(), nullptr, pipes[1], nullptr, 1, /*flags=*/0), + EXPECT_THAT(splice(fd.get(), nullptr, pipefds[1], nullptr, 1, /*flags=*/0), SyscallSucceedsWithValue(1)); - // Surprisingly, events are not generated in Linux if we read from a file. + // Surprisingly, events may not be generated in Linux if we read from a file. + // fs/splice.c:generic_file_splice_read, which is used most often, does not + // generate events, whereas fs/splice.c:default_file_splice_read does. std::vector<Event> events = ASSERT_NO_ERRNO_AND_VALUE(DrainEvents(inotify_fd.get())); - ASSERT_THAT(events, Are({})); + if (IsRunningOnGvisor() && !IsRunningWithVFS1()) { + ASSERT_THAT(events, Are({Event(IN_ACCESS, dir_wd, Basename(file.path())), + Event(IN_ACCESS, file_wd)})); + } - EXPECT_THAT(splice(pipes[0], nullptr, fd.get(), nullptr, 1, /*flags=*/0), + EXPECT_THAT(splice(pipefds[0], nullptr, fd.get(), nullptr, 1, /*flags=*/0), SyscallSucceedsWithValue(1)); events = ASSERT_NO_ERRNO_AND_VALUE(DrainEvents(inotify_fd.get())); @@ -1817,8 +1850,8 @@ TEST(Inotify, SpliceOnWatchTarget) { } TEST(Inotify, SpliceOnInotifyFD) { - int pipes[2]; - ASSERT_THAT(pipe2(pipes, O_NONBLOCK), SyscallSucceeds()); + int pipefds[2]; + ASSERT_THAT(pipe2(pipefds, O_NONBLOCK), SyscallSucceeds()); const TempPath root = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); const FileDescriptor fd = @@ -1834,11 +1867,11 @@ TEST(Inotify, SpliceOnInotifyFD) { char buf; EXPECT_THAT(read(file1_fd.get(), &buf, 1), SyscallSucceeds()); - EXPECT_THAT(splice(fd.get(), nullptr, pipes[1], nullptr, + EXPECT_THAT(splice(fd.get(), nullptr, pipefds[1], nullptr, sizeof(struct inotify_event) + 1, SPLICE_F_NONBLOCK), SyscallSucceedsWithValue(sizeof(struct inotify_event))); - const FileDescriptor read_fd(pipes[0]); + const FileDescriptor read_fd(pipefds[0]); const std::vector<Event> events = ASSERT_NO_ERRNO_AND_VALUE(DrainEvents(read_fd.get())); ASSERT_THAT(events, Are({Event(IN_ACCESS, watcher)})); @@ -1936,24 +1969,29 @@ TEST(Inotify, Xattr) { } TEST(Inotify, Exec) { - const TempPath dir = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateDir()); - const TempPath bin = ASSERT_NO_ERRNO_AND_VALUE( - TempPath::CreateSymlinkTo(dir.path(), "/bin/true")); - + SKIP_IF(IsRunningWithVFS1()); const FileDescriptor fd = ASSERT_NO_ERRNO_AND_VALUE(InotifyInit1(IN_NONBLOCK)); const int wd = ASSERT_NO_ERRNO_AND_VALUE( - InotifyAddWatch(fd.get(), bin.path(), IN_ALL_EVENTS)); + InotifyAddWatch(fd.get(), "/bin/true", IN_ALL_EVENTS)); // Perform exec. - ScopedThread t([&bin]() { - ASSERT_THAT(execl(bin.path().c_str(), bin.path().c_str(), (char*)nullptr), - SyscallSucceeds()); - }); - t.Join(); + pid_t child = -1; + int execve_errno = -1; + auto kill = ASSERT_NO_ERRNO_AND_VALUE( + ForkAndExec("/bin/true", {}, {}, nullptr, &child, &execve_errno)); + ASSERT_EQ(0, execve_errno); + + int status; + ASSERT_THAT(RetryEINTR(waitpid)(child, &status, 0), SyscallSucceeds()); + EXPECT_EQ(0, status); + + // Process cleanup no longer needed. + kill.Release(); std::vector<Event> events = ASSERT_NO_ERRNO_AND_VALUE(DrainEvents(fd.get())); - EXPECT_THAT(events, Are({Event(IN_OPEN, wd), Event(IN_ACCESS, wd)})); + EXPECT_THAT(events, Are({Event(IN_OPEN, wd), Event(IN_ACCESS, wd), + Event(IN_CLOSE_NOWRITE, wd)})); } // Watches without IN_EXCL_UNLINK, should continue to emit events for file 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..733b17834 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 @@ -517,6 +517,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/packet_socket_raw.cc b/test/syscalls/linux/packet_socket_raw.cc index 2ed4f6f9c..d25be0e30 100644 --- a/test/syscalls/linux/packet_socket_raw.cc +++ b/test/syscalls/linux/packet_socket_raw.cc @@ -548,13 +548,7 @@ TEST_P(RawPacketTest, SetSocketSendBuf) { ASSERT_THAT(getsockopt(s_, SOL_SOCKET, SO_SNDBUF, &val, &val_len), SyscallSucceeds()); - // Linux doubles the value set by SO_SNDBUF/SO_RCVBUF. - // TODO(gvisor.dev/issue/2926): Remove the gvisor special casing when Netstack - // matches linux behavior. - if (!IsRunningOnGvisor()) { - quarter_sz *= 2; - } - + quarter_sz *= 2; ASSERT_EQ(quarter_sz, val); } 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/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/raw_socket.cc b/test/syscalls/linux/raw_socket.cc index 955bcee4b..32924466f 100644 --- a/test/syscalls/linux/raw_socket.cc +++ b/test/syscalls/linux/raw_socket.cc @@ -621,13 +621,7 @@ TEST_P(RawSocketTest, SetSocketSendBuf) { ASSERT_THAT(getsockopt(s_, SOL_SOCKET, SO_SNDBUF, &val, &val_len), SyscallSucceeds()); - // Linux doubles the value set by SO_SNDBUF/SO_RCVBUF. - // TODO(gvisor.dev/issue/2926): Remove the gvisor special casing when Netstack - // matches linux behavior. - if (!IsRunningOnGvisor()) { - quarter_sz *= 2; - } - + quarter_sz *= 2; ASSERT_EQ(quarter_sz, val); } 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/shm.cc b/test/syscalls/linux/shm.cc index d6e8b3e59..baf794152 100644 --- a/test/syscalls/linux/shm.cc +++ b/test/syscalls/linux/shm.cc @@ -256,32 +256,26 @@ TEST(ShmTest, IpcInfo) { } TEST(ShmTest, ShmInfo) { - struct shm_info info; - - // We generally can't know what other processes on a linux machine - // does with shared memory segments, so we can't test specific - // numbers on Linux. When running under gvisor, we're guaranteed to - // be the only ones using shm, so we can easily verify machine-wide - // numbers. - if (IsRunningOnGvisor()) { - ASSERT_NO_ERRNO(Shmctl(0, SHM_INFO, &info)); - EXPECT_EQ(info.used_ids, 0); - EXPECT_EQ(info.shm_tot, 0); - EXPECT_EQ(info.shm_rss, 0); - EXPECT_EQ(info.shm_swp, 0); - } + // Take a snapshot of the system before the test runs. + struct shm_info snap; + ASSERT_NO_ERRNO(Shmctl(0, SHM_INFO, &snap)); const ShmSegment shm = ASSERT_NO_ERRNO_AND_VALUE( Shmget(IPC_PRIVATE, kAllocSize, IPC_CREAT | 0777)); const char* addr = ASSERT_NO_ERRNO_AND_VALUE(Shmat(shm.id(), nullptr, 0)); + struct shm_info info; ASSERT_NO_ERRNO(Shmctl(1, SHM_INFO, &info)); + // We generally can't know what other processes on a linux machine do with + // shared memory segments, so we can't test specific numbers on Linux. When + // running under gvisor, we're guaranteed to be the only ones using shm, so + // we can easily verify machine-wide numbers. if (IsRunningOnGvisor()) { ASSERT_NO_ERRNO(Shmctl(shm.id(), SHM_INFO, &info)); - EXPECT_EQ(info.used_ids, 1); - EXPECT_EQ(info.shm_tot, kAllocSize / kPageSize); - EXPECT_EQ(info.shm_rss, kAllocSize / kPageSize); + EXPECT_EQ(info.used_ids, snap.used_ids + 1); + EXPECT_EQ(info.shm_tot, snap.shm_tot + (kAllocSize / kPageSize)); + EXPECT_EQ(info.shm_rss, snap.shm_rss + (kAllocSize / kPageSize)); EXPECT_EQ(info.shm_swp, 0); // Gvisor currently never swaps. } @@ -378,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/sigiret.cc b/test/syscalls/linux/sigreturn_amd64.cc index 6227774a4..6227774a4 100644 --- a/test/syscalls/linux/sigiret.cc +++ b/test/syscalls/linux/sigreturn_amd64.cc diff --git a/test/syscalls/linux/sigreturn_arm64.cc b/test/syscalls/linux/sigreturn_arm64.cc new file mode 100644 index 000000000..2c19e2984 --- /dev/null +++ b/test/syscalls/linux/sigreturn_arm64.cc @@ -0,0 +1,97 @@ +// Copyright 2021 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 <linux/unistd.h> +#include <signal.h> +#include <sys/syscall.h> +#include <sys/types.h> +#include <sys/ucontext.h> +#include <unistd.h> + +#include "gtest/gtest.h" +#include "test/util/logging.h" +#include "test/util/signal_util.h" +#include "test/util/test_util.h" +#include "test/util/timer_util.h" + +namespace gvisor { +namespace testing { + +namespace { + +constexpr uint64_t kOrigX7 = 0xdeadbeeffacefeed; + +void sigvtalrm(int sig, siginfo_t* siginfo, void* _uc) { + ucontext_t* uc = reinterpret_cast<ucontext_t*>(_uc); + + // Verify that: + // - x7 value in mcontext_t matches kOrigX7. + if (uc->uc_mcontext.regs[7] == kOrigX7) { + // Modify the value x7 in the ucontext. This is the value seen by the + // application after the signal handler returns. + uc->uc_mcontext.regs[7] = ~kOrigX7; + } +} + +int testX7(uint64_t* val, uint64_t sysno, uint64_t tgid, uint64_t tid, + uint64_t signo) { + register uint64_t* x9 __asm__("x9") = val; + register uint64_t x8 __asm__("x8") = sysno; + register uint64_t x0 __asm__("x0") = tgid; + register uint64_t x1 __asm__("x1") = tid; + register uint64_t x2 __asm__("x2") = signo; + + // Initialize x7, send SIGVTALRM to itself and read x7. + __asm__( + "ldr x7, [x9, 0]\n" + "svc 0\n" + "str x7, [x9, 0]\n" + : "=r"(x0) + : "r"(x0), "r"(x1), "r"(x2), "r"(x9), "r"(x8) + : "x7"); + return x0; +} + +// On ARM64, when ptrace stops on a system call, it uses the x7 register to +// indicate whether the stop has been signalled from syscall entry or syscall +// exit. This means that we can't get a value of this register and we can't +// change it. More details are in the comment for tracehook_report_syscall in +// arch/arm64/kernel/ptrace.c. +// +// CheckR7 checks that the ptrace platform handles the x7 register properly. +TEST(SigreturnTest, CheckX7) { + // Setup signal handler for SIGVTALRM. + struct sigaction sa = {}; + sigfillset(&sa.sa_mask); + sa.sa_sigaction = sigvtalrm; + sa.sa_flags = SA_SIGINFO; + auto const action_cleanup = + ASSERT_NO_ERRNO_AND_VALUE(ScopedSigaction(SIGVTALRM, sa)); + + auto const mask_cleanup = + ASSERT_NO_ERRNO_AND_VALUE(ScopedSignalMask(SIG_UNBLOCK, SIGVTALRM)); + + uint64_t x7 = kOrigX7; + + testX7(&x7, __NR_tgkill, getpid(), syscall(__NR_gettid), SIGVTALRM); + + // The following check verifies that %x7 was not clobbered + // when returning from the signal handler (via sigreturn(2)). + EXPECT_EQ(x7, ~kOrigX7); +} + +} // namespace + +} // namespace testing +} // namespace gvisor diff --git a/test/syscalls/linux/socket.cc b/test/syscalls/linux/socket.cc index 32f583581..b616c2c87 100644 --- a/test/syscalls/linux/socket.cc +++ b/test/syscalls/linux/socket.cc @@ -16,6 +16,7 @@ #include <sys/stat.h> #include <sys/statfs.h> #include <sys/types.h> +#include <sys/wait.h> #include <unistd.h> #include "gtest/gtest.h" @@ -90,8 +91,7 @@ TEST(SocketTest, UnixSocketStat) { EXPECT_EQ(statbuf.st_mode, S_IFSOCK | (sock_perm & ~mask)); // Timestamps should be equal and non-zero. - // TODO(b/158882152): Sockets currently don't implement timestamps. - if (!IsRunningOnGvisor()) { + if (!IsRunningWithVFS1()) { EXPECT_NE(statbuf.st_atime, 0); EXPECT_EQ(statbuf.st_atime, statbuf.st_mtime); EXPECT_EQ(statbuf.st_atime, statbuf.st_ctime); @@ -111,6 +111,77 @@ TEST(SocketTest, UnixSocketStatFS) { EXPECT_EQ(st.f_namelen, NAME_MAX); } +TEST(SocketTest, UnixSCMRightsOnlyPassedOnce_NoRandomSave) { + const DisableSave ds; + + int sockets[2]; + ASSERT_THAT(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets), SyscallSucceeds()); + // Send more than what will fit inside the send/receive buffers, so that it is + // split into multiple messages. + constexpr int kBufSize = 0x100000; + + pid_t pid = fork(); + if (pid == 0) { + TEST_PCHECK(close(sockets[0]) == 0); + + // Construct a message with some control message. + struct msghdr msg = {}; + char control[CMSG_SPACE(sizeof(int))] = {}; + std::vector<char> buf(kBufSize); + struct iovec iov = {}; + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + + struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + ((int*)CMSG_DATA(cmsg))[0] = sockets[1]; + + iov.iov_base = buf.data(); + iov.iov_len = kBufSize; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + int n = sendmsg(sockets[1], &msg, 0); + TEST_PCHECK(n == kBufSize); + TEST_PCHECK(shutdown(sockets[1], SHUT_RDWR) == 0); + TEST_PCHECK(close(sockets[1]) == 0); + _exit(0); + } + + close(sockets[1]); + + struct msghdr msg = {}; + char control[CMSG_SPACE(sizeof(int))] = {}; + std::vector<char> buf(kBufSize); + struct iovec iov = {}; + msg.msg_control = &control; + msg.msg_controllen = sizeof(control); + + iov.iov_base = buf.data(); + iov.iov_len = kBufSize; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + // The control message should only be present in the first message received. + int n; + ASSERT_THAT(n = recvmsg(sockets[0], &msg, 0), SyscallSucceeds()); + ASSERT_GT(n, 0); + ASSERT_EQ(msg.msg_controllen, CMSG_SPACE(sizeof(int))); + + while (n > 0) { + ASSERT_THAT(n = recvmsg(sockets[0], &msg, 0), SyscallSucceeds()); + ASSERT_EQ(msg.msg_controllen, 0); + } + + close(sockets[0]); + + int status; + ASSERT_THAT(waitpid(pid, &status, 0), SyscallSucceedsWithValue(pid)); + ASSERT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); +} + using SocketOpenTest = ::testing::TestWithParam<int>; // UDS cannot be opened. diff --git a/test/syscalls/linux/socket_ip_tcp_generic.cc b/test/syscalls/linux/socket_ip_tcp_generic.cc index 831d96262..a73987a7e 100644 --- a/test/syscalls/linux/socket_ip_tcp_generic.cc +++ b/test/syscalls/linux/socket_ip_tcp_generic.cc @@ -65,6 +65,33 @@ TEST_P(TCPSocketPairTest, ZeroTcpInfoSucceeds) { SyscallSucceeds()); } +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()); + + // Validates the received tcp_info fields. + EXPECT_EQ(opt.tcpi_ca_state, 0); + 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/socket_ipv4_udp_unbound.cc b/test/syscalls/linux/socket_ipv4_udp_unbound.cc index e557572a7..8eec31a46 100644 --- a/test/syscalls/linux/socket_ipv4_udp_unbound.cc +++ b/test/syscalls/linux/socket_ipv4_udp_unbound.cc @@ -2513,11 +2513,7 @@ TEST_P(IPv4UDPUnboundSocketTest, SetSocketSendBuf) { ASSERT_THAT(getsockopt(s->get(), SOL_SOCKET, SO_SNDBUF, &val, &val_len), SyscallSucceeds()); - // Linux doubles the value set by SO_SNDBUF/SO_RCVBUF. - if (!IsRunningOnGvisor()) { - quarter_sz *= 2; - } - + quarter_sz *= 2; ASSERT_EQ(quarter_sz, val); } diff --git a/test/syscalls/linux/socket_unix_cmsg.cc b/test/syscalls/linux/socket_unix_cmsg.cc index a16899493..22a4ee0d1 100644 --- a/test/syscalls/linux/socket_unix_cmsg.cc +++ b/test/syscalls/linux/socket_unix_cmsg.cc @@ -362,7 +362,7 @@ TEST_P(UnixSocketPairCmsgTest, BasicThreeFDPassTruncationMsgCtrunc) { // BasicFDPassUnalignedRecv starts off by sending a single FD just like // BasicFDPass. The difference is that when calling recvmsg, the length of the -// receive data is only aligned on a 4 byte boundry instead of the normal 8. +// receive data is only aligned on a 4 byte boundary instead of the normal 8. TEST_P(UnixSocketPairCmsgTest, BasicFDPassUnalignedRecv) { auto sockets = ASSERT_NO_ERRNO_AND_VALUE(NewSocketPair()); 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 714848b8e..9028ab024 100644 --- a/test/syscalls/linux/tcp_socket.cc +++ b/test/syscalls/linux/tcp_socket.cc @@ -2008,6 +2008,29 @@ TEST_P(SimpleTcpSocketTest, GetSocketAcceptConnWithShutdown) { EXPECT_EQ(got, 0); } +// Tests that connecting to an unspecified address results in ECONNREFUSED. +TEST_P(SimpleTcpSocketTest, ConnectUnspecifiedAddress) { + sockaddr_storage addr; + socklen_t addrlen = sizeof(addr); + memset(&addr, 0, addrlen); + addr.ss_family = GetParam(); + auto do_connect = [&addr, addrlen]() { + FileDescriptor s = ASSERT_NO_ERRNO_AND_VALUE( + Socket(addr.ss_family, SOCK_STREAM, IPPROTO_TCP)); + ASSERT_THAT( + RetryEINTR(connect)(s.get(), reinterpret_cast<struct sockaddr*>(&addr), + addrlen), + SyscallFailsWithErrno(ECONNREFUSED)); + }; + do_connect(); + // Test the v4 mapped address as well. + if (GetParam() == AF_INET6) { + auto sin6 = reinterpret_cast<struct sockaddr_in6*>(&addr); + sin6->sin6_addr.s6_addr[10] = sin6->sin6_addr.s6_addr[11] = 0xff; + do_connect(); + } +} + INSTANTIATE_TEST_SUITE_P(AllInetTests, SimpleTcpSocketTest, ::testing::Values(AF_INET, AF_INET6)); diff --git a/test/syscalls/linux/truncate.cc b/test/syscalls/linux/truncate.cc index 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/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/BUILD b/test/util/BUILD index 1b028a477..e561f3daa 100644 --- a/test/util/BUILD +++ b/test/util/BUILD @@ -172,6 +172,7 @@ cc_library( ":posix_error", ":save_util", ":test_util", + gtest, "@com_google_absl//absl/strings", ], ) 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..9d224ea05 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,39 @@ 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(); \ + }) + } // namespace testing } // namespace gvisor diff --git a/test/util/multiprocess_util.cc b/test/util/multiprocess_util.cc index 8b676751b..a6b0de24b 100644 --- a/test/util/multiprocess_util.cc +++ b/test/util/multiprocess_util.cc @@ -154,6 +154,9 @@ PosixErrorOr<int> InForkedProcess(const std::function<void()>& fn) { pid_t pid = fork(); if (pid == 0) { fn(); + TEST_CHECK_MSG(!::testing::Test::HasFailure(), + "EXPECT*/ASSERT* failed. These are not async-signal-safe " + "and must not be called from fn."); _exit(0); } MaybeSave(); 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> |