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 "golang.org/x/sys/unix"
14)
15
16func TestSecurityUnlockedResumeUsesOwnerRetry(t *testing.T) {
17 for _, stage := range []string{"engage-during-request", "master-loss"} {
18 t.Run(stage, func(t *testing.T) {
19 var state atomic.Uint64
20 var rule func(*atomicReq, uint32) error
21 if stage == "master-loss" {
22 // The lock engages as the kernel refuses the first enable for
23 // lack of master: the retry must be a protected modeset.
24 refused := false
25 rule = func(*atomicReq, uint32) error {
26 if refused {
27 return nil
28 }
29 refused = true
30 state.Store(3)
31 return unix.EACCES
32 }
33 }
34 o, k, commits, mu := testOutputRules(t, rule)
35 o.cursor, o.tearing = nil, false
36 securityGate(t, o, &state)
37 events := make(chan ports.SecurityBackendEvent, 4)
38 o.SecurityEvents = events
39 active := make(chan bool, 2)
40 active <- false // retain existing images; first modeset is resume
41 wake := make(chan ports.SecurityState, 1)
42 o.SecurityChanges = wake
43 r := portsmocks.NewMockRenderer(t)
44 r.EXPECT().UseTarget(0).Return().Once()
45 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
46 if s.Seq != 0 || s.Background != "#000000" || len(s.Windows) != 0 {
47 t.Fatal("resume rendered desktop")
48 }
49 return nil, nil
50 }).Once()
51 r.EXPECT().Close().Return().Once()
52 var blobs atomic.Int32
53 k.EXPECT().createBlob(mock.Anything).RunAndReturn(func([]byte) (uint32, error) {
54 if blobs.Add(1) == 1 && stage == "engage-during-request" {
55 state.Store(3)
56 }
57 return 99, nil
58 })
59 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
60 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
61 defer cancel()
62 done := make(chan error, 1)
63 go func() {
64 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, nil, nil, nil, nil)
65 }()
66 active <- true
67 if stage == "master-loss" {
68 waitFor(t, func() bool { mu.Lock(); defer mu.Unlock(); return len(*commits) >= 1 })
69 wake <- ports.SecurityState{}
70 // The seat reported active before master came back: the owner
71 // retries the protected modeset itself, with no second seat
72 // event. The proof below must follow that successful retry.
73 }
74 for {
75 select {
76 case ev := <-events:
77 if p, ok := ev.(ports.SecurityOutputProof); ok {
78 if p.Proof.Generation != 1 || p.Proof.Instance != 77 {
79 t.Fatalf("proof %+v", p)
80 }
81 if stage == "master-loss" {
82 mu.Lock()
83 n := len(*commits)
84 mu.Unlock()
85 if n < 2 {
86 t.Fatal("master loss forged proof: no successful retry")
87 }
88 }
89 cancel()
90 if err := <-done; err != nil {
91 t.Fatal(err)
92 }
93 return
94 }
95 case err := <-done:
96 t.Fatalf("resume ended before fresh proof: %v", err)
97 case <-ctx.Done():
98 t.Fatal("missing fresh resume proof")
99 }
100 }
101 })
102 }
103}