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)
15
16func TestSecurityProtectedStartupBlackProofAndCaptureRejection(t *testing.T) {
17 o, k, commits, mu := testOutputMu(t)
18 o.cursor = nil
19 var state atomic.Uint64
20 state.Store(3)
21 securityGate(t, o, &state)
22 events := make(chan ports.SecurityBackendEvent, 2)
23 o.SecurityEvents = events
24 r := portsmocks.NewMockRenderer(t)
25 buf := func() ports.DMABuf {
26 f, w := protectionFence(t, false)
27 w.Close()
28 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
29 }
30 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
31 r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
32 r.EXPECT().UseTarget(mock.Anything).Return()
33 r.EXPECT().Render(ports.Scene{Background: "#000000"}, map[ports.WindowID]ports.SurfaceContent(nil)).Return(nil, nil).Times(3)
34 r.EXPECT().Close().Return().Once()
35 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
36 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
37 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
38 captures := make(chan ports.CaptureRequest, 1)
39 captured := make(chan ports.CaptureDone, 1)
40 scenes := make(chan ports.Scene, 1)
41 scenes <- ports.Scene{Seq: 99} // never render stale desktop after startup
42 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
43 defer cancel()
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, scenes, nil, nil, nil, captures, captured)
47 }()
48 select {
49 case ev := <-events:
50 if p, ok := ev.(ports.SecurityOutputProof); !ok || p.Proof.Generation != 1 || p.Proof.Instance != 77 || p.Proof.Kind != ports.ProtectionProtectedFrame {
51 t.Fatalf("proof %+v", ev)
52 }
53 case <-ctx.Done():
54 t.Fatal("no startup black proof")
55 }
56 mu.Lock()
57 for i, c := range *commits {
58 if c.flags&atomicTestOnly != 0 {
59 continue
60 }
61 active, _ := c.req.value(tCrtc, pActive)
62 if active != 0 && i != len(*commits)-1 {
63 t.Error("startup exposed an active target before protected transaction")
64 }
65 }
66 mu.Unlock()
67 f, w := protectionFence(t, false)
68 w.Close()
69 captures <- ports.CaptureRequest{ID: 9, Dst: ports.SHMBuffer{File: f}}
70 select {
71 case reply := <-captured:
72 if reply.ID != 9 || reply.Err == nil {
73 t.Fatalf("capture result %+v", reply)
74 }
75 case <-ctx.Done():
76 t.Fatal("protected capture not failed")
77 }
78 cancel()
79 if err := <-done; err != nil {
80 t.Fatal(err)
81 }
82}
83
84func TestSecurityStartupClearWaitUsesRunContext(t *testing.T) {
85 o, k, _ := testOutput(t)
86 o.cursor = nil
87 var state atomic.Uint64
88 state.Store(3)
89 securityGate(t, o, &state)
90 r := portsmocks.NewMockRenderer(t)
91 buf := func() ports.DMABuf {
92 f, w := protectionFence(t, false)
93 w.Close()
94 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
95 }
96 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
97 r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
98 r.EXPECT().ExportTargets(0, []uint64(nil), false).Return(nil, nil).Once()
99 r.EXPECT().UseTarget(0).Return().Once()
100 fence, _ := protectionFence(t, false)
101 ctx, cancel := context.WithCancel(context.Background())
102 defer cancel()
103 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
104 cancel()
105 return fence, nil
106 }).Once()
107 r.EXPECT().Close().Return().Once()
108 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
109 k.EXPECT().rmFB(mock.Anything).Return(nil)
110 err := o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, nil, nil, nil, nil, nil, nil)
111 if !errors.Is(err, context.Canceled) {
112 t.Fatalf("startup cancellation: %v", err)
113 }
114 if _, err := fence.Stat(); !errors.Is(err, os.ErrClosed) {
115 t.Fatal("startup fence leaked")
116 }
117}
118
119func TestSecurityWiredUnlockedCloseNeverRestoresSavedFB(t *testing.T) {
120 o, k, commits := testOutput(t)
121 var state atomic.Uint64
122 securityGate(t, o, &state)
123 o.saved = modeCrtc{crtcID: tCrtc, fbID: 123, modeValid: 1, mode: o.mode}
124 k.EXPECT().rmFB(mock.Anything).Return(nil)
125 o.Close() // no protected snapshot needed: saved restoration is never admitted
126 checkProtectedDisable(t, (*commits)[0], tPrimary, tCursor)
127}