feat/data-dnd e05cb15

protection_retry_test.go RawHistoryFolder

1package drm
2
3import (
4	"context"
5	"errors"
6	"os"
7	"sync/atomic"
8	"testing"
9	"time"
10
11	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
12	"github.com/bnema/neferwl/internal/ports"
13	"github.com/stretchr/testify/mock"
14	"golang.org/x/sys/unix"
15)
16
17// runProtectedStartup runs o with the session already protected. k and r are
18// set up with permissive renderer/KMS expectations; inject holds the failure
19// under test.
20func runProtectedStartup(t *testing.T, o *Output, k *mockkms, renders func() (*os.File, error)) (<-chan ports.SecurityBackendEvent, <-chan error, context.CancelFunc) {
21	t.Helper()
22	o.cursor = nil
23	var state atomic.Uint64
24	state.Store(3)
25	securityGate(t, o, &state)
26	events := make(chan ports.SecurityBackendEvent, 32)
27	o.SecurityEvents = events
28	r := portsmocks.NewMockRenderer(t)
29	buf := func() ports.DMABuf {
30		f, w := protectionFence(t, false)
31		w.Close()
32		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
33	}
34	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
35	r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
36	r.EXPECT().UseTarget(mock.Anything).Return().Maybe()
37	r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
38		return renders()
39	}).Maybe()
40	r.EXPECT().Close().Return().Once()
41	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Maybe()
42	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
43	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
44	done := make(chan error, 1)
45	go func() {
46		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, nil, nil, nil, nil, discardSink(t), nil, nil)
47	}()
48	return events, done, cancel
49}
50
51func waitProof(t *testing.T, events <-chan ports.SecurityBackendEvent, done <-chan error) {
52	t.Helper()
53	timeout := time.After(4 * time.Second)
54	for {
55		select {
56		case ev := <-events:
57			if _, ok := ev.(ports.SecurityOutputProof); ok {
58				return
59			}
60		case err := <-done:
61			t.Fatalf("output stopped while protected: %v", err)
62		case <-timeout:
63			t.Fatal("no proof after retry")
64		}
65	}
66}
67
68// A protected startup whose first KMS attempt is refused (EBUSY,
69// or a swallowed EINVAL refusal) keeps the output, backs off from that first
70// refusal, and proves only after the accepted retry.
71func TestProtectedStartupRefusalRetriesWithBackoffFromFirstRefusal(t *testing.T) {
72	for _, errno := range []error{unix.EBUSY, unix.EINVAL} {
73		t.Run(errno.Error(), func(t *testing.T) {
74			// Commits: test-only modeset, then the blocking disable (refused).
75			o, k, commits, mu := testOutputMu(t, nil, errno)
76			k.EXPECT().createBlob(mock.Anything).Return(99, nil).Maybe()
77			k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
78			events, done, cancel := runProtectedStartup(t, o, k, func() (*os.File, error) { return nil, nil })
79			defer cancel()
80			waitProof(t, events, done)
81			cancel()
82			if err := <-done; err != nil {
83				t.Fatalf("output stopped: %v", err)
84			}
85			mu.Lock()
86			defer mu.Unlock()
87			var real []commitRec
88			for _, c := range *commits {
89				if c.flags&atomicTestOnly == 0 {
90					real = append(real, c)
91				}
92			}
93			if len(real) < 3 {
94				t.Fatalf("real commits %d", len(real))
95			}
96			if gap := real[1].at.Sub(real[0].at); gap < protectRetryMin {
97				t.Fatalf("retry after %v of the first refusal, want >= %v", gap, protectRetryMin)
98			}
99		})
100	}
101}
102
103// Lost master at protected startup keeps the output running, dark,
104// without proof; it follows the lost-master invalidate path.
105func TestProtectedStartupLostMasterStaysRunningWithoutProof(t *testing.T) {
106	for _, errno := range []error{unix.EACCES, unix.EPERM} {
107		t.Run(errno.Error(), func(t *testing.T) {
108			o, k, _, _ := testOutputMu(t, nil, errno)
109			k.EXPECT().createBlob(mock.Anything).Return(99, nil).Maybe()
110			k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
111			events, done, cancel := runProtectedStartup(t, o, k, func() (*os.File, error) { return nil, nil })
112			defer cancel()
113			select {
114			case err := <-done:
115				t.Fatalf("output stopped on lost master: %v", err)
116			case ev := <-events:
117				if _, ok := ev.(ports.SecurityOutputProof); ok {
118					t.Fatal("proof after lost master")
119				}
120			case <-time.After(300 * time.Millisecond):
121			}
122			cancel()
123			if err := <-done; err != nil {
124				t.Fatalf("output stopped: %v", err)
125			}
126			if o.securityPrepared || !o.protected {
127				t.Fatalf("prepared=%v protected=%v", o.securityPrepared, o.protected)
128			}
129		})
130	}
131}
132
133// Startup failures of detachment, mode blob or clear fence keep
134// the output and recover through the bounded retry; proof only after success.
135func TestProtectedStartupRecoverableFailuresRetry(t *testing.T) {
136	for _, stage := range []string{"detach", "blob", "fence"} {
137		t.Run(stage, func(t *testing.T) {
138			o, k, _, _ := testOutputMu(t)
139			var failures, blobs, objs, renders atomic.Int32
140			switch stage {
141			case "detach":
142				o.stray = []*plane{{id: 53, props: planeProps}}
143				k.EXPECT().objProps(uint32(53), uint32(objPlane)).RunAndReturn(func(uint32, uint32) (map[string][2]uint64, error) {
144					// 1: test modeset request (error ignored); 2: first detach.
145					if objs.Add(1) == 2 {
146						failures.Add(1)
147						return nil, unix.EIO
148					}
149					return map[string][2]uint64{"CRTC_ID": {pCrtcID, 0}}, nil
150				}).Maybe()
151				k.EXPECT().createBlob(mock.Anything).Return(99, nil).Maybe()
152			case "blob":
153				k.EXPECT().createBlob(mock.Anything).RunAndReturn(func([]byte) (uint32, error) {
154					if blobs.Add(1) == 2 { // 1: test modeset; 2: activation
155						failures.Add(1)
156						return 0, unix.ENOMEM
157					}
158					return 99, nil
159				}).Maybe()
160			case "fence":
161				k.EXPECT().createBlob(mock.Anything).Return(99, nil).Maybe()
162			}
163			k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
164			events, done, cancel := runProtectedStartup(t, o, k, func() (*os.File, error) {
165				// Renders 1-2 initialise the images; 3 is the first protected clear.
166				if stage != "fence" || renders.Add(1) != 3 {
167					return nil, nil
168				}
169				failures.Add(1)
170				f, w := protectionFence(t, false)
171				w.Close() // HUP without POLLIN is an error, not completion
172				return f, nil
173			})
174			defer cancel()
175			waitProof(t, events, done)
176			if failures.Load() == 0 {
177				t.Fatal("failure was not injected before proof")
178			}
179			cancel()
180			if err := <-done; err != nil {
181				t.Fatalf("output stopped: %v", err)
182			}
183		})
184	}
185}
186
187// Recoverable preparation failures are retryable protected errors
188// handled by commitFailed with backoff; a renderer error stays fatal.
189func TestProtectedPreparationFailuresRetryableRenderFatal(t *testing.T) {
190	boom := errors.New("render broke")
191	for _, stage := range []string{"stray-read", "stray-missing", "blob", "fence", "render"} {
192		t.Run(stage, func(t *testing.T) {
193			o, k, _ := testOutput(t)
194			r := portsmocks.NewMockRenderer(t)
195			switch stage {
196			case "stray-read", "stray-missing":
197				o.stray = []*plane{{id: 53, props: planeProps}}
198				var err error
199				props := map[string][2]uint64{}
200				if stage == "stray-read" {
201					err = unix.EIO
202				}
203				k.EXPECT().objProps(uint32(53), uint32(objPlane)).Return(props, err).Once()
204			case "blob":
205				k.EXPECT().createBlob(mock.Anything).Return(0, unix.ENOMEM).Once()
206				r.EXPECT().UseTarget(0).Return().Once()
207				r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
208			case "fence":
209				f, w := protectionFence(t, false)
210				w.Close()
211				r.EXPECT().UseTarget(0).Return().Once()
212				r.EXPECT().Render(mock.Anything, mock.Anything).Return(f, nil).Once()
213			case "render":
214				r.EXPECT().UseTarget(0).Return().Once()
215				r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, boom).Once()
216			}
217			o.protected = true
218			err := o.protectedModeset(context.Background(), r)
219			if err == nil {
220				t.Fatal("failure reported success")
221			}
222			enabled := true
223			handled := o.commitFailed(err, &enabled)
224			if stage == "render" {
225				if retryableProtected(err) || handled {
226					t.Fatalf("renderer error must stay fatal: retryable=%v handled=%v", retryableProtected(err), handled)
227				}
228				return
229			}
230			if !retryableProtected(err) || !handled || !enabled || !o.protectBackoffActive() || o.securityPrepared {
231				t.Fatalf("not retried with backoff: %v", err)
232			}
233		})
234	}
235}
236
237// A protected EBUSY/lost-master/detach error from showImages is a
238// protected KMS error: it never triggers the HDR->SDR fallback or hdrFailed.
239func TestProtectedShowImagesHDRKeepsHDRForKMSErrors(t *testing.T) {
240	for _, errno := range []error{unix.EBUSY, unix.EACCES, unix.EPERM, unix.EINVAL, nil} {
241		name := "detach"
242		if errno != nil {
243			name = errno.Error()
244		}
245		t.Run(name, func(t *testing.T) {
246			var errs []error
247			if errno != nil {
248				errs = []error{nil, errno} // HDR test commit, then the disable
249			}
250			o, k, _ := testOutput(t, errs...)
251			o.cursor = nil
252			o.protected = true
253			o.hdr.cap = hdrCapability{Capable: true}
254			o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 203}
255			o.hdr.props = connectorHDRProps{Metadata: 100, Colorspace: 101, MaxBPC: 102, BT2020Value: 7, HasDefault: true, MaxBPCValue: 8}
256			o.primary.formats = []ports.DMABufFormat{{Format: fourccXR30, Modifier: 0}}
257			if errno == nil {
258				o.primary.props = map[string]uint32{"CRTC_ID": pCrtcID} // incomplete: detach fails
259			}
260			r := portsmocks.NewMockRenderer(t)
261			buf := func() ports.DMABuf {
262				f, w := protectionFence(t, false)
263				w.Close()
264				return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
265			}
266			r.EXPECT().SetHDR(float64(203)).Return().Once()
267			r.EXPECT().UseTarget(mock.Anything).Return().Maybe()
268			r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Maybe()
269			r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
270			k.EXPECT().addFB(mock.Anything, uint32(fourccXR30)).Return(70, nil).Maybe()
271			k.EXPECT().createBlob(mock.Anything).Return(321, nil).Maybe()
272			k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
273			err := o.showImages(r, imagesDriver, nil)
274			if err == nil {
275				t.Fatal("protected KMS failure reported success")
276			}
277			enabled := true
278			lost := errno != nil && lostMaster(errno)
279			if !o.commitFailed(err, &enabled) {
280				t.Fatalf("protected failure not handled: %v", err)
281			}
282			if o.hdr.failed || !o.hdr.on {
283				t.Fatalf("HDR fell back: failed=%v on=%v", o.hdr.failed, o.hdr.on)
284			}
285			if lost == enabled {
286				t.Fatalf("lost master=%v enabled=%v", lost, enabled)
287			}
288			if !o.protected || o.securityPrepared {
289				t.Fatal("protection dropped or certified")
290			}
291		})
292	}
293}

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 25

Tags 10

↑↓ moveenter openesc close