1package app
2
3import (
4 "context"
5 "errors"
6 "os"
7 "testing"
8 "time"
9
10 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
11 "github.com/bnema/neferwl/internal/ports"
12)
13
14func TestSecurityRegistrationBeforeStart(t *testing.T) {
15 ctx, cancel := context.WithCancel(context.Background())
16 defer cancel()
17 events := make(chan ports.SecurityBackendEvent)
18 set := newOutputSet(ctx, nil)
19 set.wireSecurity(outputChannels{securityEvents: events})
20 set.setSecurity(ports.SecurityState{Generation: 7, Protected: true})
21 started := make(chan ports.OutputInstance, 1)
22 result := make(chan bool, 1)
23 go func() {
24 result <- set.start(ctx, "DP-1", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, security <-chan ports.SecurityState, instance ports.OutputInstance) error {
25 state := <-security
26 if state != (ports.SecurityState{Generation: 7, Protected: true}) {
27 return errors.New("missing startup protection")
28 }
29 started <- instance
30 <-ctx.Done()
31 return nil
32 })
33 }()
34 select {
35 case <-started:
36 t.Fatal("owner ran before registration")
37 default:
38 }
39 var added ports.SecurityOutputAdded
40 select {
41 case event := <-events:
42 added = event.(ports.SecurityOutputAdded)
43 case <-time.After(2 * time.Second):
44 t.Fatal("registration missing")
45 }
46 if added.Instance == 0 || added.Output != "DP-1" {
47 t.Fatalf("registration: %+v", added)
48 }
49 if !<-result {
50 t.Fatal("start failed")
51 }
52 select {
53 case instance := <-started:
54 if instance != added.Instance {
55 t.Fatal("owner lifetime differs")
56 }
57 case <-time.After(2 * time.Second):
58 t.Fatal("owner missing startup protection")
59 }
60 if err := set.wait(); err != nil {
61 t.Fatal(err)
62 }
63}
64
65func TestSecurityReplacementAndStaleState(t *testing.T) {
66 ctx := context.Background()
67 events := make(chan ports.SecurityBackendEvent, 4)
68 set := newOutputSet(ctx, nil)
69 set.wireSecurity(outputChannels{securityEvents: events})
70 run := func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error {
71 <-ctx.Done()
72 return nil
73 }
74 set.start(ctx, "DP-1", run)
75 old := (<-events).(ports.SecurityOutputAdded)
76 set.outs["DP-1"].stop()
77 if err := set.finish("DP-1"); err != nil {
78 t.Fatal(err)
79 }
80 set.start(ctx, "DP-1", run)
81 replacement := (<-events).(ports.SecurityOutputAdded)
82 if replacement.Instance <= old.Instance {
83 t.Fatal("reused lifetime")
84 }
85 if set.stoppedCurrent(outputStopped{name: "DP-1", instance: old.Instance}) {
86 t.Fatal("old stop matches replacement")
87 }
88 set.setSecurity(ports.SecurityState{Generation: 3, Protected: true})
89 set.setSecurity(ports.SecurityState{Generation: 2})
90 if state := <-set.outs["DP-1"].security; state.Generation != 3 || !state.Protected {
91 t.Fatalf("stale transition: %+v", state)
92 }
93 set.setSecurity(ports.SecurityState{Generation: 4})
94 if state := <-set.outs["DP-1"].security; state.Generation != 4 || state.Protected {
95 t.Fatalf("unlock not forwarded: %+v", state)
96 }
97 if err := set.wait(); err != nil {
98 t.Fatal(err)
99 }
100 select {
101 case ev := <-events:
102 t.Fatalf("stop emitted false removal: %+v", ev)
103 default:
104 }
105}
106
107func TestGuardedLeaseRejectsProtected(t *testing.T) {
108 gate := portsmocks.NewMockSessionSecurity(t)
109 gate.EXPECT().Snapshot().Return(ports.SecurityState{Generation: 1, Protected: true}).Once()
110 card := newMocksecurityLeaseCard(t)
111 fd, id, err := guardedLease(card, gate, []string{"DP-1"})
112 if fd != nil || id != 0 || err == nil {
113 t.Fatalf("protected lease: %v %d %v", fd, id, err)
114 }
115}
116
117func TestGuardedLeaseCrossingTransition(t *testing.T) {
118 for _, revokeErr := range []error{nil, errors.New("revoke failed")} {
119 t.Run(map[bool]string{false: "revoked", true: "failed"}[revokeErr != nil], func(t *testing.T) {
120 fd, err := os.CreateTemp(t.TempDir(), "lease")
121 if err != nil {
122 t.Fatal(err)
123 }
124 gate := portsmocks.NewMockSessionSecurity(t)
125 gate.EXPECT().Snapshot().Return(ports.SecurityState{}).Once()
126 gate.EXPECT().Snapshot().Return(ports.SecurityState{Generation: 1, Protected: true}).Once()
127 card := newMocksecurityLeaseCard(t)
128 card.EXPECT().Lease([]string{"DP-1"}).Return(fd, uint32(12), nil).Once()
129 card.EXPECT().Revoke(uint32(12)).Return(revokeErr).Once()
130 got, id, err := guardedLease(card, gate, []string{"DP-1"})
131 if got != nil || id != 0 || err == nil {
132 t.Fatalf("late grant escaped: %v %d %v", got, id, err)
133 }
134 if _, statErr := fd.Stat(); !errors.Is(statErr, os.ErrClosed) {
135 t.Fatalf("FD not closed: %v", statErr)
136 }
137 if revokeErr != nil && !errors.Is(err, revokeErr) {
138 t.Fatal("revoke failure lost")
139 }
140 })
141 }
142}
143
144func TestSecurityRevokesAllLeasesAndRetainsFailures(t *testing.T) {
145 card := newMocksecurityLeaseCard(t)
146 card.EXPECT().LeaseIDs().Return([]uint32{1, 2}).Once()
147 card.EXPECT().LeaseIDs().Return([]uint32{2}).Once()
148 card.EXPECT().Revoke(uint32(1)).Return(nil).Once()
149 failed := errors.New("native revoke failure")
150 card.EXPECT().Revoke(uint32(2)).Return(failed).Once()
151 card.EXPECT().Path().Return("card0")
152 var finished []uint32
153 err := revokeSecurityLeases(card, func(id uint32) { finished = append(finished, id) })
154 if !errors.Is(err, failed) || len(finished) != 1 || finished[0] != 1 {
155 t.Fatalf("revocation: %v finished %v", err, finished)
156 }
157 card.EXPECT().LeaseIDs().Return([]uint32{2}).Once()
158 card.EXPECT().LeaseIDs().Return([]uint32{}).Once()
159 card.EXPECT().Revoke(uint32(2)).Return(nil).Once()
160 if err := revokeSecurityLeases(card, func(id uint32) {}); err != nil {
161 t.Fatal(err)
162 }
163}
164
165func TestSecurityInstanceExhaustion(t *testing.T) {
166 set := newOutputSet(context.Background(), nil)
167 events := make(chan ports.SecurityBackendEvent, 2)
168 set.wireSecurity(outputChannels{securityEvents: events})
169 set.nextInstance = ^ports.OutputInstance(0) - 1
170 run := func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error {
171 <-ctx.Done()
172 return nil
173 }
174 if !set.start(context.Background(), "last", run) {
175 t.Fatal("last valid lifetime refused")
176 }
177 if added := (<-events).(ports.SecurityOutputAdded); added.Instance != ^ports.OutputInstance(0) {
178 t.Fatalf("last instance: %+v", added)
179 }
180 for range 2 {
181 if set.start(context.Background(), "overflow", run) {
182 t.Fatal("exhausted counter launched owner")
183 }
184 }
185 if set.nextInstance != ^ports.OutputInstance(0) || set.startErr == nil {
186 t.Fatal("counter wrapped or error missing")
187 }
188 select {
189 case ev := <-events:
190 t.Fatalf("registered overflow: %+v", ev)
191 default:
192 }
193 if err := set.wait(); err == nil {
194 t.Fatal("exhaustion not returned")
195 }
196}
197
198// The shared display event channel and an output content queue can both be
199// saturated. Routing must retain the next content without pinning the backend
200// outside its select; a lease barrier can then finish before either is drained.
201func TestSecurityFullEventsAndContentsAllowLeaseBarrier(t *testing.T) {
202 ctx, cancel := context.WithCancel(context.Background())
203 defer cancel()
204 set := newOutputSet(ctx, nil)
205 events := make(chan ports.SecurityBackendEvent, 1)
206 set.wireSecurity(outputChannels{securityEvents: events})
207 var ownerEvents chan<- ports.SecurityBackendEvent
208 set.start(ctx, "DP-1", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error {
209 <-ctx.Done()
210 return nil
211 }, &ownerEvents)
212 <-events // ordered registration
213 events <- ports.SecurityBackendBarrier{Generation: 99} // blocked display
214 r := set.outs["DP-1"]
215 for i := range cap(r.contents) {
216 r.contents <- ports.SurfaceContent{ID: ports.WindowID(i + 1)}
217 }
218 first := ports.SecurityOutputInvalidated{Generation: 1, Instance: r.instance}
219 second := ports.SecurityOutputProof{Proof: ports.OutputProtection{Generation: 1, Instance: r.instance, Kind: ports.ProtectionProtectedFrame}}
220 ownerEvents <- first
221 ownerEvents <- second
222 card := newMocksecurityLeaseCard(t)
223 card.EXPECT().LeaseIDs().Return([]uint32{9}).Once()
224 card.EXPECT().LeaseIDs().Return([]uint32{}).Once()
225 card.EXPECT().Revoke(uint32(9)).Return(nil).Once()
226 progressed := make(chan error, 1)
227 go func() {
228 set.content(ports.SurfaceContent{ID: 70})
229 set.setSecurity(ports.SecurityState{Generation: 1, Protected: true})
230 err := revokeSecurityLeases(card, func(uint32) {})
231 set.securityEvent(ports.SecurityBackendBarrier{Generation: 1, Err: err})
232 progressed <- err
233 }()
234 select {
235 case err := <-progressed:
236 if err != nil {
237 t.Fatal(err)
238 }
239 case <-time.After(time.Second):
240 t.Fatal("content/event backpressure blocked lease transition")
241 }
242 incoming := make(chan ports.SurfaceContent)
243 contentIn, output, next, owner := set.contentOut(incoming)
244 if contentIn != nil || output != r.contents || owner != r || next.ID != 70 || len(r.pending) != 1 {
245 t.Fatal("pending content not bounded/preserved")
246 }
247 for range cap(r.contents) {
248 <-r.contents
249 }
250 output <- next
251 owner.contentSent()
252 if got := <-r.contents; got.ID != 70 {
253 t.Fatal("lost pending content")
254 }
255 if contentIn, _, _, _ := set.contentOut(incoming); contentIn != incoming {
256 t.Fatal("content intake stayed disabled")
257 }
258 <-events // unblock display
259 var ownerSequence []ports.SecurityBackendEvent
260 for range 3 {
261 select {
262 case ev := <-events:
263 switch ev.(type) {
264 case ports.SecurityOutputInvalidated, ports.SecurityOutputProof:
265 ownerSequence = append(ownerSequence, ev)
266 }
267 case <-time.After(time.Second):
268 t.Fatal("lossless forwarder lost event")
269 }
270 }
271 if len(ownerSequence) != 2 || ownerSequence[0] != first || ownerSequence[1] != second {
272 t.Fatalf("owner order lost: %+v", ownerSequence)
273 }
274 if err := set.wait(); err != nil {
275 t.Fatal(err)
276 }
277}
278
279func TestSecurityLargeReplayDoesNotBlockStart(t *testing.T) {
280 set := newOutputSet(context.Background(), nil)
281 for i := range 100 {
282 set.content(ports.SurfaceContent{ID: ports.WindowID(i + 1), SHM: &ports.SHMBuffer{}})
283 }
284 if !set.start(context.Background(), "A", func(ctx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, _ ports.OutputInstance) error {
285 <-ctx.Done()
286 return nil
287 }) {
288 t.Fatal("start failed")
289 }
290 r := set.outs["A"]
291 if len(r.contents) != cap(r.contents) || len(r.pending) != 100-cap(r.contents) {
292 t.Fatal("replay not preserved")
293 }
294 seen := map[ports.WindowID]bool{}
295 for len(seen) != 100 {
296 select {
297 case c := <-r.contents:
298 if seen[c.ID] {
299 t.Fatal("duplicate replay")
300 }
301 seen[c.ID] = true
302 default:
303 _, out, c, owner := set.contentOut(nil)
304 if out == nil {
305 t.Fatal("replay lost")
306 }
307 out <- c
308 owner.contentSent()
309 }
310 }
311 if len(r.pending) != 0 {
312 t.Fatal("replay remains")
313 }
314 if err := set.wait(); err != nil {
315 t.Fatal(err)
316 }
317}