v0.2.0 ab733e5

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.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}

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