Skip to content

Commit dbcfa18

Browse files
committed
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.
1 parent 1db35da commit dbcfa18

2 files changed

Lines changed: 68 additions & 8 deletions

File tree

socket.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99

1010
// isReadReady reports whether the netlink connection is ready for reading.
11-
// It uses pselect6 with a zero timeout on the underlying raw connection.
11+
// It uses poll(2) with a zero timeout on the underlying raw connection.
1212
// This allows for an efficient check of socket readiness without blocking.
1313
// If the Conn was created with a TestDial function, it assumes readiness.
1414
func (cc *Conn) isReadReady(conn *netlink.Conn) (bool, error) {
@@ -24,13 +24,12 @@ func (cc *Conn) isReadReady(conn *netlink.Conn) (bool, error) {
2424
var n int
2525
var opErr error
2626
err = rawConn.Control(func(fd uintptr) {
27-
var readfds unix.FdSet
28-
readfds.Zero()
29-
readfds.Set(int(fd))
30-
31-
ts := &unix.Timespec{} // zero timeout: immediate return
27+
fds := []unix.PollFd{{
28+
Fd: int32(fd),
29+
Events: unix.POLLIN,
30+
}}
3231
for {
33-
n, opErr = unix.Pselect(int(fd)+1, &readfds, nil, nil, ts, nil)
32+
n, opErr = unix.Poll(fds, 0) // 0 timeout: immediate return
3433
if opErr != unix.EINTR {
3534
break
3635
}
@@ -41,7 +40,7 @@ func (cc *Conn) isReadReady(conn *netlink.Conn) (bool, error) {
4140
}
4241

4342
if opErr != nil {
44-
return false, fmt.Errorf("pselect6: %w", opErr)
43+
return false, fmt.Errorf("poll: %w", opErr)
4544
}
4645

4746
return n > 0, nil

socket_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package nftables_test
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/google/nftables"
8+
"github.com/google/nftables/internal/nftest"
9+
"golang.org/x/sys/unix"
10+
)
11+
12+
// TestIsReadReadyHighFD verifies that the nftables library works correctly when
13+
// the underlying netlink socket gets an fd >= FD_SETSIZE (1024). The old
14+
// pselect-based implementation would panic in this scenario because
15+
// unix.FdSet.Set panics for fd >= FD_SETSIZE. The current poll-based
16+
// implementation has no such limit.
17+
func TestIsReadReadyHighFD(t *testing.T) {
18+
_, newNS := nftest.OpenSystemConn(t, *enableSysTests)
19+
20+
// Exhaust low file descriptors so the next socket allocation gets fd >= FD_SETSIZE.
21+
var fillers []*os.File
22+
defer func() {
23+
for _, f := range fillers {
24+
f.Close()
25+
}
26+
}()
27+
28+
for {
29+
f, err := os.Open("/dev/null")
30+
if err != nil {
31+
t.Fatalf("os.Open(/dev/null): %v", err)
32+
}
33+
fillers = append(fillers, f)
34+
if int(f.Fd()) >= unix.FD_SETSIZE {
35+
break
36+
}
37+
}
38+
t.Logf("exhausted fds up to %d", fillers[len(fillers)-1].Fd())
39+
40+
// Now use the actual library. The netlink socket it opens will get an fd
41+
// >= FD_SETSIZE. With the old pselect code this would panic; with poll it
42+
// must work.
43+
c, err := nftables.New(nftables.WithNetNSFd(int(newNS)))
44+
if err != nil {
45+
t.Fatalf("New() failed: %v", err)
46+
}
47+
48+
// Add a command and flush it to trigger the isReadReady code path.
49+
c.AddTable(&nftables.Table{Name: "test_high_fd", Family: nftables.TableFamilyIPv4})
50+
func() {
51+
// turn the potential panic into a test failure.
52+
defer func() {
53+
if r := recover(); r != nil {
54+
t.Fatalf("isReadReady panicked for fd >= %d: %v", unix.FD_SETSIZE, r)
55+
}
56+
}()
57+
if err := c.Flush(); err != nil {
58+
t.Fatalf("Flush() failed: %v", err)
59+
}
60+
}()
61+
}

0 commit comments

Comments
 (0)