1package wayland
2
3import (
4 "context"
5
6 "github.com/bnema/go-wayland-bindings/server/extsessionlock"
7 "github.com/bnema/go-wayland-bindings/server/wayland"
8 "github.com/bnema/neferwl/internal/ports"
9 "github.com/bnema/purego-libwayland/server"
10)
11
12// sessionLock is owned by the display goroutine. Resource destruction removes
13// its owner, never the protection gate. An ordinary new owner may take over.
14type sessionLock struct {
15 s *Server
16 res *extsessionlock.ExtSessionLockV1
17 state ports.SecurityState
18 locked bool
19 surfaces map[*output]*lockSurface
20}
21
22type sessionLockManager struct{ s *Server }
23
24// registerSessionLock is enabled only when the display and backend share
25// protection control and authoritative, non-dropping proof channels.
26func registerSessionLock(d *server.Display, s *Server) error {
27 return extsessionlock.NewExtSessionLockManagerV1Global(d, 1, func(c server.Client, version, id uint32) {
28 _, _ = extsessionlock.NewExtSessionLockManagerV1(c, int32(version), id, sessionLockManager{s})
29 })
30}
31func (sessionLockManager) Destroy(*extsessionlock.ExtSessionLockManagerV1) {}
32
33func (m sessionLockManager) Lock(r *extsessionlock.ExtSessionLockManagerV1, id uint32) {
34 s := m.s
35 l := &sessionLock{s: s, surfaces: make(map[*output]*lockSurface)}
36 res, err := extsessionlock.NewExtSessionLockV1(r.Client(), r.Version(), id, l)
37 if err != nil {
38 return
39 }
40 l.res = res
41 if s.securityController == nil || s.channels.SecurityChanges == nil || s.channels.SecurityEvents == nil || s.sessionLock != nil || len(s.securityPending) >= maxSecurityPending {
42 res.SendFinished()
43 return
44 }
45 // Engage before queueing any core event. No display-owned protocol
46 // admission can observe the old policy after this point.
47 state, err := s.securityController.Engage()
48 if err != nil {
49 res.SendFinished()
50 return
51 }
52 l.state = state
53 s.sessionLock = l
54 s.lockReadiness.Begin(state.Generation)
55 res.OnDestroy = l.orphan
56 s.resetProtectionInput()
57 s.endCaptureSessions()
58 for _, lease := range s.activeLeases {
59 lease.destroy()
60 }
61 // Preserve card tombstones until late replies are consumed/revoked, but
62 // revoke their protocol authority across both acquire and release.
63 for id, lease := range s.pendingLeases {
64 lease.finish()
65 delete(s.pendingLeases, id)
66 }
67 s.updateWorkspaceManagers(s.workspaceSnapshot)
68 s.emit(ports.SessionLockChanged{State: state})
69 s.requestBackendSecurity(state)
70 s.maybeLocked()
71}
72
73func (l *sessionLock) Destroy(r *extsessionlock.ExtSessionLockV1) {
74 if l.locked {
75 r.PostError(uint32(extsessionlock.ExtSessionLockV1ErrorInvalidDestroy), "destroy while locked")
76 }
77}
78func (l *sessionLock) UnlockAndDestroy(r *extsessionlock.ExtSessionLockV1) {
79 s := l.s
80 if !l.locked || s.sessionLock != l {
81 r.PostError(uint32(extsessionlock.ExtSessionLockV1ErrorInvalidUnlock), "unlock before protection confirmation")
82 return
83 }
84 state, err := s.securityController.Release(l.state.Generation)
85 if err != nil {
86 r.PostError(uint32(extsessionlock.ExtSessionLockV1ErrorInvalidUnlock), "protection release rejected")
87 return
88 }
89 // Clear input before any desktop focus enter; no password held keys.
90 s.resetProtectionInput()
91 s.lockReadiness.End(l.state.Generation)
92 s.sessionLock = nil
93 s.refreshToplevels()
94 s.castInventoryChanged() // the windows are listed again
95 s.updateWorkspaceManagers(s.workspaceSnapshot)
96 for _, surface := range l.surfaces {
97 surface.unmap()
98 }
99 s.emit(ports.SessionLockChanged{State: state})
100 s.requestBackendSecurity(state)
101}
102
103func (l *sessionLock) GetLockSurface(r *extsessionlock.ExtSessionLockV1, id uint32, wl *wayland.Surface, out *wayland.Output) {
104 s := l.s
105 if s.sessionLock != l {
106 _, _ = extsessionlock.NewExtSessionLockSurfaceV1(r.Client(), r.Version(), id, refusedLockSurface{})
107 return
108 }
109 surface, output := s.surfaceOf(wl), s.outputOf(out)
110 if surface == nil || output == nil || wl == nil || out == nil || wl.Client() != r.Client() || out.Client() != r.Client() {
111 r.PostError(uint32(extsessionlock.ExtSessionLockV1ErrorRole), "lock surface and output must belong to lock owner")
112 return
113 }
114 if _, exists := l.surfaces[output]; output != nil && exists {
115 r.PostError(uint32(extsessionlock.ExtSessionLockV1ErrorDuplicateOutput), "duplicate lock output")
116 return
117 }
118 ls, err := s.newLockSurface(r, id, surface, output, func(*lockSurface) { s.lockSurfaceChanged() })
119 if err != nil || ls == nil {
120 return
121 }
122 l.surfaces[output] = ls
123 s.lockSurfaceChanged()
124}
125
126func (l *sessionLock) orphan() {
127 s := l.s
128 if s.sessionLock != l {
129 return
130 }
131 // Never release or send finished merely because a client died.
132 s.resetProtectionInput()
133 s.sessionLock = nil
134 for _, surface := range l.surfaces {
135 surface.unmap()
136 }
137 s.emit(ports.SessionLockChanged{State: l.state})
138}
139
140func (s *Server) resetProtectionInput() {
141 s.cursorOverride = ""
142 s.dndCancelAll()
143 s.changeFocus(0)
144 s.changePointerFocus(0, 0, 0)
145 s.deactivateText()
146 s.deactivateConstraint()
147 clear(s.seat.heldKeys)
148 clear(s.seat.grabKeys)
149 s.seat.modState = ports.ModState{}
150 s.seat.press, s.seat.pressClient = 0, server.Client{}
151 s.seat.grabSerial, s.seat.grabButton = 0, 0
152 s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}
153 s.cursorSurface = nil
154}
155
156// maxSecurityPending bounds takeover churn while the backend is stalled.
157// Acquire checks this bound before engaging; release retains a reserved slot.
158const maxSecurityPending = 16
159
160func (s *Server) requestBackendSecurity(state ports.SecurityState) {
161 s.securityPending = append(s.securityPending, state)
162 select {
163 case s.securityWake <- struct{}{}:
164 default:
165 }
166}
167
168// forwardBackendSecurity transfers immutable states without blocking display
169// dispatch. Queue mutation is always on the display owner, not this goroutine.
170func (s *Server) forwardBackendSecurity(ctx context.Context) {
171 for {
172 var state ports.SecurityState
173 var pending bool
174 if !s.display.Do(func() {
175 if len(s.securityPending) != 0 {
176 state, pending = s.securityPending[0], true
177 }
178 }) {
179 return
180 }
181 if !pending {
182 select {
183 case <-ctx.Done():
184 return
185 case <-s.display.Stopped():
186 return
187 case <-s.securityWake:
188 continue
189 }
190 }
191 select {
192 case <-ctx.Done():
193 return
194 case <-s.display.Stopped():
195 return
196 case s.channels.SecurityChanges <- state:
197 }
198 if !s.display.Do(func() {
199 // Only this sender pops; appending on display preserves the head.
200 if len(s.securityPending) != 0 && s.securityPending[0] == state {
201 s.securityPending[0] = ports.SecurityState{}
202 s.securityPending = s.securityPending[1:]
203 }
204 }) {
205 return
206 }
207 }
208}
209
210func (s *Server) forwardSecurityEvents(ctx context.Context) {
211 for {
212 select {
213 case <-ctx.Done():
214 return
215 case <-s.display.Stopped():
216 return
217 case event, ok := <-s.channels.SecurityEvents:
218 if !ok {
219 return
220 }
221 if !s.display.Do(func() { s.applySecurityEvent(event) }) {
222 return
223 }
224 }
225 }
226}
227
228func (s *Server) applySecurityEvent(event ports.SecurityBackendEvent) {
229 switch v := event.(type) {
230 case ports.SecurityOutputAdded:
231 s.lockReadiness.Add(v.Instance)
232 case ports.SecurityOutputRemoved:
233 s.lockReadiness.Remove(v.Instance)
234 case ports.SecurityOutputProof:
235 s.lockReadiness.Record(v.Proof)
236 case ports.SecurityOutputInvalidated:
237 s.lockReadiness.Invalidate(v.Generation, v.Instance)
238 case ports.SecurityBackendBarrier:
239 if v.Err == nil {
240 s.lockReadiness.BackendBarrier(v.Generation)
241 } else {
242 s.log.Error().Err(v.Err).Msg("session security backend barrier failed")
243 }
244 }
245 s.maybeLocked()
246}
247
248func (s *Server) lockInputTarget(id ports.WindowID) bool {
249 l := s.sessionLock
250 if l == nil || !l.locked {
251 return false
252 }
253 surface := s.lockSurfaces[id]
254 if surface == nil || !surface.mapped || surface.closed || !surface.resource.Alive() {
255 return false
256 }
257 for _, owned := range l.surfaces {
258 if surface == owned {
259 return true
260 }
261 }
262 return false
263}
264
265func (s *Server) maybeLocked() {
266 l := s.sessionLock
267 if l == nil || l.locked || !l.res.Alive() {
268 return
269 }
270 if len(s.captureInflight) == 0 {
271 s.lockReadiness.CaptureBarrier(l.state.Generation)
272 }
273 if !s.lockReadiness.Ready() {
274 return
275 }
276 l.locked = true
277 l.res.SendLocked()
278 s.lockSurfaceChanged()
279}