summaryrefslogtreecommitdiffhomepage
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/fsstress/fsstress_test.go2
-rw-r--r--test/iptables/BUILD2
-rw-r--r--test/iptables/filter_input.go198
-rw-r--r--test/iptables/iptables_test.go32
-rw-r--r--test/iptables/iptables_util.go2
-rw-r--r--test/iptables/nat.go265
-rw-r--r--test/packetimpact/dut/BUILD2
-rw-r--r--test/packetimpact/dut/posix_server.cc40
-rw-r--r--test/packetimpact/proto/posix_server.proto23
-rw-r--r--test/packetimpact/runner/defs.bzl10
-rw-r--r--test/packetimpact/testbench/connections.go40
-rw-r--r--test/packetimpact/testbench/dut.go52
-rw-r--r--test/packetimpact/tests/BUILD41
-rw-r--r--test/packetimpact/tests/ipv4_fragment_reassembly_test.go12
-rw-r--r--test/packetimpact/tests/tcp_info_test.go103
-rw-r--r--test/packetimpact/tests/tcp_noaccept_close_rst_test.go15
-rw-r--r--test/packetimpact/tests/tcp_rack_test.go221
-rw-r--r--test/packetimpact/tests/tcp_zero_window_probe_retransmit_test.go28
-rw-r--r--test/packetimpact/tests/udp_recv_mcast_bcast_test.go115
-rw-r--r--test/packetimpact/tests/udp_send_recv_dgram_test.go338
-rw-r--r--test/syscalls/BUILD2
-rw-r--r--test/syscalls/linux/BUILD10
-rw-r--r--test/syscalls/linux/chmod.cc36
-rw-r--r--test/syscalls/linux/chown.cc38
-rw-r--r--test/syscalls/linux/dup.cc85
-rw-r--r--test/syscalls/linux/fadvise64.cc11
-rw-r--r--test/syscalls/linux/fallocate.cc7
-rw-r--r--test/syscalls/linux/fchdir.cc12
-rw-r--r--test/syscalls/linux/fcntl.cc292
-rw-r--r--test/syscalls/linux/getdents.cc26
-rw-r--r--test/syscalls/linux/inotify.cc94
-rw-r--r--test/syscalls/linux/ioctl.cc13
-rw-r--r--test/syscalls/linux/link.cc55
-rw-r--r--test/syscalls/linux/madvise.cc12
-rw-r--r--test/syscalls/linux/mmap.cc12
-rw-r--r--test/syscalls/linux/open.cc22
-rw-r--r--test/syscalls/linux/packet_socket_raw.cc8
-rw-r--r--test/syscalls/linux/ping_socket.cc55
-rw-r--r--test/syscalls/linux/pread64.cc10
-rw-r--r--test/syscalls/linux/preadv.cc14
-rw-r--r--test/syscalls/linux/preadv2.cc18
-rw-r--r--test/syscalls/linux/pty.cc11
-rw-r--r--test/syscalls/linux/pwrite64.cc11
-rw-r--r--test/syscalls/linux/pwritev2.cc17
-rw-r--r--test/syscalls/linux/raw_socket.cc8
-rw-r--r--test/syscalls/linux/read.cc9
-rw-r--r--test/syscalls/linux/readv.cc14
-rw-r--r--test/syscalls/linux/shm.cc36
-rw-r--r--test/syscalls/linux/sigreturn_amd64.cc (renamed from test/syscalls/linux/sigiret.cc)0
-rw-r--r--test/syscalls/linux/sigreturn_arm64.cc97
-rw-r--r--test/syscalls/linux/socket.cc75
-rw-r--r--test/syscalls/linux/socket_ip_tcp_generic.cc27
-rw-r--r--test/syscalls/linux/socket_ipv4_udp_unbound.cc6
-rw-r--r--test/syscalls/linux/socket_unix_cmsg.cc2
-rw-r--r--test/syscalls/linux/stat.cc37
-rw-r--r--test/syscalls/linux/statfs.cc10
-rw-r--r--test/syscalls/linux/symlink.cc30
-rw-r--r--test/syscalls/linux/sync.cc12
-rw-r--r--test/syscalls/linux/tcp_socket.cc23
-rw-r--r--test/syscalls/linux/truncate.cc10
-rw-r--r--test/syscalls/linux/write.cc38
-rw-r--r--test/syscalls/linux/xattr.cc21
-rw-r--r--test/util/BUILD1
-rw-r--r--test/util/fs_util.cc16
-rw-r--r--test/util/fs_util.h2
-rw-r--r--test/util/logging.cc8
-rw-r--r--test/util/logging.h39
-rw-r--r--test/util/multiprocess_util.cc3
-rw-r--r--test/util/multiprocess_util.h3
-rw-r--r--test/util/posix_error.cc2
-rw-r--r--test/util/posix_error.h14
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 = &ethernetBroadcastAddress
+ } else if tc.sendTo.IsMulticast() {
+ ethernetMulticastAddress := header.EthernetAddressFromMulticastIPv4Address(tcpip.Address(tc.sendTo.To4()))
+ ethernetLayer.DstAddr = &ethernetMulticastAddress
+ }
+ expectedLayers := testbench.Layers{&ethernetLayer}
+
+ var conn udpConn
+ if sendTo4 := tc.sendTo.To4(); sendTo4 != nil {
+ v4Conn := dut.Net.NewUDPIPv4(t, outgoingUDP, incomingUDP)
+ conn = &v4Conn
+ expectedLayers = append(
+ expectedLayers,
+ &testbench.IPv4{
+ DstAddr: testbench.Address(tcpip.Address(sendTo4)),
+ },
+ )
+ } else {
+ v6Conn := dut.Net.NewUDPIPv6(t, outgoingUDP, incomingUDP)
+ conn = &v6Conn
+ expectedLayers = append(
+ expectedLayers,
+ &testbench.IPv6{
+ DstAddr: testbench.Address(tcpip.Address(tc.sendTo)),
+ },
+ )
+ }
+ defer conn.Close(t)
+
+ expectedLayers = append(expectedLayers, &incomingUDP)
+ for _, v := range []struct {
+ name string
+ payload []byte
+ }{
+ {"emptypayload", nil},
+ {"small payload", []byte("hello world")},
+ {"1kPayload", testbench.GenerateRandomPayload(t, 1<<10)},
+ // Even though UDP allows larger dgrams we don't test it here as
+ // they need to be fragmented and written out as individual
+ // frames.
+ } {
+ runTc(t, dut, conn, socketFD, tc, v.payload, expectedLayers)
}
}
diff --git a/test/syscalls/BUILD b/test/syscalls/BUILD
index 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>