v0.5.0 089fa12

fd_test.go RawHistoryFolder

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}

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 1

Tags 8

↑↓ moveenter openesc close