diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-23 20:06:30 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-23 20:06:30 +0300 |
| commit | a2151af07c58efa3bf541e0273702ae991eda799 (patch) | |
| tree | bce91331822961365c19aa78337bd2e7542447b8 /cmd | |
| parent | 4541ee21203f7c494530f142a6387244ac3a6c62 (diff) | |
1c guard ioworkload select fd_set against high fd values
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/ioworkload/scenario_polling.go | 24 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_polling_test.go | 31 |
2 files changed, 51 insertions, 4 deletions
diff --git a/cmd/ioworkload/scenario_polling.go b/cmd/ioworkload/scenario_polling.go index e41ec9a..f840e7c 100644 --- a/cmd/ioworkload/scenario_polling.go +++ b/cmd/ioworkload/scenario_polling.go @@ -198,17 +198,33 @@ func callPpoll(pipefd [2]int) (int, error) { return int(r1), nil } +// fdSetBits is the maximum number of file descriptors an fdSet can represent. +// This matches the Linux FD_SETSIZE constant (1024). +const fdSetBits = 16 * 64 + +// errFDOutOfRange is returned when a file descriptor is too large for the +// fixed-size fd_set used by select/pselect6 (>= 1024). +var errFDOutOfRange = fmt.Errorf("fd >= %d: too large for select fd_set", fdSetBits) + type fdSet [16]uint64 -func (s *fdSet) set(fd int) { +// set marks fd in the bitset. It returns errFDOutOfRange when fd falls +// outside the fixed 1024-bit array, preventing an index-out-of-bounds panic. +func (s *fdSet) set(fd int) error { + if fd < 0 || fd >= fdSetBits { + return errFDOutOfRange + } idx := fd / 64 bit := uint(fd % 64) s[idx] |= 1 << bit + return nil } func callSelect(pipefd [2]int) (int, error) { var readSet fdSet - readSet.set(pipefd[0]) + if err := readSet.set(pipefd[0]); err != nil { + return 0, fmt.Errorf("select: %w", err) + } timeout := syscall.Timeval{Sec: 0, Usec: 100000} r1, _, errno := syscall.RawSyscall6( syscall.SYS_SELECT, @@ -229,7 +245,9 @@ func callSelect(pipefd [2]int) (int, error) { func callPselect6(pipefd [2]int) (int, error) { var readSet fdSet - readSet.set(pipefd[0]) + if err := readSet.set(pipefd[0]); err != nil { + return 0, fmt.Errorf("pselect6: %w", err) + } timeout := unix.Timespec{Sec: 0, Nsec: 100 * 1_000_000} r1, _, errno := syscall.RawSyscall6( unix.SYS_PSELECT6, diff --git a/cmd/ioworkload/scenario_polling_test.go b/cmd/ioworkload/scenario_polling_test.go index 304602c..ebb04d2 100644 --- a/cmd/ioworkload/scenario_polling_test.go +++ b/cmd/ioworkload/scenario_polling_test.go @@ -37,12 +37,41 @@ func TestFdSetSetMarksBit(t *testing.T) { t.Parallel() var set fdSet - set.set(65) + if err := set.set(65); err != nil { + t.Fatalf("set(65) unexpected error: %v", err) + } if set[1]&(1<<1) == 0 { t.Fatalf("fd bit was not set: %#v", set) } } +func TestFdSetSetRejectsOutOfRange(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + fd int + }{ + {name: "exactly_1024", fd: 1024}, + {name: "large_fd", fd: 4096}, + {name: "negative_fd", fd: -1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var set fdSet + err := set.set(tt.fd) + if err == nil { + t.Fatalf("set(%d) should return error, got nil", tt.fd) + } + if !errors.Is(err, errFDOutOfRange) { + t.Fatalf("set(%d) error = %v, want %v", tt.fd, err, errFDOutOfRange) + } + }) + } +} + func TestClassicPollingSyscallsReturnReadyCount(t *testing.T) { var pipefd [2]int if err := syscall.Pipe(pipefd[:]); err != nil { |
