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