1package core_test
2
3import (
4 "context"
5 "sync/atomic"
6 "testing"
7
8 "github.com/bnema/neferwl/internal/core"
9 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
10 "github.com/bnema/neferwl/internal/ports"
11)
12
13func TestSecurityOwnerAdversarialQueues(t *testing.T) {
14 var epoch atomic.Value
15 epoch.Store(ports.SecurityState{})
16 gate := portsmocks.NewMockSessionSecurity(t)
17 gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) })
18 client := make(chan ports.ClientEvent, 32)
19 input := make(chan ports.InputEvent, 32)
20 output := make(chan ports.OutputEvent, 8)
21 reload := make(chan ports.ConfigChanged, 8)
22 commands := make(chan ports.ClientCommand, 256)
23 scenes := make(chan []ports.Scene, 1)
24 spawn := make(chan ports.SpawnRequest, 8)
25 constraints := make(chan ports.PointerConstraint, 1)
26 cfg := scrollDefaults()
27 cfg.Terminal.Command = []string{"terminal"}
28 cfg.Terminal.AutoOpen = "off"
29 cfg.Binds = map[string]string{"Super+q": "quit"}
30 c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Config: reload, Commands: commands, Scenes: scenes, Spawn: spawn, Constraints: constraints}, core.Options{Security: gate, Terminal: true})
31 if err != nil {
32 t.Fatal(err)
33 }
34 ctx, cancel := context.WithCancel(context.Background())
35 done := make(chan error, 1)
36 go func() { done <- c.Run(ctx) }()
37 t.Cleanup(func() {
38 cancel()
39 if err := receive(t, done); err != nil {
40 t.Error(err)
41 }
42 })
43 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "A", Width: 300, Height: 200}}
44 scene(t, scenes)
45 client <- ports.WindowMapped{ID: 1}
46 scene(t, scenes)
47 // A pre-lock key is queued BEFORE any SessionLockChanged event. The gate
48 // must block it regardless of which channel select chooses next.
49 locked := ports.SecurityState{Generation: 1, Protected: true}
50 epoch.Store(locked)
51 input <- ports.SecurityInput{State: ports.SecurityState{}, Event: ports.KeyEvent{Keysym: "q", Mods: ports.ModSuper, Pressed: true}}
52 s := sceneMatch(t, scenes, func(s ports.Scene) bool { return s.Security == locked })
53 assertProtected(t, s)
54 if len(s.Windows) != 0 {
55 t.Fatal(s)
56 }
57 receive(t, constraints)
58 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "B", Width: 200, Height: 100}}
59 sceneMatch(t, scenes, func(s ports.Scene) bool { return s.Security == locked })
60 surfaces := []ports.LockSurfacePlacement{{ID: 10, Output: "A", Width: 300, Height: 200}, {ID: 20, Output: "B", Width: 200, Height: 100}}
61 client <- ports.SessionLockChanged{State: locked, Surfaces: surfaces}
62 s = sceneMatch(t, scenes, func(s ports.Scene) bool { return len(s.Windows) == 1 && s.Windows[0].ID == 10 })
63 assertProtected(t, s)
64 // Raw input is refused even in the initial unlocked epoch when wired;
65 // stale, raw, bind and swipe events must not activate desktop behavior.
66 input <- ports.KeyEvent{Keysym: "raw", Pressed: true}
67 input <- ports.SecurityInput{State: locked, Event: ports.KeyEvent{Keysym: "q", Mods: ports.ModSuper, Pressed: true}}
68 input <- ports.SecurityInput{State: locked, Event: ports.SwipeBegin{Fingers: 3}}
69 input <- ports.SecurityInput{State: locked, Event: ports.KeyEvent{Keysym: "password", Keycode: 30, Pressed: true}}
70 expectKey(t, commands, locked, 10, "password")
71 input <- ports.SecurityInput{State: locked, Event: ports.PointerMotion{X: 350, Y: 30}}
72 input <- ports.SecurityInput{State: locked, Event: ports.PointerButton{Button: 0x110, Pressed: true}}
73 input <- ports.SecurityInput{State: locked, Event: ports.KeyEvent{Keysym: "clicked", Pressed: true}}
74 expectKey(t, commands, locked, 20, "clicked")
75 // Stale surfaces cannot replace the current lock roles. Invalid logical
76 // dimensions and unknown outputs never become drawn/input targets.
77 client <- ports.SessionLockChanged{State: ports.SecurityState{}, Surfaces: []ports.LockSurfacePlacement{{ID: 1, Output: "A", Width: 300, Height: 200}}}
78 client <- ports.SessionLockChanged{State: locked, Surfaces: append(surfaces, ports.LockSurfacePlacement{ID: 99, Output: "unknown", Width: 300, Height: 200})}
79 s = sceneMatch(t, scenes, func(s ports.Scene) bool { return len(s.Windows) == 1 && s.Windows[0].ID == 10 })
80 assertProtected(t, s)
81 cfg.Terminal.AutoOpen = "always"
82 cfg.Startup = [][]string{{"must-not-start"}}
83 reload <- ports.ConfigChanged{Config: cfg}
84 s = scene(t, scenes)
85 assertProtected(t, s)
86 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "C", Width: 100, Height: 100}}
87 for _, s := range receive(t, scenes) {
88 assertProtected(t, s)
89 if s.Output == "C" && len(s.Windows) != 0 {
90 t.Fatal(s)
91 }
92 }
93 select {
94 case req := <-spawn:
95 t.Fatalf("protected spawn: %+v", req)
96 default:
97 }
98 // Unlock discards all presses/grabs, including a password press with no
99 // corresponding release until AFTER the transition.
100 unlocked := ports.SecurityState{Generation: 2}
101 epoch.Store(unlocked)
102 input <- ports.SecurityInput{State: locked, Event: ports.KeyEvent{Keysym: "stale-password", Pressed: true}}
103 input <- ports.SecurityInput{State: unlocked, Event: ports.KeyEvent{Keysym: "password", Keycode: 30, Pressed: false}}
104 input <- ports.SecurityInput{State: unlocked, Event: ports.KeyEvent{Keysym: "desktop", Pressed: true}}
105 expectKey(t, commands, unlocked, 1, "desktop")
106 s = sceneMatch(t, scenes, func(s ports.Scene) bool { return s.Security == unlocked })
107 if len(s.Windows) == 0 || s.Windows[0].ID != 1 {
108 t.Fatal(s)
109 }
110}
111
112func assertProtected(t *testing.T, s ports.Scene) {
113 t.Helper()
114 if !s.Security.Protected || s.Background != "#000000" || s.Capture != nil || s.CaptureScene != nil || len(s.Layers) != 0 || len(s.Separators) != 0 || s.Border.Width != 0 || s.Dim != 0 || s.WorkspaceClip != (ports.Rect{}) {
115 t.Fatalf("unsafe scene: %+v", s)
116 }
117 for _, w := range s.Windows {
118 if !w.Fullscreen || w.Hidden || w.Popup || w.Rect != (ports.Rect{W: s.OutputWidth, H: s.OutputHeight}) {
119 t.Fatalf("unsafe role: %+v", w)
120 }
121 }
122}
123
124func expectKey(t *testing.T, ch <-chan ports.ClientCommand, state ports.SecurityState, id ports.WindowID, name string) {
125 t.Helper()
126 for {
127 wrapped, ok := receive(t, ch).(ports.SecurityCommand)
128 if !ok {
129 t.Fatal("unwrapped command")
130 }
131 if k, ok := wrapped.Command.(ports.ForwardKey); ok {
132 if wrapped.State != state {
133 continue
134 } // older commands keep their epoch
135 if k.Key.Keysym == "raw" || k.Key.Keysym == "stale-password" || !k.Key.Pressed {
136 t.Fatalf("leaked input: %+v", k)
137 }
138 if k.Key.Keysym == name {
139 if k.ID != id {
140 t.Fatalf("key target %d want %d", k.ID, id)
141 }
142 return
143 }
144 }
145 }
146}