From d5a2d3f1b81d232c346882bff234804c174ef13a Mon Sep 17 00:00:00 2001 From: Leonard Cohnen Date: Thu, 12 Feb 2026 10:33:32 +0000 Subject: [PATCH] Use poll instead of pselect6 pselect6 is limited to FD_SETSIZE, which is 1024 in most cases. When a application holds many fds, this can be reached easily, resulting in a panic when the fd is added to the fd set. Instead of pselect6 use poll, which doesn't have such a limitation. --- socket.go | 15 ++++++------- socket_test.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) create mode 100644 socket_test.go diff --git a/socket.go b/socket.go index 27f9220..c6164b3 100644 --- a/socket.go +++ b/socket.go @@ -8,7 +8,7 @@ import ( ) // isReadReady reports whether the netlink connection is ready for reading. -// It uses pselect6 with a zero timeout on the underlying raw connection. +// It uses poll(2) with a zero timeout on the underlying raw connection. // This allows for an efficient check of socket readiness without blocking. // If the Conn was created with a TestDial function, it assumes readiness. func (cc *Conn) isReadReady(conn *netlink.Conn) (bool, error) { @@ -24,13 +24,12 @@ func (cc *Conn) isReadReady(conn *netlink.Conn) (bool, error) { var n int var opErr error err = rawConn.Control(func(fd uintptr) { - var readfds unix.FdSet - readfds.Zero() - readfds.Set(int(fd)) - - ts := &unix.Timespec{} // zero timeout: immediate return + fds := []unix.PollFd{{ + Fd: int32(fd), + Events: unix.POLLIN, + }} for { - n, opErr = unix.Pselect(int(fd)+1, &readfds, nil, nil, ts, nil) + n, opErr = unix.Poll(fds, 0) // 0 timeout: immediate return if opErr != unix.EINTR { break } @@ -41,7 +40,7 @@ func (cc *Conn) isReadReady(conn *netlink.Conn) (bool, error) { } if opErr != nil { - return false, fmt.Errorf("pselect6: %w", opErr) + return false, fmt.Errorf("poll: %w", opErr) } return n > 0, nil diff --git a/socket_test.go b/socket_test.go new file mode 100644 index 0000000..0e9eba1 --- /dev/null +++ b/socket_test.go @@ -0,0 +1,58 @@ +package nftables_test + +import ( + "os" + "testing" + + "github.com/google/nftables" + "github.com/google/nftables/internal/nftest" + "golang.org/x/sys/unix" +) + +// TestIsReadReadyHighFD verifies that the nftables library works correctly when +// the underlying netlink socket gets an fd >= FD_SETSIZE (1024). The old +// pselect-based implementation would panic in this scenario because +// unix.FdSet.Set panics for fd >= FD_SETSIZE. The current poll-based +// implementation has no such limit. +func TestIsReadReadyHighFD(t *testing.T) { + c, newNS := nftest.OpenSystemConn(t, *enableSysTests) + defer nftest.CleanupSystemConn(t, newNS) + + // Exhaust low file descriptors so the next socket allocation gets fd >= FD_SETSIZE. + var fillers []*os.File + defer func() { + for _, f := range fillers { + f.Close() + } + }() + + for { + f, err := os.Open("/dev/null") + if err != nil { + t.Fatalf("os.Open(/dev/null): %v", err) + } + fillers = append(fillers, f) + if int(f.Fd()) >= unix.FD_SETSIZE { + break + } + } + t.Logf("exhausted fds up to %d", fillers[len(fillers)-1].Fd()) + + // By default, a transient socket is created for each request. The socket + // will get an fd >= FD_SETSIZE. With the old pselect code this would panic; + // with poll it must work. + + // Add a command and flush it to trigger the isReadReady code path. + c.AddTable(&nftables.Table{Name: "test_high_fd", Family: nftables.TableFamilyIPv4}) + func() { + // turn the potential panic into a test failure. + defer func() { + if r := recover(); r != nil { + t.Fatalf("isReadReady panicked for fd >= %d: %v", unix.FD_SETSIZE, r) + } + }() + if err := c.Flush(); err != nil { + t.Fatalf("Flush() failed: %v", err) + } + }() +}