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.objects.Store(uint32(7), &fdRecordingProxy{
94 BaseProxy: BaseProxy{id: 7, context: d.context},
95 fds: &fds,
96 })
97 return d, &fds
98}
99
100// writeSentinel writes a unique byte string through fd and closes the caller's
101// copy of the descriptor. Reading the same sentinel from the paired read end
102// proves the descriptor really is the expected one.
103func writeSentinel(t *testing.T, fd uintptr, sentinel string) {
104 t.Helper()
105 if _, err := unix.Write(int(fd), []byte(sentinel)); err != nil {
106 t.Fatalf("write sentinel through received fd: %v", err)
107 }
108 if err := unix.Close(int(fd)); err != nil {
109 t.Fatalf("close received fd: %v", err)
110 }
111}
112
113// readSentinel reads exactly len(sentinel) bytes and compares them.
114func readSentinel(t *testing.T, r *os.File, sentinel string) {
115 t.Helper()
116 buf := make([]byte, len(sentinel))
117 if _, err := io.ReadFull(r, buf); err != nil {
118 t.Fatalf("read sentinel: %v", err)
119 }
120 if string(buf) != sentinel {
121 t.Fatalf("sentinel = %q, want %q", buf, sentinel)
122 }
123}
124
125func TestDisplayFD_Single(t *testing.T) {
126 client, peer := unixSocketPair(t)
127 d, received := newFDDisplay(t, client)
128
129 r, w, err := os.Pipe()
130 if err != nil {
131 t.Fatalf("pipe: %v", err)
132 }
133 defer r.Close()
134
135 sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}), []int{int(w.Fd())})
136 w.Close()
137
138 if err := d.Dispatch(); err != nil {
139 t.Fatalf("Dispatch: %v", err)
140 }
141 if len(*received) != 1 {
142 t.Fatalf("received %d descriptors, want 1", len(*received))
143 }
144
145 writeSentinel(t, uintptr((*received)[0]), "single-fd-sentinel")
146 readSentinel(t, r, "single-fd-sentinel")
147}
148
149func TestDisplayFD_MultipleInOneControlMessage(t *testing.T) {
150 client, peer := unixSocketPair(t)
151 d, received := newFDDisplay(t, client)
152
153 firstR, firstW, err := os.Pipe()
154 if err != nil {
155 t.Fatalf("pipe: %v", err)
156 }
157 defer firstR.Close()
158 secondR, secondW, err := os.Pipe()
159 if err != nil {
160 t.Fatalf("pipe: %v", err)
161 }
162 defer secondR.Close()
163
164 sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}),
165 []int{int(firstW.Fd()), int(secondW.Fd())})
166 firstW.Close()
167 secondW.Close()
168
169 if err := d.Dispatch(); err != nil {
170 t.Fatalf("Dispatch: %v", err)
171 }
172
173 // A handler consumes one descriptor per fd argument, so the test drains
174 // whatever the connection still holds, in arrival order.
175 for {
176 fd, ok := d.nextFD()
177 if !ok {
178 break
179 }
180 *received = append(*received, fd)
181 }
182
183 if len(*received) != 2 {
184 t.Fatalf("received %d descriptors, want 2", len(*received))
185 }
186
187 writeSentinel(t, uintptr((*received)[0]), "first-pipe")
188 writeSentinel(t, uintptr((*received)[1]), "second-pipe")
189
190 readSentinel(t, firstR, "first-pipe")
191 readSentinel(t, secondR, "second-pipe")
192}
193
194func TestDisplayFD_EventWithoutFDsAfterOneWithFDs(t *testing.T) {
195 client, peer := unixSocketPair(t)
196 d, received := newFDDisplay(t, client)
197
198 r, w, err := os.Pipe()
199 if err != nil {
200 t.Fatalf("pipe: %v", err)
201 }
202 defer r.Close()
203
204 // First event carries a descriptor.
205 sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}), []int{int(w.Fd())})
206 w.Close()
207 if err := d.Dispatch(); err != nil {
208 t.Fatalf("first Dispatch: %v", err)
209 }
210
211 // Second event carries none and must not steal the first one.
212 sendMessageWithFDs(t, peer, message(7, 4, []byte{0, 0, 0, 0}), nil)
213 if err := d.Dispatch(); err != nil {
214 t.Fatalf("second Dispatch: %v", err)
215 }
216
217 if len(*received) != 1 {
218 t.Fatalf("received %d descriptors, want exactly 1", len(*received))
219 }
220
221 writeSentinel(t, uintptr((*received)[0]), "only-fd")
222 readSentinel(t, r, "only-fd")
223}
224
225func TestDisplayFD_TwoDisplaysAreIsolated(t *testing.T) {
226 firstClient, firstPeer := unixSocketPair(t)
227 secondClient, secondPeer := unixSocketPair(t)
228
229 firstDisplay, firstReceived := newFDDisplay(t, firstClient)
230 secondDisplay, secondReceived := newFDDisplay(t, secondClient)
231
232 firstR, firstW, err := os.Pipe()
233 if err != nil {
234 t.Fatalf("pipe: %v", err)
235 }
236 defer firstR.Close()
237 secondR, secondW, err := os.Pipe()
238 if err != nil {
239 t.Fatalf("pipe: %v", err)
240 }
241 defer secondR.Close()
242
243 sendMessageWithFDs(t, firstPeer, message(7, 3, []byte{0, 0, 0, 0}), []int{int(firstW.Fd())})
244 firstW.Close()
245 sendMessageWithFDs(t, secondPeer, message(7, 3, []byte{0, 0, 0, 0}), []int{int(secondW.Fd())})
246 secondW.Close()
247
248 if err := secondDisplay.Dispatch(); err != nil {
249 t.Fatalf("second Dispatch: %v", err)
250 }
251 if err := firstDisplay.Dispatch(); err != nil {
252 t.Fatalf("first Dispatch: %v", err)
253 }
254
255 // Each connection must only see its own descriptor.
256 if len(*firstReceived) != 1 {
257 t.Fatalf("first display received %d descriptors, want 1", len(*firstReceived))
258 }
259 if len(*secondReceived) != 1 {
260 t.Fatalf("second display received %d descriptors, want 1", len(*secondReceived))
261 }
262
263 writeSentinel(t, uintptr((*secondReceived)[0]), "second-display")
264 writeSentinel(t, uintptr((*firstReceived)[0]), "first-display")
265
266 readSentinel(t, secondR, "second-display")
267 readSentinel(t, firstR, "first-display")
268}
269
270func TestDisplayFD_TruncatedControlData(t *testing.T) {
271 client, peer := unixSocketPair(t)
272 d, _ := newFDDisplay(t, client)
273
274 // One more descriptor than a single read can carry.
275 devNull, err := os.Open(os.DevNull)
276 if err != nil {
277 t.Fatalf("open %s: %v", os.DevNull, err)
278 }
279 defer devNull.Close()
280
281 fds := make([]int, 0, maxRecvFDs+1)
282 for i := 0; i < maxRecvFDs+1; i++ {
283 dup, err := unix.Dup(int(devNull.Fd()))
284 if err != nil {
285 t.Fatalf("dup: %v", err)
286 }
287 t.Cleanup(func() { _ = unix.Close(dup) })
288 fds = append(fds, dup)
289 }
290
291 sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}), fds)
292
293 err = d.Dispatch()
294 if !errors.Is(err, ErrMalformedFrame) {
295 t.Fatalf("Dispatch error = %v, want %v", err, ErrMalformedFrame)
296 }
297
298 var perr *ProtocolError
299 if !errors.As(err, &perr) {
300 t.Fatalf("Dispatch error type = %T, want *ProtocolError", err)
301 }
302 if perr.Kind != "control_truncated" {
303 t.Fatalf("error kind = %q, want %q", perr.Kind, "control_truncated")
304 }
305
306 // A truncated control message must not leave descriptors queued.
307 d.fdMu.Lock()
308 pending := len(d.pendingFDs)
309 d.fdMu.Unlock()
310 if pending != 0 {
311 t.Fatalf("%d descriptors still queued after a truncated control message", pending)
312 }
313}
314
315func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) {
316 client, peer := unixSocketPair(t)
317 d := newDisplay(client)
318 d.objects.Store(uint32(7), &fdIgnoringProxy{
319 BaseProxy: BaseProxy{id: 7, context: d.context},
320 })
321
322 r, w, err := os.Pipe()
323 if err != nil {
324 t.Fatalf("pipe: %v", err)
325 }
326 defer r.Close()
327
328 sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}), []int{int(w.Fd())})
329 // Drop our own copy: the display now holds the only writer.
330 w.Close()
331
332 if err := d.Dispatch(); err != nil {
333 t.Fatalf("Dispatch: %v", err)
334 }
335
336 // While the display still holds the descriptor the pipe stays open.
337 if err := r.SetReadDeadline(time.Now().Add(200 * time.Millisecond)); err != nil {
338 t.Fatalf("SetReadDeadline: %v", err)
339 }
340 if _, err := r.Read(make([]byte, 1)); !errors.Is(err, os.ErrDeadlineExceeded) {
341 t.Fatalf("read with the display holding the descriptor = %v, want %v", err, os.ErrDeadlineExceeded)
342 }
343
344 // Closing the display must release it, so the read end sees EOF.
345 if err := d.Close(); err != nil {
346 t.Fatalf("Close: %v", err)
347 }
348 if err := r.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
349 t.Fatalf("SetReadDeadline: %v", err)
350 }
351 if _, err := r.Read(make([]byte, 1)); !errors.Is(err, io.EOF) {
352 t.Fatalf("read after Close = %v, want %v", err, io.EOF)
353 }
354}
355
356// rawSocketPair returns a connected socket pair without registering cleanup,
357// so loop tests can close descriptors deterministically.
358func rawSocketPair(t *testing.T) (client, peer *net.UnixConn) {
359 t.Helper()
360
361 fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
362 if err != nil {
363 t.Fatalf("socketpair: %v", err)
364 }
365
366 clientFile := os.NewFile(uintptr(fds[0]), "client")
367 clientConn, err := net.FileConn(clientFile)
368 clientFile.Close()
369 if err != nil {
370 t.Fatalf("net.FileConn(client): %v", err)
371 }
372
373 peerFile := os.NewFile(uintptr(fds[1]), "peer")
374 peerConn, err := net.FileConn(peerFile)
375 peerFile.Close()
376 if err != nil {
377 t.Fatalf("net.FileConn(peer): %v", err)
378 }
379
380 return clientConn.(*net.UnixConn), peerConn.(*net.UnixConn)
381}
382
383// openFDs reports how many descriptors the process holds.
384func openFDs(t *testing.T) int {
385 t.Helper()
386
387 entries, err := os.ReadDir("/proc/self/fd")
388 if err != nil {
389 t.Skipf("cannot count open descriptors: %v", err)
390 }
391 return len(entries)
392}
393
394func TestDisplayFD_NoLeakAcrossConnections(t *testing.T) {
395 const iterations = 100
396
397 devNull, err := os.Open(os.DevNull)
398 if err != nil {
399 t.Fatalf("open %s: %v", os.DevNull, err)
400 }
401 defer devNull.Close()
402
403 // Warm up so caches and pools are populated before measuring.
404 runFDLeakIteration(t, devNull)
405
406 before := openFDs(t)
407 for i := 0; i < iterations; i++ {
408 runFDLeakIteration(t, devNull)
409 }
410 after := openFDs(t)
411
412 if after > before {
413 t.Fatalf("descriptor count grew from %d to %d over %d connections", before, after, iterations)
414 }
415}
416
417// runFDLeakIteration opens a connection, delivers a descriptor nobody reads and
418// tears everything down.
419func runFDLeakIteration(t *testing.T, devNull *os.File) {
420 t.Helper()
421
422 client, peer := rawSocketPair(t)
423 d := newDisplay(client)
424 d.objects.Store(uint32(7), &fdIgnoringProxy{
425 BaseProxy: BaseProxy{id: 7, context: d.context},
426 })
427
428 sent, err := unix.Dup(int(devNull.Fd()))
429 if err != nil {
430 t.Fatalf("dup: %v", err)
431 }
432 sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}), []int{sent})
433 if err := unix.Close(sent); err != nil {
434 t.Fatalf("close sent descriptor: %v", err)
435 }
436
437 if err := d.Dispatch(); err != nil {
438 t.Fatalf("Dispatch: %v", err)
439 }
440 if err := d.Close(); err != nil {
441 t.Fatalf("Close: %v", err)
442 }
443 if err := peer.Close(); err != nil {
444 t.Fatalf("close peer: %v", err)
445 }
446}
447
448func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) {
449 client, peer := unixSocketPair(t)
450 d := newDisplay(client)
451 d.objects.Store(uint32(7), &fdIgnoringProxy{
452 BaseProxy: BaseProxy{id: 7, context: d.context},
453 })
454
455 r, w, err := os.Pipe()
456 if err != nil {
457 t.Fatalf("pipe: %v", err)
458 }
459 defer r.Close()
460
461 // A descriptor arrives together with a malformed frame.
462 sendMessageWithFDs(t, peer, header(7, 3, 0), []int{int(w.Fd())})
463 w.Close()
464
465 err = d.Dispatch()
466 if !errors.Is(err, ErrMalformedFrame) {
467 t.Fatalf("Dispatch error = %v, want %v", err, ErrMalformedFrame)
468 }
469
470 d.fdMu.Lock()
471 pending := len(d.pendingFDs)
472 d.fdMu.Unlock()
473 if pending != 0 {
474 t.Fatalf("%d descriptors still queued after a malformed frame", pending)
475 }
476
477 if err := r.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
478 t.Fatalf("SetReadDeadline: %v", err)
479 }
480 if _, err := r.Read(make([]byte, 1)); !errors.Is(err, io.EOF) {
481 t.Fatalf("read after malformed frame = %v, want %v", err, io.EOF)
482 }
483}