From ff8774b5ce3f6b37e5152d0dc06ae46b7a36d1da Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 28 May 2026 10:43:37 +0300 Subject: close_range: honor last bound and CLOSE_RANGE_CLOEXEC flag close_range was captured as a single-fd fd_event carrying only first, so the runtime evicted every tracked fd >= first, ignoring the last upper bound and the flags. Bounded calls wrongly dropped still-open higher fds, and CLOSE_RANGE_CLOEXEC (which keeps fds open) was treated as a full close. Reclassify close_range to the two_fd_event kind, mapping fd_a/fd_b/extra to first/last/flags. The runtime now closes only the inclusive [first, last] range (a negative last from ~0U means unbounded) and skips eviction when CLOSE_RANGE_CLOEXEC is set or the syscall fails. Co-Authored-By: Claude Opus 4.7 --- cmd/ioworkload/scenario_close.go | 46 ++++++++++++++++++++++++++++++++++++++++ cmd/ioworkload/scenarios.go | 1 + 2 files changed, 47 insertions(+) (limited to 'cmd') diff --git a/cmd/ioworkload/scenario_close.go b/cmd/ioworkload/scenario_close.go index fc5044c..1be5376 100644 --- a/cmd/ioworkload/scenario_close.go +++ b/cmd/ioworkload/scenario_close.go @@ -64,6 +64,52 @@ func closeRange() error { return nil } +// closeRangeBounded opens a contiguous block of low fds plus one higher fd, +// then closes only the low block via close_range(first, last, 0) where last is +// strictly below the higher fd. It writes to the higher fd afterwards to prove +// it stayed open. This exercises close_range's upper-bound handling end to end: +// ior must keep the higher fd tracked rather than evicting everything >= first. +func closeRangeBounded() error { + dir, cleanup, err := makeTempDir("close-range-bounded") + if err != nil { + return err + } + defer cleanup() + + var lowFds []int + for i := range 3 { + path := filepath.Join(dir, fmt.Sprintf("closerangelow-%d.txt", i)) + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open low %d: %w", i, err) + } + lowFds = append(lowFds, fd) + } + + highPath := filepath.Join(dir, "closerangehigh.txt") + highFd, err := syscall.Open(highPath, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open high: %w", err) + } + defer syscall.Close(highFd) + + if highFd <= lowFds[len(lowFds)-1] { + return fmt.Errorf("high fd %d not above low fds %v", highFd, lowFds) + } + + first := uintptr(lowFds[0]) + last := uintptr(lowFds[len(lowFds)-1]) + if _, _, errno := syscall.Syscall(sysCloseRange, first, last, 0); errno != 0 { + return fmt.Errorf("close_range: %w", errno) + } + + // highFd is above last, so it must still be open and usable. + if _, err := syscall.Write(highFd, []byte("still-open")); err != nil { + return fmt.Errorf("write high fd: %w", err) + } + return nil +} + // closeInvalidFd attempts to close a very high fd number that is not open. // The close fails with EBADF, but ior should capture the enter_close tracepoint // because arguments are read on syscall entry before the kernel returns an error. diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go index ba444ef..534c9b4 100644 --- a/cmd/ioworkload/scenarios.go +++ b/cmd/ioworkload/scenarios.go @@ -43,6 +43,7 @@ var scenarios = map[string]func() error{ "family-mixed": familyMixed, "close-basic": closeBasic, "close-range": closeRange, + "close-range-bounded": closeRangeBounded, "close-invalid-fd": closeInvalidFd, "close-double-close": closeDoubleClose, "close-range-empty": closeRangeEmpty, -- cgit v1.2.3