tun_file_linux_test.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. //go:build linux && !android && !e2e_testing
  2. // +build linux,!android,!e2e_testing
  3. package tio
  4. import (
  5. "errors"
  6. "log/slog"
  7. "os"
  8. "sync"
  9. "testing"
  10. "time"
  11. "github.com/stretchr/testify/require"
  12. "golang.org/x/sys/unix"
  13. )
  14. // newReadPipe returns a read fd. The matching write fd is registered for cleanup.
  15. // The caller takes ownership of the read fd (pass it into a QueueSet).
  16. func newReadPipe(t *testing.T) int {
  17. t.Helper()
  18. var fds [2]int
  19. if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil {
  20. t.Fatalf("pipe2: %v", err)
  21. }
  22. t.Cleanup(func() { _ = unix.Close(fds[1]) })
  23. return fds[0]
  24. }
  25. func TestPoll_WakeForShutdown_WakesFriends(t *testing.T) {
  26. pipe1 := newReadPipe(t)
  27. pipe2 := newReadPipe(t)
  28. parent, err := NewPollQueueSet()
  29. require.NoError(t, err)
  30. require.NoError(t, parent.Add(pipe1))
  31. require.NoError(t, parent.Add(pipe2))
  32. t.Cleanup(func() {
  33. _ = unix.Close(pipe1)
  34. _ = unix.Close(pipe2)
  35. })
  36. readers := parent.Queues()
  37. errs := make([]error, len(readers))
  38. var wg sync.WaitGroup
  39. for i, r := range readers {
  40. wg.Add(1)
  41. go func(i int, r Queue) {
  42. defer wg.Done()
  43. _, errs[i] = r.Read()
  44. }(i, r)
  45. }
  46. time.Sleep(50 * time.Millisecond)
  47. if err := parent.Close(); err != nil {
  48. t.Fatalf("Close: %v", err)
  49. }
  50. done := make(chan struct{})
  51. go func() { wg.Wait(); close(done) }()
  52. select {
  53. case <-done:
  54. case <-time.After(2 * time.Second):
  55. t.Fatal("readers did not wake")
  56. }
  57. for i, err := range errs {
  58. if !errors.Is(err, os.ErrClosed) {
  59. t.Errorf("reader %d: expected os.ErrClosed, got %v", i, err)
  60. }
  61. }
  62. }
  63. // TestPoll_ConcurrentWrite_NoRace hammers a single Poll queue from two writer
  64. // goroutines while a reader drains the other end of the pipe. The writers
  65. // overflow the pipe buffer, so both repeatedly park in blockOnWrite at the same
  66. // time — the exact scenario that raced on the old shared writePoll member
  67. // array. Run under -race; a shared-array regression trips the detector here.
  68. func TestPoll_ConcurrentWrite_NoRace(t *testing.T) {
  69. var fds [2]int
  70. require.NoError(t, unix.Pipe2(fds[:], unix.O_CLOEXEC))
  71. readFd, writeFd := fds[0], fds[1]
  72. shutdownFd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
  73. require.NoError(t, err)
  74. t.Cleanup(func() { _ = unix.Close(shutdownFd) })
  75. p, err := newPoll(writeFd, shutdownFd)
  76. require.NoError(t, err)
  77. const writers = 2
  78. const perWriter = 4000
  79. payload := make([]byte, 100)
  80. total := writers * perWriter * len(payload)
  81. // Reader: drain the read end (blocking) until every writer's bytes are
  82. // consumed, so the writers keep making progress rather than wedging on a
  83. // permanently full pipe.
  84. readDone := make(chan struct{})
  85. go func() {
  86. defer close(readDone)
  87. buf := make([]byte, 4096)
  88. got := 0
  89. for got < total {
  90. n, rerr := unix.Read(readFd, buf)
  91. got += n
  92. if rerr != nil {
  93. if rerr == unix.EINTR {
  94. continue
  95. }
  96. return
  97. }
  98. if n == 0 { // EOF
  99. return
  100. }
  101. }
  102. }()
  103. var wg sync.WaitGroup
  104. for w := 0; w < writers; w++ {
  105. wg.Add(1)
  106. go func() {
  107. defer wg.Done()
  108. for i := 0; i < perWriter; i++ {
  109. if _, werr := p.Write(payload); werr != nil {
  110. t.Errorf("write: %v", werr)
  111. return
  112. }
  113. }
  114. }()
  115. }
  116. wg.Wait()
  117. select {
  118. case <-readDone:
  119. case <-time.After(10 * time.Second):
  120. t.Fatal("reader did not drain")
  121. }
  122. require.NoError(t, p.Close())
  123. _ = unix.Close(readFd)
  124. }
  125. // TestPoll_NewPoll_DoesNotCloseFdOnFailure pins the ownership rule: when
  126. // newPoll fails, it must leave fd open so the caller (pollQueueSet.Add's
  127. // callers in tun_linux.go) is the sole closer. If newPoll also closed fd,
  128. // the poll path would double-close on Add error. We force the failure with
  129. // an O_PATH descriptor: fcntl(F_SETFL) — which SetNonblock performs — is not
  130. // permitted on O_PATH fds and fails with EBADF, while the fd itself stays
  131. // open so we can observe that newPoll left it alone.
  132. func TestPoll_NewPoll_DoesNotCloseFdOnFailure(t *testing.T) {
  133. fd, err := unix.Open("/", unix.O_PATH|unix.O_CLOEXEC, 0)
  134. require.NoError(t, err)
  135. t.Cleanup(func() { _ = unix.Close(fd) })
  136. p, err := newPoll(fd, 1)
  137. require.Error(t, err, "SetNonblock on an O_PATH fd should fail")
  138. require.Nil(t, p)
  139. // If newPoll had closed fd, F_GETFD would report it closed. It staying
  140. // open proves newPoll left the fd for the caller to close exactly once.
  141. require.True(t, fdOpen(t, fd), "newPoll must not close fd on failure; caller is the sole closer")
  142. }
  143. func TestPoll_Close_Idempotent(t *testing.T) {
  144. tf, err := newPoll(newReadPipe(t), 1)
  145. require.NoError(t, err)
  146. if err := tf.Close(); err != nil {
  147. t.Fatalf("first Close: %v", err)
  148. }
  149. if err := tf.Close(); err != nil {
  150. t.Fatalf("second Close should be a no-op, got %v", err)
  151. }
  152. }
  153. // fdOpen reports whether fd currently refers to an open file description.
  154. // A closed (or never-allocated) fd makes F_GETFD fail with EBADF.
  155. func fdOpen(t *testing.T, fd int) bool {
  156. t.Helper()
  157. _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)
  158. if err == nil {
  159. return true
  160. }
  161. if errors.Is(err, unix.EBADF) {
  162. return false
  163. }
  164. t.Fatalf("unexpected fcntl(F_GETFD) error on fd %d: %v", fd, err)
  165. return false
  166. }
  167. // TestPollQueueSet_Close_ClosesShutdownFd is the regression test for the
  168. // leaked shutdown eventfd: the container that owns shutdownFd must close it in
  169. // Close, and a second Close must be a safe no-op.
  170. func TestPollQueueSet_Close_ClosesShutdownFd(t *testing.T) {
  171. qs, err := NewPollQueueSet()
  172. require.NoError(t, err)
  173. c, ok := qs.(*pollQueueSet)
  174. require.True(t, ok)
  175. require.NoError(t, qs.Add(newReadPipe(t)))
  176. shutdownFd := c.shutdownFd
  177. require.True(t, fdOpen(t, shutdownFd), "shutdown eventfd should be open before Close")
  178. require.NoError(t, qs.Close())
  179. require.False(t, fdOpen(t, shutdownFd), "shutdown eventfd should be closed after Close")
  180. // Second Close must not touch fds (shutdownFd is now -1) and must return nil.
  181. require.NoError(t, qs.Close())
  182. }
  183. // TestOffloadQueueSet_Close_ClosesShutdownFd mirrors the poll regression test
  184. // for the GSO/offload queueset.
  185. func TestOffloadQueueSet_Close_ClosesShutdownFd(t *testing.T) {
  186. qs, err := NewOffloadQueueSet(false, slog.New(slog.DiscardHandler))
  187. require.NoError(t, err)
  188. c, ok := qs.(*offloadQueueSet)
  189. require.True(t, ok)
  190. require.NoError(t, qs.Add(newReadPipe(t)))
  191. shutdownFd := c.shutdownFd
  192. require.True(t, fdOpen(t, shutdownFd), "shutdown eventfd should be open before Close")
  193. require.NoError(t, qs.Close())
  194. require.False(t, fdOpen(t, shutdownFd), "shutdown eventfd should be closed after Close")
  195. // Second Close must not touch fds (shutdownFd is now -1) and must return nil.
  196. require.NoError(t, qs.Close())
  197. }