main 5d6fa26

security_recovery_test.go RawHistoryFolder

1package drm
2
3import (
4	"context"
5	"os"
6	"sync/atomic"
7	"testing"
8	"time"
9
10	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
11	"github.com/bnema/neferwl/internal/ports"
12	"github.com/stretchr/testify/mock"
13)
14
15// A missing flip event is recoverable only after the real acquire fence
16// signals. Recovery revokes earlier proof before a fresh blocking black proof.
17func TestSecurityStuckRecoveryInvalidatesBeforeFreshProof(t *testing.T) {
18	o, k, commits, mu := testOutputMu(t)
19	o.fbs = [2]uint32{} // the first setup of a run: no image yet
20	o.cursor, o.tearing = nil, false
21	o.frame.stuckAfter = 40 * time.Millisecond
22	o.flipped = make(chan flipEvent) // the event is intentionally lost
23	var state atomic.Uint64
24	state.Store(3)
25	securityGate(t, o, &state)
26	events := make(chan ports.SecurityBackendEvent, 8)
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()
37	fence, signal := protectionFence(t, false)
38	var renderedOnce atomic.Bool
39	rendered := make(chan struct{}, 1)
40	r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
41		if s.Seq == 12 {
42			if renderedOnce.CompareAndSwap(false, true) {
43				select {
44				case rendered <- struct{}{}:
45				default:
46				}
47				return fence, nil
48			}
49			return nil, nil
50		}
51		if s.Background != "#000000" || s.Seq != 0 || len(s.Windows) != 0 {
52			t.Error("recovery did not compose exact black")
53		}
54		return nil, nil
55	})
56	r.EXPECT().Close().Return().Once()
57	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
58	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
59	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
60	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
61	defer cancel()
62	scenes := make(chan ports.Scene, 1)
63	done := make(chan error, 1)
64	go func() {
65		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, nil, discardSink(t), nil, nil)
66	}()
67	receive := func() ports.SecurityBackendEvent {
68		t.Helper()
69		select {
70		case event := <-events:
71			return event
72		case <-ctx.Done():
73			t.Fatal("missing recovery security event")
74			return nil
75		}
76	}
77	initial := receive()
78	if p, ok := initial.(ports.SecurityOutputProof); !ok || p.Proof != (ports.OutputProtection{Generation: 1, Instance: 77, Kind: ports.ProtectionProtectedFrame}) {
79		t.Fatalf("initial proof %+v", initial)
80	}
81	scenes <- ports.Scene{Seq: 12, Security: ports.SecurityState{Generation: 1, Protected: true}}
82	select {
83	case <-rendered:
84	case <-ctx.Done():
85		t.Fatal("protected frame not submitted")
86	}
87	waitFor(t, func() bool {
88		mu.Lock()
89		defer mu.Unlock()
90		for _, c := range *commits {
91			if c.user&3 == userFrame {
92				return true
93			}
94		}
95		return false
96	})
97	select {
98	case event := <-events:
99		t.Fatalf("unsignalled acquire fence forged recovery evidence: %+v", event)
100	case <-time.After(3 * o.frame.stuckAfter):
101	}
102	if _, err := signal.Write([]byte{1}); err != nil {
103		t.Fatal(err)
104	}
105	if event := receive(); event != (ports.SecurityOutputInvalidated{Generation: 1, Instance: 77}) {
106		t.Fatalf("fresh proof preceded recovery invalidation: %+v", event)
107	}
108	if event := receive(); event != initial {
109		t.Fatalf("fresh recovery proof changed lifetime/epoch: %+v", event)
110	}
111	cancel()
112	if err := <-done; err != nil {
113		t.Fatal(err)
114	}
115	mu.Lock()
116	defer mu.Unlock()
117	var frameIndex int
118	for i, c := range *commits {
119		if c.user&3 == userFrame {
120			frameIndex = i
121			break
122		}
123	}
124	if frameIndex == 0 || len(*commits) < frameIndex+3 {
125		t.Fatal("no actual blocking black recovery transaction")
126	}
127	checkProtectedDisable(t, (*commits)[frameIndex+1], tPrimary)
128	activate := (*commits)[frameIndex+2]
129	if active, ok := activate.req.value(tCrtc, pActive); !ok || active != 1 || activate.flags != atomicAllowModes {
130		t.Fatal("recovery proof did not follow blocking activation")
131	}
132}

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 23

Tags 10

↑↓ moveenter openesc close