1//go:build linux
2// +build linux
3
4package wlturbo
5
6import (
7 "errors"
8 "io"
9 "net"
10 "os"
11 "testing"
12 "time"
13
14 "golang.org/x/sys/unix"
15)
16
17// fdRecordingProxy consumes the file descriptor of every event it receives.
18// Events without a descriptor are left untouched.
19type fdRecordingProxy struct {
20 BaseProxy
21 fds *[]int
22}
23
24func (p *fdRecordingProxy) Dispatch(event *Event) {
25 if fd := event.Fd(); fd != 0 {
26 *p.fds = append(*p.fds, int(fd))
27 return
28 }
29 // No descriptor arrived. Fd already consumed the placeholder argument.
30}
31
32// fdIgnoringProxy never reads the descriptor of an event.
33type fdIgnoringProxy struct {
34 BaseProxy
35}
36
37func (p *fdIgnoringProxy) Dispatch(*Event) {}
38
39// unixSocketPair returns a connected socket pair as net.UnixConns.
40func unixSocketPair(t *testing.T) (client, peer *net.UnixConn) {
41 t.Helper()
42
43 fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
44 if err != nil {
45 t.Fatalf("socketpair: %v", err)
46 }
47
48 clientFile := os.NewFile(uintptr(fds[0]), "client")
49 peerFile := os.NewFile(uintptr(fds[1]), "peer")
50
51 clientConn, err := net.FileConn(clientFile)
52 clientFile.Close()
53 if err != nil {
54 peerFile.Close()
55 t.Fatalf("net.FileConn(client): %v", err)
56 }
57
58 peerConn, err := net.FileConn(peerFile)
59 peerFile.Close()
60 if err != nil {
61 clientConn.Close()
62 t.Fatalf("net.FileConn(peer): %v", err)
63 }
64
65 t.Cleanup(func() {
66 clientConn.Close()
67 peerConn.Close()
68 })
69
70 return clientConn.(*net.UnixConn), peerConn.(*net.UnixConn)
71}
72
73// sendMessageWithFDs writes one framed message and attaches fds to it.
74func sendMessageWithFDs(t *testing.T, peer *net.UnixConn, msg []byte, fds []int) {
75 t.Helper()
76
77 var oob []byte
78 if len(fds) > 0 {
79 oob = unix.UnixRights(fds...)
80 }
81 if _, _, err := peer.WriteMsgUnix(msg, oob, nil); err != nil {
82 t.Fatalf("WriteMsgUnix: %v", err)
83 }
84}
85
86// newFDDisplay builds a display over the client end with a proxy registered at
87// object ID 7 and returns the descriptors that proxy consumes.
88func newFDDisplay(t *testing.T, client *net.UnixConn) (*Display, *[]int) {
89 t.Helper()
90
91 d := newDisplay(client)
92 fds := []int{}
93 d.RegisterEventSignature(7, 3, "uint,fd,")
94 d.RegisterEventSignature(7, 4, "uint,")
95 d.objects.Store(uint32(7), &fdRecordingProxy{
96 BaseProxy: BaseProxy{id: 7, context: d.context},
97 fds: &fds,
98 })
99 return d, &fds
100}
101
102// writeSentinel writes a unique byte string through fd and closes the caller's
103// copy of the descriptor. Reading the same sentinel from the paired read end
104// proves the descriptor really is the expected one.
105func writeSentinel(t *testing.T, fd uintptr, sentinel string) {
106 t.Helper()
107 if _, err := unix.Write(int(fd), []byte(sentinel)); err != nil {
108 t.Fatalf("write sentinel through received fd: %v", err)
109 }
110 if err := unix.Close(int(fd)); err != nil {
111 t.Fatalf("close received fd: %v", err)
112 }
113}
114
115// readSentinel reads exactly len(sentinel) bytes and compares them.
116func readSentinel(t *testing.T, r *os.File, sentinel string) {
117 t.Helper()
118 buf := make([]byte, len(sentinel))
119 if _, err := io.ReadFull(r, buf); err != nil {
120 t.Fatalf("read sentinel: %v", err)
121 }
122 if string(buf) != sentinel {
123 t.Fatalf("sentinel = %q, want %q", buf, sentinel)
124 }
125}
126
127func TestDisplayFD_Single(t *testing.T) {
128 client, peer := unixSocketPair(t)
129 d, received := newFDDisplay(t, client)
130
131 r, w, err := os.Pipe()
132 if err != nil {
133 t.Fatalf("pipe: %v", err)
134 }
135 defer r.Close()
136
137 sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}), []int{int(w.Fd())})
138 w.Close()
139
140 if err := d.Dispatch(); err != nil {
141 t.Fatalf("Dispatch: %v", err)
142 }
143 if len(*received) != 1 {
144 t.Fatalf("received %d descriptors, want 1", len(*received))
145 }
146
147 writeSentinel(t, uintptr((*received)[0]), "single-fd-sentinel")
148 readSentinel(t, r, "single-fd-sentinel")
149}
150
151func TestDisplayFD_ExtraDescriptorRejected(t *testing.T) {
152 client, peer := unixSocketPair(t)
153 d, _ := newFDDisplay(t, client)
154 r, w, err := os.Pipe()
155 if err != nil {
156 t.Fatal(err)
157 }
158 defer r.Close()
159 defer w.Close()
160 sendMessageWithFDs(t, peer, message(7, 3, nil), []int{int(w.Fd()), int(w.Fd())})
161 if err := d.Dispatch(); !errors.Is(err, ErrMalformedFrame) {
162 t.Fatalf("extra FD: %v", err)
163 }
164}
165
166func TestDisplayFD_EventWithoutFDsAfterOneWithFDs(t *testing.T) {
167 client, peer := unixSocketPair(t)
168 d, received := newFDDisplay(t, client)
169
170 r, w, err := os.Pipe()
171 if err != nil {
172 t.Fatalf("pipe: %v", err)
173 }
174 defer r.Close()
175
176 // First event carries a descriptor.
177 sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}), []int{int(w.Fd())})
178 w.Close()
179 if err := d.Dispatch(); err != nil {
180 t.Fatalf("first Dispatch: %v", err)
181 }
182
183 // Second event carries none and must not steal the first one.
184 sendMessageWithFDs(t, peer, message(7, 4, []byte{0, 0, 0, 0}), nil)
185 if err := d.Dispatch(); err != nil {
186 t.Fatalf("second Dispatch: %v", err)
187 }
188
189 if len(*received) != 1 {
190 t.Fatalf("received %d descriptors, want exactly 1", len(*received))
191 }
192
193 writeSentinel(t, uintptr((*received)[0]), "only-fd")
194 readSentinel(t, r, "only-fd")
195}
196
197func TestDisplayFD_TwoDisplaysAreIsolated(t *testing.T) {
198 firstClient, firstPeer := unixSocketPair(t)
199 secondClient, secondPeer := unixSocketPair(t)
200
201 firstDisplay, firstReceived := newFDDisplay(t, firstClient)
202 secondDisplay, secondReceived := newFDDisplay(t, secondClient)
203
204 firstR, firstW, err := os.Pipe()
205 if err != nil {
206 t.Fatalf("pipe: %v", err)
207 }
208 defer firstR.Close()
209 secondR, secondW, err := os.Pipe()
210 if err != nil {
211 t.Fatalf("pipe: %v", err)
212 }
213 defer secondR.Close()
214
215 sendMessageWithFDs(t, firstPeer, message(7, 3, []byte{0, 0, 0, 0}), []int{int(firstW.Fd())})
216 firstW.Close()
217 sendMessageWithFDs(t, secondPeer, message(7, 3, []byte{0, 0, 0, 0}), []int{int(secondW.Fd())})
218 secondW.Close()
219
220 if err := secondDisplay.Dispatch(); err != nil {
221 t.Fatalf("second Dispatch: %v", err)
222 }
223 if err := firstDisplay.Dispatch(); err != nil {
224 t.Fatalf("first Dispatch: %v", err)
225 }
226
227 // Each connection must only see its own descriptor.
228 if len(*firstReceived) != 1 {
229 t.Fatalf("first display received %d descriptors, want 1", len(*firstReceived))
230 }
231 if len(*secondReceived) != 1 {
232 t.Fatalf("second display received %d descriptors, want 1", len(*secondReceived))
233 }
234
235 writeSentinel(t, uintptr((*secondReceived)[0]), "second-display")
236 writeSentinel(t, uintptr((*firstReceived)[0]), "first-display")
237
238 readSentinel(t, secondR, "second-display")
239 readSentinel(t, firstR, "first-display")
240}
241
242func TestDisplayFD_TruncatedControlData(t *testing.T) {
243 client, peer := unixSocketPair(t)
244 d, _ := newFDDisplay(t, client)
245
246 // One more descriptor than a single read can carry.
247 devNull, err := os.Open(os.DevNull)
248 if err != nil {
249 t.Fatalf("open %s: %v", os.DevNull, err)
250 }
251 defer devNull.Close()
252
253 fds := make([]int, 0, maxRecvFDs+1)
254 for i := 0; i < maxRecvFDs+1; i++ {
255 dup, err := unix.Dup(int(devNull.Fd()))
256 if err != nil {
257 t.Fatalf("dup: %v", err)
258 }
259 t.Cleanup(func() { _ = unix.Close(dup) })
260 fds = append(fds, dup)
261 }
262
263 sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}), fds)
264
265 err = d.Dispatch()
266 if !errors.Is(err, ErrMalformedFrame) {
267 t.Fatalf("Dispatch error = %v, want %v", err, ErrMalformedFrame)
268 }
269
270 var perr *ProtocolError
271 if !errors.As(err, &perr) {
272 t.Fatalf("Dispatch error type = %T, want *ProtocolError", err)
273 }
274 if perr.Kind != "control_truncated" {
275 t.Fatalf("error kind = %q, want %q", perr.Kind, "control_truncated")
276 }
277
278 // A truncated control message must not leave descriptors queued.
279 d.recvMu.Lock()
280 pending := len(d.pendingFDs)
281 d.recvMu.Unlock()
282 if pending != 0 {
283 t.Fatalf("%d descriptors still queued after a truncated control message", pending)
284 }
285}
286
287func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) {
288 client, peer := unixSocketPair(t)
289 d := newDisplay(client)
290 d.RegisterEventSignature(7, 3, "uint,fd,")
291 d.objects.Store(uint32(7), &fdIgnoringProxy{
292 BaseProxy: BaseProxy{id: 7, context: d.context},
293 })
294
295 r, w, err := os.Pipe()
296 if err != nil {
297 t.Fatalf("pipe: %v", err)
298 }
299 defer r.Close()
300
301 sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}), []int{int(w.Fd())})
302 // Drop our own copy: the display now holds the only writer.
303 w.Close()
304
305 if err := d.Dispatch(); err != nil {
306 t.Fatalf("Dispatch: %v", err)
307 }
308
309 // An ignored FD is closed at the end of the event, not at shutdown.
310 if err := r.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
311 t.Fatal(err)
312 }
313 if _, err := r.Read(make([]byte, 1)); !errors.Is(err, io.EOF) {
314 t.Fatalf("ignored FD: %v", err)
315 }
316
317 // Closing the display must release it, so the read end sees EOF.
318 if err := d.Close(); err != nil {
319 t.Fatalf("Close: %v", err)
320 }
321 if err := r.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
322 t.Fatalf("SetReadDeadline: %v", err)
323 }
324 if _, err := r.Read(make([]byte, 1)); !errors.Is(err, io.EOF) {
325 t.Fatalf("read after Close = %v, want %v", err, io.EOF)
326 }
327}
328
329// rawSocketPair returns a connected socket pair without registering cleanup,
330// so loop tests can close descriptors deterministically.
331func rawSocketPair(t *testing.T) (client, peer *net.UnixConn) {
332 t.Helper()
333
334 fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
335 if err != nil {
336 t.Fatalf("socketpair: %v", err)
337 }
338
339 clientFile := os.NewFile(uintptr(fds[0]), "client")
340 clientConn, err := net.FileConn(clientFile)
341 clientFile.Close()
342 if err != nil {
343 t.Fatalf("net.FileConn(client): %v", err)
344 }
345
346 peerFile := os.NewFile(uintptr(fds[1]), "peer")
347 peerConn, err := net.FileConn(peerFile)
348 peerFile.Close()
349 if err != nil {
350 t.Fatalf("net.FileConn(peer): %v", err)
351 }
352
353 return clientConn.(*net.UnixConn), peerConn.(*net.UnixConn)
354}
355
356// openFDs reports how many descriptors the process holds.
357func openFDs(t *testing.T) int {
358 t.Helper()
359
360 entries, err := os.ReadDir("/proc/self/fd")
361 if err != nil {
362 t.Skipf("cannot count open descriptors: %v", err)
363 }
364 return len(entries)
365}
366
367func TestDisplayFD_NoLeakAcrossConnections(t *testing.T) {
368 const iterations = 100
369
370 devNull, err := os.Open(os.DevNull)
371 if err != nil {
372 t.Fatalf("open %s: %v", os.DevNull, err)
373 }
374 defer devNull.Close()
375
376 // Warm up so caches and pools are populated before measuring.
377 runFDLeakIteration(t, devNull)
378
379 before := openFDs(t)
380 for i := 0; i < iterations; i++ {
381 runFDLeakIteration(t, devNull)
382 }
383 after := openFDs(t)
384
385 if after > before {
386 t.Fatalf("descriptor count grew from %d to %d over %d connections", before, after, iterations)
387 }
388}
389
390// runFDLeakIteration opens a connection, delivers a descriptor nobody reads and
391// tears everything down.
392func runFDLeakIteration(t *testing.T, devNull *os.File) {
393 t.Helper()
394
395 client, peer := rawSocketPair(t)
396 d := newDisplay(client)
397 d.RegisterEventSignature(7, 3, "uint,fd,")
398 d.objects.Store(uint32(7), &fdIgnoringProxy{
399 BaseProxy: BaseProxy{id: 7, context: d.context},
400 })
401
402 sent, err := unix.Dup(int(devNull.Fd()))
403 if err != nil {
404 t.Fatalf("dup: %v", err)
405 }
406 sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}), []int{sent})
407 if err := unix.Close(sent); err != nil {
408 t.Fatalf("close sent descriptor: %v", err)
409 }
410
411 if err := d.Dispatch(); err != nil {
412 t.Fatalf("Dispatch: %v", err)
413 }
414 if err := d.Close(); err != nil {
415 t.Fatalf("Close: %v", err)
416 }
417 if err := peer.Close(); err != nil {
418 t.Fatalf("close peer: %v", err)
419 }
420}
421
422func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) {
423 client, peer := unixSocketPair(t)
424 d := newDisplay(client)
425 d.RegisterEventSignature(7, 3, "uint,fd,")
426 d.objects.Store(uint32(7), &fdIgnoringProxy{
427 BaseProxy: BaseProxy{id: 7, context: d.context},
428 })
429
430 r, w, err := os.Pipe()
431 if err != nil {
432 t.Fatalf("pipe: %v", err)
433 }
434 defer r.Close()
435
436 // A descriptor arrives together with a malformed frame.
437 sendMessageWithFDs(t, peer, header(7, 3, 0), []int{int(w.Fd())})
438 w.Close()
439
440 err = d.Dispatch()
441 if !errors.Is(err, ErrMalformedFrame) {
442 t.Fatalf("Dispatch error = %v, want %v", err, ErrMalformedFrame)
443 }
444
445 d.recvMu.Lock()
446 pending := len(d.pendingFDs)
447 d.recvMu.Unlock()
448 if pending != 0 {
449 t.Fatalf("%d descriptors still queued after a malformed frame", pending)
450 }
451
452 if err := r.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
453 t.Fatalf("SetReadDeadline: %v", err)
454 }
455 if _, err := r.Read(make([]byte, 1)); !errors.Is(err, io.EOF) {
456 t.Fatalf("read after malformed frame = %v, want %v", err, io.EOF)
457 }
458}
459
460// Closing in a callback must not wait for the dispatch reader's state lock.
461type closingProxy struct {
462 BaseProxy
463 d *Display
464 returned bool
465}
466
467func (p *closingProxy) EventSignature(op uint16) (string, bool) { return "fd,", op == 0 }
468func (p *closingProxy) Dispatch(*Event) { p.returned = p.d.Close() == nil }
469
470func TestCloseInsideHandlerClosesQueuedFDs(t *testing.T) {
471 client, peer := unixSocketPair(t)
472 d := newDisplay(client)
473 proxy := &closingProxy{BaseProxy: BaseProxy{id: 7, context: d.context}, d: d}
474 d.context.Register(proxy)
475 r, w, err := os.Pipe()
476 if err != nil {
477 t.Fatal(err)
478 }
479 defer r.Close()
480 extra, xw, err := os.Pipe()
481 if err != nil {
482 t.Fatal(err)
483 }
484 defer extra.Close()
485 // Two frames in one read; second FD remains queued while first handler runs.
486 payload := append(message(7, 0, nil), message(7, 0, nil)...)
487 sendMessageWithFDs(t, peer, payload, []int{int(w.Fd()), int(xw.Fd())})
488 w.Close()
489 xw.Close()
490 if err := d.Dispatch(); err != nil {
491 t.Fatal(err)
492 }
493 if !proxy.returned {
494 t.Fatal("Close did not return from handler")
495 }
496 for _, reader := range []*os.File{r, extra} {
497 if err := reader.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
498 t.Fatal(err)
499 }
500 if _, err := reader.Read(make([]byte, 1)); !errors.Is(err, io.EOF) {
501 t.Fatalf("fd not closed: %v", err)
502 }
503 }
504}
505
506func TestParseFDsKeepsRightsBeforeMalformedHeader(t *testing.T) {
507 r, w, err := os.Pipe()
508 if err != nil {
509 t.Fatal(err)
510 }
511 defer r.Close()
512 defer w.Close()
513 rights := unix.UnixRights(int(w.Fd()))
514 oob := append(rights, make([]byte, unix.SizeofCmsghdr)...)
515 fds, err := parseFDs(oob)
516 if err == nil || len(fds) != 1 {
517 t.Fatalf("fds=%v err=%v", fds, err)
518 }
519 if _, err := unix.FcntlInt(uintptr(fds[0]), unix.F_GETFD, 0); err != nil {
520 t.Fatal(err)
521 }
522 for _, fd := range fds {
523 if err := unix.Close(fd); err != nil {
524 t.Fatal(err)
525 }
526 }
527}