1package drm
2
3import (
4 "context"
5 "errors"
6 "os"
7 "sync/atomic"
8 "testing"
9
10 "github.com/bnema/neferwl/internal/adapters/capture"
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 TestSecurityEngageDuringExcludeRenderRejectsCaptureTransfer(t *testing.T) {
17 o, _, commits := testOutput(t)
18 o.cursor = nil
19 var state atomic.Uint64
20 securityGate(t, o, &state)
21 r := portsmocks.NewMockRenderer(t)
22 f, _ := protectionFence(t, false)
23 r.EXPECT().UseTarget(0).Return().Once()
24 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
25 state.Store(3)
26 return f, nil
27 }).Once()
28 replies := make(chan ports.CaptureDone, 2)
29 pipeline := capture.NewPipeline(context.Background(), replies)
30 requests := []ports.CaptureRequest{sessionRequest(t, 1, true), sessionRequest(t, 2, false)}
31 files := []*os.File{requests[0].Dst.File, requests[1].Dst.File}
32 _, err := o.submitFrame(context.Background(), r, drmSessionScene(), nil, nil, requests, pipeline)
33 if !errors.Is(err, errSecurityScene) || len(*commits) != 0 {
34 t.Fatalf("capture admission result: %v", err)
35 }
36 if len(o.readFences) != 1 || o.readDone() {
37 t.Fatal("rejected excluded render dropped GPU reads")
38 }
39 if _, err := f.Stat(); !errors.Is(err, os.ErrClosed) {
40 t.Fatal("returned excluded fence not closed")
41 }
42 // Match Run's remaining-credit rejection: nothing was transferred to the
43 // worker, each request is failed once, and every owned FD is closed.
44 for _, q := range requests {
45 if capture.Handed(q) {
46 t.Fatal("obsolete capture transferred credit")
47 }
48 capture.Fail(context.Background(), q, err, replies)
49 }
50 pipeline.Close(r)
51 r.AssertNotCalled(t, "BeginCapture")
52 if len(replies) != 2 {
53 t.Fatalf("capture replies %d", len(replies))
54 }
55 for range 2 {
56 if reply := <-replies; !errors.Is(reply.Err, errSecurityScene) {
57 t.Fatalf("reply %+v", reply)
58 }
59 }
60 for _, file := range files {
61 if _, err := file.Stat(); !errors.Is(err, os.ErrClosed) {
62 t.Fatal("capture FD not closed")
63 }
64 }
65 o.dropRead()
66}
67
68func TestSecurityHiddenAdmissionRechecksBeforeNativeStart(t *testing.T) {
69 o, _, _ := testOutput(t)
70 o.cursor = nil
71 security := portsmocks.NewMockSessionSecurity(t)
72 security.EXPECT().Snapshot().Return(ports.SecurityState{}).Once()
73 security.EXPECT().Snapshot().Return(ports.SecurityState{Generation: 1, Protected: true})
74 o.Security = security
75 r := portsmocks.NewMockRenderer(t)
76 child := portsmocks.NewMockRenderer(t)
77 replies := make(chan ports.CaptureDone, 1)
78 pipeline := capture.NewPipeline(context.Background(), replies)
79 pipeline.EnableOffscreen(func(int, int) (ports.Renderer, error) {
80 t.Fatal("obsolete hidden capture created child")
81 return child, nil
82 })
83 s := drmSessionScene()
84 s.CaptureScene = &ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1}
85 requests := []ports.CaptureRequest{sessionRequest(t, 1, true)}
86 file := requests[0].Dst.File
87 _, err := o.submitFrame(context.Background(), r, s, nil, nil, requests, pipeline)
88 if !errors.Is(err, errSecurityScene) || capture.Handed(requests[0]) {
89 t.Fatalf("hidden admission result %v", err)
90 }
91 capture.Fail(context.Background(), requests[0], err, replies)
92 pipeline.Close(r)
93 if len(replies) != 1 || (<-replies).ID != 1 {
94 t.Fatal("hidden request credit not failed once")
95 }
96 if _, err := file.Stat(); !errors.Is(err, os.ErrClosed) {
97 t.Fatal("hidden FD not closed")
98 }
99 r.AssertNotCalled(t, "BeginCapture")
100 child.AssertNotCalled(t, "Render", mock.Anything, mock.Anything)
101 child.AssertNotCalled(t, "BeginCapture")
102}