detached bda62c7

fix(fdwait): check readiness before parking and fail on lost watches

Poll checks fds once before registering them, so an expired deadline
never skips the readiness check (Poll(fds, 0) now matches poll(2)) and
ready fds cost no epoll registration. A descriptor listed twice is
watched once. A registration that cannot be removed marks the poller
broken instead of leaving a stale readable watch. Errors name the fd.

syncWaiter drops its dead fixture branch and duplicate pipe fd; tests
close the waiter's epoll fd. The recycled-fd test, which could skip and
did not guard the regression, is replaced by edge-case tests.
commit
bda62c7a29ccf19892571d808740cd3e51af7e84
author
brice · · 2026-10-14 02:42 UTC
committer
brice · · 2026-10-14 04:35 UTC
parent
cef9892
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +208 -94
A internal/adapters/fdwait/edges_test.go +129 -0
@@ -0,0 +1,129 @@
1+package fdwait
2+
3+import (
4+ "testing"
5+
6+ "golang.org/x/sys/unix"
7+)
8+
9+func eventfd(t *testing.T) int {
10+ t.Helper()
11+ fd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
12+ if err != nil {
13+ t.Fatal(err)
14+ }
15+ t.Cleanup(func() { unix.Close(fd) })
16+ return fd
17+}
18+
19+func signal(t *testing.T, fd int) {
20+ t.Helper()
21+ if _, err := unix.Write(fd, []byte{1, 0, 0, 0, 0, 0, 0, 0}); err != nil {
22+ t.Fatal(err)
23+ }
24+}
25+
26+// assertNoWatches fails if Poll left a registration behind, by making every
27+// test descriptor readable and checking epoll reports nothing.
28+func assertNoWatches(t *testing.T, p *Poller, fds ...int) {
29+ t.Helper()
30+ for _, fd := range fds {
31+ signal(t, fd)
32+ }
33+ if err := p.raw.Control(func(ep uintptr) {
34+ var events [4]unix.EpollEvent
35+ if n, err := unix.EpollWait(int(ep), events[:], 0); n != 0 || err != nil {
36+ t.Errorf("registrations left after Poll returned: %d %v", n, err)
37+ }
38+ }); err != nil {
39+ t.Fatal(err)
40+ }
41+}
42+
43+// Poll(fds, 0) reports existing readiness like poll(2): an expired netpoll
44+// deadline never runs the readiness check.
45+func TestPollZeroTimeoutReportsReady(t *testing.T) {
46+ p, err := New()
47+ if err != nil {
48+ t.Fatal(err)
49+ }
50+ defer p.Close()
51+ fd := eventfd(t)
52+ fds := []unix.PollFd{{Fd: -1}, {Fd: int32(fd), Events: unix.POLLIN}}
53+ if n, err := p.Poll(fds, 0); n != 0 || err != nil {
54+ t.Fatalf("idle: %d %v", n, err)
55+ }
56+ signal(t, fd)
57+ if n, err := p.Poll(fds, 0); n != 1 || err != nil || fds[1].Revents&unix.POLLIN == 0 {
58+ t.Fatalf("ready: %d %v %+v", n, err, fds)
59+ }
60+}
61+
62+// A descriptor listed twice is watched once and removed once.
63+func TestPollDuplicateDescriptor(t *testing.T) {
64+ p, err := New()
65+ if err != nil {
66+ t.Fatal(err)
67+ }
68+ defer p.Close()
69+ fd := eventfd(t)
70+ fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}, {Fd: int32(fd), Events: unix.POLLIN}}
71+ if n, err := p.Poll(fds, 1); n != 0 || err != nil {
72+ t.Fatalf("duplicate: %d %v", n, err)
73+ }
74+ assertNoWatches(t, p, fd)
75+}
76+
77+// A failed registration removes the ones already added in the same call.
78+func TestPollFailedWatchRemovesAdded(t *testing.T) {
79+ p, err := New()
80+ if err != nil {
81+ t.Fatal(err)
82+ }
83+ defer p.Close()
84+ fd := eventfd(t)
85+ var self int32
86+ if err := p.raw.Control(func(ep uintptr) { self = int32(ep) }); err != nil {
87+ t.Fatal(err)
88+ }
89+ // epoll refuses to watch itself (EINVAL) and is not ready.
90+ fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}, {Fd: self, Events: unix.POLLIN}}
91+ if _, err := p.Poll(fds, 1); err == nil {
92+ t.Fatal("watching the epoll fd itself succeeded")
93+ }
94+ assertNoWatches(t, p, fd)
95+}
96+
97+// A registration whose fd was closed while another reference keeps its file
98+// open cannot be removed: Poll fails from then on instead of spinning.
99+func TestPollFailsAfterLostRegistration(t *testing.T) {
100+ p, err := New()
101+ if err != nil {
102+ t.Fatal(err)
103+ }
104+ defer p.Close()
105+ fd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
106+ if err != nil {
107+ t.Fatal(err)
108+ }
109+ dup, err := unix.Dup(fd)
110+ if err != nil {
111+ unix.Close(fd)
112+ t.Fatal(err)
113+ }
114+ defer unix.Close(dup)
115+ if err := p.raw.Control(func(ep uintptr) {
116+ ev := unix.EpollEvent{Events: unix.EPOLLIN, Fd: int32(fd)}
117+ if err := unix.EpollCtl(int(ep), unix.EPOLL_CTL_ADD, fd, &ev); err != nil {
118+ t.Fatal(err)
119+ }
120+ }); err != nil {
121+ t.Fatal(err)
122+ }
123+ unix.Close(fd) // what a caller closing fd during a wait does
124+ p.unwatch([]unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}})
125+ other := eventfd(t)
126+ if _, err := p.Poll([]unix.PollFd{{Fd: int32(other), Events: unix.POLLIN}}, 1); err == nil {
127+ t.Fatal("Poll succeeded with a registration it could not remove")
128+ }
129+}
@@ -0,0 +1,129 @@
1+package fdwait
2+
3+import (
4+ "testing"
5+
6+ "golang.org/x/sys/unix"
7+)
8+
9+func eventfd(t *testing.T) int {
10+ t.Helper()
11+ fd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
12+ if err != nil {
13+ t.Fatal(err)
14+ }
15+ t.Cleanup(func() { unix.Close(fd) })
16+ return fd
17+}
18+
19+func signal(t *testing.T, fd int) {
20+ t.Helper()
21+ if _, err := unix.Write(fd, []byte{1, 0, 0, 0, 0, 0, 0, 0}); err != nil {
22+ t.Fatal(err)
23+ }
24+}
25+
26+// assertNoWatches fails if Poll left a registration behind, by making every
27+// test descriptor readable and checking epoll reports nothing.
28+func assertNoWatches(t *testing.T, p *Poller, fds ...int) {
29+ t.Helper()
30+ for _, fd := range fds {
31+ signal(t, fd)
32+ }
33+ if err := p.raw.Control(func(ep uintptr) {
34+ var events [4]unix.EpollEvent
35+ if n, err := unix.EpollWait(int(ep), events[:], 0); n != 0 || err != nil {
36+ t.Errorf("registrations left after Poll returned: %d %v", n, err)
37+ }
38+ }); err != nil {
39+ t.Fatal(err)
40+ }
41+}
42+
43+// Poll(fds, 0) reports existing readiness like poll(2): an expired netpoll
44+// deadline never runs the readiness check.
45+func TestPollZeroTimeoutReportsReady(t *testing.T) {
46+ p, err := New()
47+ if err != nil {
48+ t.Fatal(err)
49+ }
50+ defer p.Close()
51+ fd := eventfd(t)
52+ fds := []unix.PollFd{{Fd: -1}, {Fd: int32(fd), Events: unix.POLLIN}}
53+ if n, err := p.Poll(fds, 0); n != 0 || err != nil {
54+ t.Fatalf("idle: %d %v", n, err)
55+ }
56+ signal(t, fd)
57+ if n, err := p.Poll(fds, 0); n != 1 || err != nil || fds[1].Revents&unix.POLLIN == 0 {
58+ t.Fatalf("ready: %d %v %+v", n, err, fds)
59+ }
60+}
61+
62+// A descriptor listed twice is watched once and removed once.
63+func TestPollDuplicateDescriptor(t *testing.T) {
64+ p, err := New()
65+ if err != nil {
66+ t.Fatal(err)
67+ }
68+ defer p.Close()
69+ fd := eventfd(t)
70+ fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}, {Fd: int32(fd), Events: unix.POLLIN}}
71+ if n, err := p.Poll(fds, 1); n != 0 || err != nil {
72+ t.Fatalf("duplicate: %d %v", n, err)
73+ }
74+ assertNoWatches(t, p, fd)
75+}
76+
77+// A failed registration removes the ones already added in the same call.
78+func TestPollFailedWatchRemovesAdded(t *testing.T) {
79+ p, err := New()
80+ if err != nil {
81+ t.Fatal(err)
82+ }
83+ defer p.Close()
84+ fd := eventfd(t)
85+ var self int32
86+ if err := p.raw.Control(func(ep uintptr) { self = int32(ep) }); err != nil {
87+ t.Fatal(err)
88+ }
89+ // epoll refuses to watch itself (EINVAL) and is not ready.
90+ fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}, {Fd: self, Events: unix.POLLIN}}
91+ if _, err := p.Poll(fds, 1); err == nil {
92+ t.Fatal("watching the epoll fd itself succeeded")
93+ }
94+ assertNoWatches(t, p, fd)
95+}
96+
97+// A registration whose fd was closed while another reference keeps its file
98+// open cannot be removed: Poll fails from then on instead of spinning.
99+func TestPollFailsAfterLostRegistration(t *testing.T) {
100+ p, err := New()
101+ if err != nil {
102+ t.Fatal(err)
103+ }
104+ defer p.Close()
105+ fd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
106+ if err != nil {
107+ t.Fatal(err)
108+ }
109+ dup, err := unix.Dup(fd)
110+ if err != nil {
111+ unix.Close(fd)
112+ t.Fatal(err)
113+ }
114+ defer unix.Close(dup)
115+ if err := p.raw.Control(func(ep uintptr) {
116+ ev := unix.EpollEvent{Events: unix.EPOLLIN, Fd: int32(fd)}
117+ if err := unix.EpollCtl(int(ep), unix.EPOLL_CTL_ADD, fd, &ev); err != nil {
118+ t.Fatal(err)
119+ }
120+ }); err != nil {
121+ t.Fatal(err)
122+ }
123+ unix.Close(fd) // what a caller closing fd during a wait does
124+ p.unwatch([]unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}})
125+ other := eventfd(t)
126+ if _, err := p.Poll([]unix.PollFd{{Fd: int32(other), Events: unix.POLLIN}}, 1); err == nil {
127+ t.Fatal("Poll succeeded with a registration it could not remove")
128+ }
129+}
M internal/adapters/fdwait/poller.go +73 -27
@@ -3,74 +3,89 @@ package fdwait
33
44 import (
55 "errors"
6+ "fmt"
67 "os"
8+ "slices"
79 "syscall"
810 "time"
911
1012 "golang.org/x/sys/unix"
1113 )
1214
13-// Poller owns an epoll descriptor and is used by one goroutine.
14-// Watched descriptors remain owned by the caller.
15+// Poller owns an epoll descriptor and is used by one goroutine; Close may
16+// also run from another goroutine to end a wait. Watched descriptors remain
17+// owned by the caller, who must not close them while a Poll waits.
1518 type Poller struct {
1619 file *os.File
1720 raw syscall.RawConn
21+ // broken is set when a registration could not be removed: it may keep
22+ // the epoll descriptor readable forever, so every later Poll fails
23+ // instead of spinning.
24+ broken error
1825 }
1926
2027 func New() (*Poller, error) {
2128 fd, err := unix.EpollCreate1(unix.EPOLL_CLOEXEC)
2229 if err != nil {
23- return nil, err
30+ return nil, fmt.Errorf("fdwait epoll create: %w", err)
2431 }
2532 if err = unix.SetNonblock(fd, true); err != nil {
2633 unix.Close(fd)
27- return nil, err
34+ return nil, fmt.Errorf("fdwait epoll nonblock: %w", err)
2835 }
2936 f := os.NewFile(uintptr(fd), "fdwait")
3037 raw, err := f.SyscallConn()
3138 if err != nil {
3239 f.Close()
33- return nil, err
40+ return nil, fmt.Errorf("fdwait epoll conn: %w", err)
3441 }
3542 return &Poller{file: f, raw: raw}, nil
3643 }
44+
3745 func (p *Poller) Close() error { return p.file.Close() }
3846
39-// Poll waits for readable/error events. It never consumes caller events.
40-// Each call explicitly removes its epoll registrations before the caller can
41-// close or recycle watched fds. Relying on close to remove them is unsafe:
42-// another fd or a kernel fence may retain the open file description.
47+// Poll waits like poll(2) for readable/error events on fds and fills their
48+// Revents. It never consumes caller events. Negative fds are ignored and a
49+// descriptor listed twice is watched once. timeout is in milliseconds; a
50+// negative one waits until an event or Close.
51+//
52+// Each call explicitly removes its epoll registrations before returning.
53+// Relying on close to remove them is unsafe: another fd or a kernel fence
54+// may retain the open file description and keep the epoll fd readable.
55+// When nothing is ready yet, a descriptor epoll cannot watch (EPERM,
56+// EINVAL, ELOOP for epoll nested too deep) fails the call.
4357 func (p *Poller) Poll(fds []unix.PollFd, timeout int) (int, error) {
58+ if p.broken != nil {
59+ return 0, p.broken
60+ }
61+ // Ready descriptors need no registration, and an expired deadline
62+ // would skip the readiness check in RawConn.Read.
63+ if n, err := pollNow(fds); n != 0 || err != nil || timeout == 0 {
64+ return n, err
65+ }
4466 var ctlErr error
4567 added := 0
4668 err := p.raw.Control(func(ep uintptr) {
4769 for _, f := range fds {
48- if f.Fd < 0 {
49- added++
50- continue
51- }
52- ev := unix.EpollEvent{Events: unix.EPOLLIN, Fd: f.Fd}
53- if e := unix.EpollCtl(int(ep), unix.EPOLL_CTL_ADD, int(f.Fd), &ev); e != nil {
54- ctlErr = e
55- return
56- }
57- added++
58- }
59- })
60- defer p.raw.Control(func(ep uintptr) {
61- for _, f := range fds[:added] {
6270 if f.Fd >= 0 {
63- unix.EpollCtl(int(ep), unix.EPOLL_CTL_DEL, int(f.Fd), nil)
71+ ev := unix.EpollEvent{Events: unix.EPOLLIN, Fd: f.Fd}
72+ e := unix.EpollCtl(int(ep), unix.EPOLL_CTL_ADD, int(f.Fd), &ev)
73+ if e != nil && !(errors.Is(e, unix.EEXIST) && listedBefore(fds, added)) {
74+ ctlErr = fmt.Errorf("fdwait watch fd %d: %w", f.Fd, e)
75+ return
76+ }
6477 }
78+ added++
6579 }
6680 })
81+ defer p.unwatch(fds[:added])
6782 if err != nil {
6883 return 0, err
6984 }
7085 if ctlErr != nil {
7186 return 0, ctlErr
7287 }
73- if timeout >= 0 {
88+ if timeout > 0 {
7489 err = p.file.SetReadDeadline(time.Now().Add(time.Duration(timeout) * time.Millisecond))
7590 } else {
7691 err = p.file.SetReadDeadline(time.Time{})
@@ -81,8 +96,8 @@ func (p *Poller) Poll(fds []unix.PollFd, timeout int) (int, error) {
8196 var n int
8297 var pollErr error
8398 err = p.raw.Read(func(_ uintptr) bool {
84- n, pollErr = unix.Poll(fds, 0)
85- return n > 0 || (pollErr != nil && !errors.Is(pollErr, unix.EINTR))
99+ n, pollErr = pollNow(fds)
100+ return n != 0 || pollErr != nil
86101 })
87102 if errors.Is(err, os.ErrDeadlineExceeded) {
88103 return 0, nil
@@ -92,3 +107,34 @@ func (p *Poller) Poll(fds []unix.PollFd, timeout int) (int, error) {
92107 }
93108 return n, pollErr
94109 }
110+
111+// unwatch removes the registrations Poll added for fds. A failure means a
112+// watched fd was closed during the wait: its registration may outlive it.
113+func (p *Poller) unwatch(fds []unix.PollFd) {
114+ p.raw.Control(func(ep uintptr) {
115+ for i, f := range fds {
116+ if f.Fd < 0 || listedBefore(fds, i) {
117+ continue
118+ }
119+ if e := unix.EpollCtl(int(ep), unix.EPOLL_CTL_DEL, int(f.Fd), nil); e != nil && p.broken == nil {
120+ p.broken = fmt.Errorf("fdwait unwatch fd %d (closed during a wait?): %w", f.Fd, e)
121+ }
122+ }
123+ })
124+}
125+
126+// listedBefore reports whether fds[i].Fd also appears earlier in fds.
127+func listedBefore(fds []unix.PollFd, i int) bool {
128+ fd := fds[i].Fd
129+ return slices.ContainsFunc(fds[:i], func(f unix.PollFd) bool { return f.Fd == fd })
130+}
131+
132+// pollNow checks fds without blocking, retrying an interrupted check.
133+func pollNow(fds []unix.PollFd) (int, error) {
134+ for {
135+ n, err := unix.Poll(fds, 0)
136+ if !errors.Is(err, unix.EINTR) {
137+ return n, err
138+ }
139+ }
140+}
@@ -3,74 +3,89 @@ package fdwait
3 3
4 import (4 import (
5 "errors"5 "errors"
6+ "fmt"
6 "os"7 "os"
8+ "slices"
7 "syscall"9 "syscall"
8 "time"10 "time"
9 11
10 "golang.org/x/sys/unix"12 "golang.org/x/sys/unix"
11 )13 )
12 14
13-// Poller owns an epoll descriptor and is used by one goroutine.15+// Poller owns an epoll descriptor and is used by one goroutine; Close may
14-// Watched descriptors remain owned by the caller.16+// also run from another goroutine to end a wait. Watched descriptors remain
17+// owned by the caller, who must not close them while a Poll waits.
15 type Poller struct {18 type Poller struct {
16 file *os.File19 file *os.File
17 raw syscall.RawConn20 raw syscall.RawConn
21+ // broken is set when a registration could not be removed: it may keep
22+ // the epoll descriptor readable forever, so every later Poll fails
23+ // instead of spinning.
24+ broken error
18 }25 }
19 26
20 func New() (*Poller, error) {27 func New() (*Poller, error) {
21 fd, err := unix.EpollCreate1(unix.EPOLL_CLOEXEC)28 fd, err := unix.EpollCreate1(unix.EPOLL_CLOEXEC)
22 if err != nil {29 if err != nil {
23- return nil, err30+ return nil, fmt.Errorf("fdwait epoll create: %w", err)
24 }31 }
25 if err = unix.SetNonblock(fd, true); err != nil {32 if err = unix.SetNonblock(fd, true); err != nil {
26 unix.Close(fd)33 unix.Close(fd)
27- return nil, err34+ return nil, fmt.Errorf("fdwait epoll nonblock: %w", err)
28 }35 }
29 f := os.NewFile(uintptr(fd), "fdwait")36 f := os.NewFile(uintptr(fd), "fdwait")
30 raw, err := f.SyscallConn()37 raw, err := f.SyscallConn()
31 if err != nil {38 if err != nil {
32 f.Close()39 f.Close()
33- return nil, err40+ return nil, fmt.Errorf("fdwait epoll conn: %w", err)
34 }41 }
35 return &Poller{file: f, raw: raw}, nil42 return &Poller{file: f, raw: raw}, nil
36 }43 }
44+
37 func (p *Poller) Close() error { return p.file.Close() }45 func (p *Poller) Close() error { return p.file.Close() }
38 46
39-// Poll waits for readable/error events. It never consumes caller events.47+// Poll waits like poll(2) for readable/error events on fds and fills their
40-// Each call explicitly removes its epoll registrations before the caller can48+// Revents. It never consumes caller events. Negative fds are ignored and a
41-// close or recycle watched fds. Relying on close to remove them is unsafe:49+// descriptor listed twice is watched once. timeout is in milliseconds; a
42-// another fd or a kernel fence may retain the open file description.50+// negative one waits until an event or Close.
51+//
52+// Each call explicitly removes its epoll registrations before returning.
53+// Relying on close to remove them is unsafe: another fd or a kernel fence
54+// may retain the open file description and keep the epoll fd readable.
55+// When nothing is ready yet, a descriptor epoll cannot watch (EPERM,
56+// EINVAL, ELOOP for epoll nested too deep) fails the call.
43 func (p *Poller) Poll(fds []unix.PollFd, timeout int) (int, error) {57 func (p *Poller) Poll(fds []unix.PollFd, timeout int) (int, error) {
58+ if p.broken != nil {
59+ return 0, p.broken
60+ }
61+ // Ready descriptors need no registration, and an expired deadline
62+ // would skip the readiness check in RawConn.Read.
63+ if n, err := pollNow(fds); n != 0 || err != nil || timeout == 0 {
64+ return n, err
65+ }
44 var ctlErr error66 var ctlErr error
45 added := 067 added := 0
46 err := p.raw.Control(func(ep uintptr) {68 err := p.raw.Control(func(ep uintptr) {
47 for _, f := range fds {69 for _, f := range fds {
48- if f.Fd < 0 {
49- added++
50- continue
51- }
52- ev := unix.EpollEvent{Events: unix.EPOLLIN, Fd: f.Fd}
53- if e := unix.EpollCtl(int(ep), unix.EPOLL_CTL_ADD, int(f.Fd), &ev); e != nil {
54- ctlErr = e
55- return
56- }
57- added++
58- }
59- })
60- defer p.raw.Control(func(ep uintptr) {
61- for _, f := range fds[:added] {
62 if f.Fd >= 0 {70 if f.Fd >= 0 {
63- unix.EpollCtl(int(ep), unix.EPOLL_CTL_DEL, int(f.Fd), nil)71+ ev := unix.EpollEvent{Events: unix.EPOLLIN, Fd: f.Fd}
72+ e := unix.EpollCtl(int(ep), unix.EPOLL_CTL_ADD, int(f.Fd), &ev)
73+ if e != nil && !(errors.Is(e, unix.EEXIST) && listedBefore(fds, added)) {
74+ ctlErr = fmt.Errorf("fdwait watch fd %d: %w", f.Fd, e)
75+ return
76+ }
64 }77 }
78+ added++
65 }79 }
66 })80 })
81+ defer p.unwatch(fds[:added])
67 if err != nil {82 if err != nil {
68 return 0, err83 return 0, err
69 }84 }
70 if ctlErr != nil {85 if ctlErr != nil {
71 return 0, ctlErr86 return 0, ctlErr
72 }87 }
73- if timeout >= 0 {88+ if timeout > 0 {
74 err = p.file.SetReadDeadline(time.Now().Add(time.Duration(timeout) * time.Millisecond))89 err = p.file.SetReadDeadline(time.Now().Add(time.Duration(timeout) * time.Millisecond))
75 } else {90 } else {
76 err = p.file.SetReadDeadline(time.Time{})91 err = p.file.SetReadDeadline(time.Time{})
@@ -81,8 +96,8 @@ func (p *Poller) Poll(fds []unix.PollFd, timeout int) (int, error) {
81 var n int96 var n int
82 var pollErr error97 var pollErr error
83 err = p.raw.Read(func(_ uintptr) bool {98 err = p.raw.Read(func(_ uintptr) bool {
84- n, pollErr = unix.Poll(fds, 0)99+ n, pollErr = pollNow(fds)
85- return n > 0 || (pollErr != nil && !errors.Is(pollErr, unix.EINTR))100+ return n != 0 || pollErr != nil
86 })101 })
87 if errors.Is(err, os.ErrDeadlineExceeded) {102 if errors.Is(err, os.ErrDeadlineExceeded) {
88 return 0, nil103 return 0, nil
@@ -92,3 +107,34 @@ func (p *Poller) Poll(fds []unix.PollFd, timeout int) (int, error) {
92 }107 }
93 return n, pollErr108 return n, pollErr
94 }109 }
110+
111+// unwatch removes the registrations Poll added for fds. A failure means a
112+// watched fd was closed during the wait: its registration may outlive it.
113+func (p *Poller) unwatch(fds []unix.PollFd) {
114+ p.raw.Control(func(ep uintptr) {
115+ for i, f := range fds {
116+ if f.Fd < 0 || listedBefore(fds, i) {
117+ continue
118+ }
119+ if e := unix.EpollCtl(int(ep), unix.EPOLL_CTL_DEL, int(f.Fd), nil); e != nil && p.broken == nil {
120+ p.broken = fmt.Errorf("fdwait unwatch fd %d (closed during a wait?): %w", f.Fd, e)
121+ }
122+ }
123+ })
124+}
125+
126+// listedBefore reports whether fds[i].Fd also appears earlier in fds.
127+func listedBefore(fds []unix.PollFd, i int) bool {
128+ fd := fds[i].Fd
129+ return slices.ContainsFunc(fds[:i], func(f unix.PollFd) bool { return f.Fd == fd })
130+}
131+
132+// pollNow checks fds without blocking, retrying an interrupted check.
133+func pollNow(fds []unix.PollFd) (int, error) {
134+ for {
135+ n, err := unix.Poll(fds, 0)
136+ if !errors.Is(err, unix.EINTR) {
137+ return n, err
138+ }
139+ }
140+}
D internal/adapters/fdwait/reuse_test.go +0 -58
@@ -1,58 +0,0 @@
1-package fdwait
2-
3-import (
4- "golang.org/x/sys/unix"
5- "testing"
6-)
7-
8-// Closing a watched file removes it from epoll. Reusing its number must
9-// register the new file, not leave it waiting on the old registration.
10-func TestPollRecycledDescriptor(t *testing.T) {
11- p, err := New()
12- if err != nil {
13- t.Fatal(err)
14- }
15- defer p.Close()
16- fd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
17- if err != nil {
18- t.Fatal(err)
19- }
20- fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}}
21- if _, err := p.Poll(fds, 1); err != nil {
22- unix.Close(fd)
23- t.Fatal(err)
24- }
25- unix.Close(fd)
26- replacement, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
27- if err != nil {
28- t.Fatal(err)
29- }
30- defer unix.Close(replacement)
31- if replacement != fd {
32- t.Skip("descriptor number not reused")
33- }
34- if _, err := unix.Write(replacement, []byte{1, 0, 0, 0, 0, 0, 0, 0}); err != nil {
35- t.Fatal(err)
36- }
37- if n, err := p.Poll(fds, 1000); n != 1 || err != nil {
38- t.Fatalf("reused descriptor: %d %v", n, err)
39- }
40-}
41-
42-func TestPollHangup(t *testing.T) {
43- p, err := New()
44- if err != nil {
45- t.Fatal(err)
46- }
47- defer p.Close()
48- var pipe [2]int
49- if err := unix.Pipe2(pipe[:], unix.O_NONBLOCK|unix.O_CLOEXEC); err != nil {
50- t.Fatal(err)
51- }
52- defer unix.Close(pipe[0])
53- unix.Close(pipe[1])
54- fds := []unix.PollFd{{Fd: int32(pipe[0]), Events: unix.POLLIN}}
55- if n, err := p.Poll(fds, 1000); n != 1 || err != nil || fds[0].Revents&unix.POLLHUP == 0 {
56- t.Fatalf("hangup: %d %v %+v", n, err, fds)
57- }
58-}
@@ -1,58 +0,0 @@
1-package fdwait
2-
3-import (
4- "golang.org/x/sys/unix"
5- "testing"
6-)
7-
8-// Closing a watched file removes it from epoll. Reusing its number must
9-// register the new file, not leave it waiting on the old registration.
10-func TestPollRecycledDescriptor(t *testing.T) {
11- p, err := New()
12- if err != nil {
13- t.Fatal(err)
14- }
15- defer p.Close()
16- fd, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
17- if err != nil {
18- t.Fatal(err)
19- }
20- fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}}
21- if _, err := p.Poll(fds, 1); err != nil {
22- unix.Close(fd)
23- t.Fatal(err)
24- }
25- unix.Close(fd)
26- replacement, err := unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC)
27- if err != nil {
28- t.Fatal(err)
29- }
30- defer unix.Close(replacement)
31- if replacement != fd {
32- t.Skip("descriptor number not reused")
33- }
34- if _, err := unix.Write(replacement, []byte{1, 0, 0, 0, 0, 0, 0, 0}); err != nil {
35- t.Fatal(err)
36- }
37- if n, err := p.Poll(fds, 1000); n != 1 || err != nil {
38- t.Fatalf("reused descriptor: %d %v", n, err)
39- }
40-}
41-
42-func TestPollHangup(t *testing.T) {
43- p, err := New()
44- if err != nil {
45- t.Fatal(err)
46- }
47- defer p.Close()
48- var pipe [2]int
49- if err := unix.Pipe2(pipe[:], unix.O_NONBLOCK|unix.O_CLOEXEC); err != nil {
50- t.Fatal(err)
51- }
52- defer unix.Close(pipe[0])
53- unix.Close(pipe[1])
54- fds := []unix.PollFd{{Fd: int32(pipe[0]), Events: unix.POLLIN}}
55- if n, err := p.Poll(fds, 1000); n != 1 || err != nil || fds[0].Revents&unix.POLLHUP == 0 {
56- t.Fatalf("hangup: %d %v %+v", n, err, fds)
57- }
58-}
M internal/adapters/wayland/alloc_hotpath_test.go +1 -0
@@ -65,6 +65,7 @@ func TestCommitSyncStorageIsPerUpdate(t *testing.T) {
6565 t.Fatal(err)
6666 }
6767 defer sw.pipeR.Close()
68+ defer sw.waiter.Close()
6869 defer sw.close()
6970 srv.syncWait = sw
7071 tl := &timeline{dev: dev, handle: 7, alive: true}
@@ -65,6 +65,7 @@ func TestCommitSyncStorageIsPerUpdate(t *testing.T) {
65 t.Fatal(err)65 t.Fatal(err)
66 }66 }
67 defer sw.pipeR.Close()67 defer sw.pipeR.Close()
68+ defer sw.waiter.Close()
68 defer sw.close()69 defer sw.close()
69 srv.syncWait = sw70 srv.syncWait = sw
70 tl := &timeline{dev: dev, handle: 7, alive: true}71 tl := &timeline{dev: dev, handle: 7, alive: true}
M internal/adapters/wayland/syncobj.go +3 -9
@@ -207,7 +207,6 @@ func (p unixAcquirePoller) poll(fds []unix.PollFd, timeout int) (int, error) {
207207 type syncWaiter struct {
208208 poller acquirePoller
209209 waiter *fdwait.Poller
210- pipeFD int
211210 terminal bool // polling ended in failure; protected by mu
212211 mu sync.Mutex
213212 ready map[*syncWait]bool
@@ -238,7 +237,7 @@ func newSyncWaiter(wake func()) (*syncWaiter, error) {
238237 w.Close()
239238 return nil, err
240239 }
241- return &syncWaiter{poller: unixAcquirePoller{p}, waiter: p, pipeFD: int(r.Fd()), ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, log: zerowrap.Default()}, nil
240+ return &syncWaiter{poller: unixAcquirePoller{p}, waiter: p, ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, log: zerowrap.Default()}, nil
242241 }
243242
244243 // watch registers an eventfd for p; the wait is ready once it fires.
@@ -384,13 +383,8 @@ func (sw *syncWaiter) fail(waits []*syncWait) {
384383 // run polls every watched eventfd; it owns and closes them.
385384 func (sw *syncWaiter) run(ctx context.Context) {
386385 defer sw.pipeR.Close()
387- if sw.waiter != nil {
388- defer sw.waiter.Close()
389- }
390- pipeFD := sw.pipeFD
391- if sw.waiter == nil { // bare test fixtures own their pipe
392- pipeFD = int(sw.pipeR.Fd())
393- }
386+ defer sw.waiter.Close()
387+ pipeFD := int(sw.pipeR.Fd())
394388 var waits []*syncWait
395389 defer func() {
396390 sw.mu.Lock()
@@ -207,7 +207,6 @@ func (p unixAcquirePoller) poll(fds []unix.PollFd, timeout int) (int, error) {
207 type syncWaiter struct {207 type syncWaiter struct {
208 poller acquirePoller208 poller acquirePoller
209 waiter *fdwait.Poller209 waiter *fdwait.Poller
210- pipeFD int
211 terminal bool // polling ended in failure; protected by mu210 terminal bool // polling ended in failure; protected by mu
212 mu sync.Mutex211 mu sync.Mutex
213 ready map[*syncWait]bool212 ready map[*syncWait]bool
@@ -238,7 +237,7 @@ func newSyncWaiter(wake func()) (*syncWaiter, error) {
238 w.Close()237 w.Close()
239 return nil, err238 return nil, err
240 }239 }
241- return &syncWaiter{poller: unixAcquirePoller{p}, waiter: p, pipeFD: int(r.Fd()), ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, log: zerowrap.Default()}, nil240+ return &syncWaiter{poller: unixAcquirePoller{p}, waiter: p, ready: map[*syncWait]bool{}, wake: wake, pipeR: r, pipeW: w, log: zerowrap.Default()}, nil
242 }241 }
243 242
244 // watch registers an eventfd for p; the wait is ready once it fires.243 // watch registers an eventfd for p; the wait is ready once it fires.
@@ -384,13 +383,8 @@ func (sw *syncWaiter) fail(waits []*syncWait) {
384 // run polls every watched eventfd; it owns and closes them.383 // run polls every watched eventfd; it owns and closes them.
385 func (sw *syncWaiter) run(ctx context.Context) {384 func (sw *syncWaiter) run(ctx context.Context) {
386 defer sw.pipeR.Close()385 defer sw.pipeR.Close()
387- if sw.waiter != nil {386+ defer sw.waiter.Close()
388- defer sw.waiter.Close()387+ pipeFD := int(sw.pipeR.Fd())
389- }
390- pipeFD := sw.pipeFD
391- if sw.waiter == nil { // bare test fixtures own their pipe
392- pipeFD = int(sw.pipeR.Fd())
393- }
394 var waits []*syncWait388 var waits []*syncWait
395 defer func() {389 defer func() {
396 sw.mu.Lock()390 sw.mu.Lock()
M internal/adapters/wayland/syncwait_test.go +2 -0
@@ -20,6 +20,7 @@ func TestSyncWaitAlreadySignalled(t *testing.T) {
2020 t.Fatal(err)
2121 }
2222 defer sw.pipeR.Close()
23+ defer sw.waiter.Close()
2324 defer sw.close()
2425 w, err := sw.watch(syncPoint{tl: &timeline{dev: dev, handle: 7}, point: 4})
2526 if err != nil || !sw.fired(w) {
@@ -34,6 +35,7 @@ func TestImplicitPollErrorRejectedAndWarnOnce(t *testing.T) {
3435 t.Fatal(err)
3536 }
3637 defer sw.pipeR.Close()
38+ defer sw.waiter.Close()
3739 defer sw.close()
3840 var output bytes.Buffer
3941 sw.log = zerowrap.New(zerowrap.Config{Output: &output})
@@ -20,6 +20,7 @@ func TestSyncWaitAlreadySignalled(t *testing.T) {
20 t.Fatal(err)20 t.Fatal(err)
21 }21 }
22 defer sw.pipeR.Close()22 defer sw.pipeR.Close()
23+ defer sw.waiter.Close()
23 defer sw.close()24 defer sw.close()
24 w, err := sw.watch(syncPoint{tl: &timeline{dev: dev, handle: 7}, point: 4})25 w, err := sw.watch(syncPoint{tl: &timeline{dev: dev, handle: 7}, point: 4})
25 if err != nil || !sw.fired(w) {26 if err != nil || !sw.fired(w) {
@@ -34,6 +35,7 @@ func TestImplicitPollErrorRejectedAndWarnOnce(t *testing.T) {
34 t.Fatal(err)35 t.Fatal(err)
35 }36 }
36 defer sw.pipeR.Close()37 defer sw.pipeR.Close()
38+ defer sw.waiter.Close()
37 defer sw.close()39 defer sw.close()
38 var output bytes.Buffer40 var output bytes.Buffer
39 sw.log = zerowrap.New(zerowrap.Config{Output: &output})41 sw.log = zerowrap.New(zerowrap.Config{Output: &output})

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close