diff --git a/overlay/tio/queueset_poll_linux.go b/overlay/tio/queueset_poll_linux.go index ab967df4..016baf6d 100644 --- a/overlay/tio/queueset_poll_linux.go +++ b/overlay/tio/queueset_poll_linux.go @@ -48,11 +48,15 @@ func (c *pollQueueSet) Add(fd int) error { func (c *pollQueueSet) wakeForShutdown() error { var buf [8]byte binary.NativeEndian.PutUint64(buf[:], 1) - _, err := unix.Write(int(c.shutdownFd), buf[:]) + _, err := unix.Write(c.shutdownFd, buf[:]) return err } func (c *pollQueueSet) Close() error { + if c.shutdownFd < 0 { + return nil + } + errs := []error{} if err := c.wakeForShutdown(); err != nil { @@ -65,5 +69,12 @@ func (c *pollQueueSet) Close() error { } } + // All Polls reference shutdownFd in their pollfd arrays, so close it + // only after every Poll.Close has returned. + if err := unix.Close(c.shutdownFd); err != nil { + errs = append(errs, err) + } + c.shutdownFd = -1 + return errors.Join(errs...) } diff --git a/overlay/tio/tun_file_linux_test.go b/overlay/tio/tun_file_linux_test.go index f92f58ec..41fe32e5 100644 --- a/overlay/tio/tun_file_linux_test.go +++ b/overlay/tio/tun_file_linux_test.go @@ -80,3 +80,24 @@ func TestPoll_Close_Idempotent(t *testing.T) { t.Fatalf("second Close should be a no-op, got %v", err) } } + +func TestPollQueueSet_Close_ClosesEventfd(t *testing.T) { + qs, err := NewPollQueueSet() + require.NoError(t, err) + require.NoError(t, qs.Add(newReadPipe(t))) + + fd := qs.(*pollQueueSet).shutdownFd + require.NoError(t, qs.Close()) + + // Closing the eventfd again should fail with EBADF, proving Close + // actually released it. + if err := unix.Close(fd); err == nil { + t.Fatalf("eventfd %d still open after QueueSet.Close", fd) + } + + // Second Close must be a no-op (and must not double-close the eventfd + // in case the kernel handed it out to another caller in the meantime). + if err := qs.Close(); err != nil { + t.Fatalf("second Close: %v", err) + } +}