v0.4.0 b6be3a8

security_test.go RawHistoryFolder

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	"golang.org/x/sys/unix"
15)
16
17// A generated mock through the real ports interface, using a coherent atomic
18// test state so transitions can race with the output owner without data races.
19func securityGate(t *testing.T, o *Output, state *atomic.Uint64) {
20	k := portsmocks.NewMockSessionSecurity(t)
21	k.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState {
22		v := state.Load()
23		return ports.SecurityState{Generation: ports.LockGeneration(v >> 1), Protected: v&1 != 0}
24	})
25	o.Security = k
26	o.Instance = 77
27	o.SecurityEvents = make(chan ports.SecurityBackendEvent, 8)
28}
29
30func TestSecurityPreparationProofAndFailure(t *testing.T) {
31	for _, stage := range []string{"success", "off", "disable", "render", "pending", "changed"} {
32		t.Run(stage, func(t *testing.T) {
33			var failures []error
34			if stage == "disable" {
35				failures = []error{unix.EACCES}
36			}
37			o, k, commits := testOutput(t, failures...)
38			var state atomic.Uint64
39			state.Store(3)
40			securityGate(t, o, &state)
41			events := make(chan ports.SecurityBackendEvent, 8)
42			o.SecurityEvents = events
43			r := portsmocks.NewMockRenderer(t)
44			if stage == "pending" {
45				f, _ := protectionFence(t, false)
46				o.frame.begin(pendingFrame{fences: []*os.File{f}}, time.Now())
47			} else if stage != "disable" {
48				r.EXPECT().UseTarget(0).Return().Once()
49				r.EXPECT().Render(ports.Scene{Background: "#000000"}, map[ports.WindowID]ports.SurfaceContent(nil)).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
50					if stage == "changed" {
51						state.Store(5)
52					}
53					if stage == "render" {
54						return nil, errors.New("render refused")
55					}
56					return nil, nil
57				}).Once()
58				if stage != "off" && stage != "render" {
59					k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
60				}
61			}
62			o.wantOff = stage == "off"
63			err := o.prepareSecurity(context.Background(), r)
64			wantErr := stage == "disable" || stage == "render" || stage == "pending"
65			if (err != nil) != wantErr {
66				t.Fatalf("result %v", err)
67			}
68			if stage == "success" || stage == "off" {
69				select {
70				case ev := <-events:
71					proof, ok := ev.(ports.SecurityOutputProof)
72					kind := ports.ProtectionProtectedFrame
73					if stage == "off" {
74						kind = ports.ProtectionInactiveOutput
75					}
76					if !ok || proof.Proof != (ports.OutputProtection{Generation: 1, Instance: 77, Kind: kind}) {
77						t.Fatalf("proof %+v", ev)
78					}
79				default:
80					t.Fatal("missing proof")
81				}
82			} else if len(events) != 0 || o.securityPrepared {
83				t.Fatal("failure or changed epoch certified protection")
84			}
85			if stage == "pending" && len(*commits) != 0 {
86				t.Fatal("pending client fence queued blocking KMS")
87			}
88		})
89	}
90}
91
92func TestSecurityRejectsStaleAcquireReleaseScenes(t *testing.T) {
93	o, _, commits := testOutput(t)
94	var state atomic.Uint64
95	securityGate(t, o, &state)
96	r := portsmocks.NewMockRenderer(t)
97	state.Store(3)
98	if _, err := o.submitFrame(context.Background(), r, ports.Scene{}, nil, nil, nil, nil); !errors.Is(err, errSecurityScene) {
99		t.Fatalf("old desktop admitted: %v", err)
100	}
101	state.Store(4)
102	if _, err := o.submitFrame(context.Background(), r, ports.Scene{Security: ports.SecurityState{Generation: 1, Protected: true}}, nil, nil, nil, nil); !errors.Is(err, errSecurityScene) {
103		t.Fatalf("old lock admitted on release: %v", err)
104	}
105	if len(*commits) != 0 {
106		t.Fatal("stale epoch committed")
107	}
108}
109
110func TestSecurityProtectedCompositionDetachesCursorAndOverlay(t *testing.T) {
111	o, _, commits := testOutput(t)
112	var state atomic.Uint64
113	state.Store(3)
114	securityGate(t, o, &state)
115	o.observeSecurity()
116	o.securityPrepared = true
117	o.cursor.image = true
118	o.cursor.Move(8, 8)
119	o.overlay = &plane{id: tOverlay, props: planeProps}
120	s, c := fullscreenScene()
121	s.Security = ports.SecurityState{Generation: 1, Protected: true}
122	s.CaptureScene = &ports.Scene{}
123	r := portsmocks.NewMockRenderer(t)
124	r.EXPECT().UseTarget(0).Return().Once()
125	r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(scene ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
126		if scene.CaptureScene != nil {
127			t.Fatal("hidden capture crossed protected path")
128		}
129		return nil, nil
130	}).Once()
131	direct, err := o.submitFrame(context.Background(), r, s, c, nil, nil, nil)
132	if err != nil || direct {
133		t.Fatalf("direct=%v err=%v", direct, err)
134	}
135	last := (*commits)[0]
136	if last.flags&flipAsyncFlag != 0 {
137		t.Fatal("protected tearing")
138	}
139	for _, plane := range []uint32{tCursor, tOverlay} {
140		if fb, _ := last.req.value(plane, pFB); fb != 0 {
141			t.Fatalf("desktop plane %d attached", plane)
142		}
143	}
144}
145
146func TestSecurityCloseReadsGateAfterOwnerStopped(t *testing.T) {
147	o, k, commits := testOutput(t)
148	var state atomic.Uint64
149	securityGate(t, o, &state)
150	o.saved = modeCrtc{crtcID: tCrtc, fbID: 123, modeValid: 1, mode: o.mode}
151	state.Store(3) // no wake or owner loop observes it
152	k.EXPECT().rmFB(mock.Anything).Return(nil)
153	o.Close()
154	checkProtectedDisable(t, (*commits)[0], tPrimary, tCursor)
155}
156
157func TestSecurityCommitStateReadsGateBeforeCursorCommit(t *testing.T) {
158	o, _, commits := testOutput(t)
159	var state atomic.Uint64
160	securityGate(t, o, &state)
161	o.cursor.image = true
162	o.cursor.Move(10, 10)
163	state.Store(3)
164	if err := o.commitState(true); !errors.Is(err, errSecurityScene) {
165		t.Fatalf("cursor result: %v", err)
166	}
167	if len(*commits) != 0 || !o.protected {
168		t.Fatal("cursor committed before owner observed lock")
169	}
170}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close