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