security_exclude_fence_test.go RawHistoryFolder

1package headless
2
3import (
4	"context"
5	"errors"
6	"image"
7	"os"
8	"sync/atomic"
9	"testing"
10	"time"
11
12	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
13	"github.com/bnema/neferwl/internal/ports"
14	"github.com/stretchr/testify/mock"
15)
16
17func TestExcludeFenceRetainsReadsAcrossEngageAndOff(t *testing.T) {
18	for _, finish := range []string{"signal", "cancel", "render-error"} {
19		t.Run(finish, func(t *testing.T) {
20			admitted := ports.SecurityState{Generation: 2}
21			protected := ports.SecurityState{Generation: 3, Protected: true}
22			var epoch atomic.Value
23			epoch.Store(admitted)
24			gate := portsmocks.NewMockSessionSecurity(t)
25			gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) })
26			fence, signal, err := os.Pipe()
27			if err != nil {
28				t.Fatal(err)
29			}
30			defer signal.Close()
31			r := portsmocks.NewMockRenderer(t)
32			r.EXPECT().Close().Return().Once()
33			rendered := make(chan struct{}, 1)
34			releaseRender := make(chan struct{})
35			failure := errors.New("excluded render failed after submission")
36			r.EXPECT().Render(mock.MatchedBy(func(s ports.Scene) bool { return s.Security == admitted && s.Seq == 0 }), mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
37				rendered <- struct{}{}
38				<-releaseRender
39				if finish == "render-error" {
40					return fence, failure
41				}
42				return fence, nil
43			}).Once()
44			if finish == "signal" {
45				r.EXPECT().Render(mock.MatchedBy(func(s ports.Scene) bool { return s.Security == protected && s.Seq == 0 }), mock.Anything).Return(nil, nil).Once()
46			}
47			// Native capture and Pixels have no expectations; neither is admitted.
48			scenes := make(chan ports.Scene, 2)
49			contents := make(chan ports.SurfaceContent, 1)
50			incoming := make(chan ports.CaptureRequest)
51			presented := make(chan ports.OutputPresented, 8)
52			proofs := make(chan ports.SecurityBackendEvent, 2)
53			replies := make(chan ports.CaptureDone, 2)
54			changes := make(chan ports.SecurityState, 1)
55			ctx, cancel := context.WithCancel(context.Background())
56			defer cancel()
57			done := make(chan error, 1)
58			go func() {
59				done <- Run(ctx, Options{Security: gate, SecurityChanges: changes, SecurityEvents: proofs, Instance: 7, Width: 2, Height: 2, Presented: presented, Captured: replies, NewRenderer: func(int, int) (ports.Renderer, error) { return r, nil }}, scenes, contents, nil, incoming)
60			}()
61			dst := sessionCaptureFile(t)
62			incoming <- ports.CaptureRequest{ID: 99, Exclude: true, Session: 7, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Dst: dst}
63			contents <- ports.SurfaceContent{ID: 1, Seq: 8, SHM: &ports.SHMBuffer{Pool: 1}}
64			scene := sessionScene()
65			scene.Security = admitted
66			scene.Scale = 1
67			scene.OutputWidth = 2
68			scene.OutputHeight = 2
69			scenes <- scene
70			securityReceive(t, rendered)
71			// Prior non-render reports are outside this read lifetime; establish the
72			// cut at excluded GPU submission before exercising transition/off handling.
73			for len(presented) > 0 {
74				<-presented
75			}
76			epoch.Store(protected)
77			scenes <- ports.Scene{Security: protected, Off: true}
78			changes <- protected
79			close(releaseRender)
80			// A bounded observation interval catches the original immediate fence
81			// close/report path without assuming a polling schedule.
82			select {
83			case report := <-presented:
84				t.Fatalf("GPU reads reported before excluded fence: %+v", report)
85			case <-time.After(150 * time.Millisecond):
86			}
87			if _, err := fence.Stat(); err != nil {
88				t.Fatalf("unsignalled excluded read fence closed: %v", err)
89			}
90			select {
91			case proof := <-proofs:
92				t.Fatalf("protection path bypassed pending excluded reads: %+v", proof)
93			default:
94			}
95			if finish == "cancel" {
96				cancel()
97			} else {
98				if _, err := signal.Write([]byte{1}); err != nil {
99					t.Fatal(err)
100				}
101			}
102			if finish == "signal" {
103				securityReceive(t, proofs)
104				cancel()
105			}
106			runErr := securityReceive(t, done)
107			if finish == "render-error" {
108				if !errors.Is(runErr, failure) {
109					t.Fatal(runErr)
110				}
111			} else if runErr != nil {
112				t.Fatal(runErr)
113			}
114			if _, err := fence.Stat(); !errors.Is(err, os.ErrClosed) {
115				t.Fatalf("excluded fence leaked: %v", err)
116			}
117			reply := securityReceive(t, replies)
118			if reply.ID != 99 || reply.Err == nil {
119				t.Fatal(reply)
120			}
121			if _, err := dst.File.Stat(); !errors.Is(err, os.ErrClosed) {
122				t.Fatal("capture destination leaked")
123			}
124			select {
125			case extra := <-replies:
126				t.Fatalf("duplicate capture reply %+v", extra)
127			default:
128			}
129			if finish != "signal" {
130				select {
131				case proof := <-proofs:
132					t.Fatalf("cancel/error falsely proved protection: %+v", proof)
133				default:
134				}
135				select {
136				case report := <-presented:
137					t.Fatalf("cancel/error falsely reported reads: %+v", report)
138				default:
139				}
140			}
141		})
142	}
143}

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