Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions socket.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
}
Expand All @@ -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
Expand Down
58 changes: 58 additions & 0 deletions socket_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}()
}