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}