1package wayland
2
3import (
4 "errors"
5 "strings"
6 "time"
7
8 "github.com/bnema/go-wayland-bindings/server/securitycontext"
9 "github.com/bnema/purego-libwayland/server"
10 "golang.org/x/sys/unix"
11)
12
13// wp_security_context_v1: a sandbox engine (Flatpak) asks the compositor to
14// serve a listening socket it made, and everything that connects to it is a
15// sandboxed client. Such a client is tagged when it is adopted and cannot
16// capture the screen (mayCapture): it asks the desktop portal, whose own
17// connection is not sandboxed. Sandboxed clients cannot nest a context.
18//
19// The display has no per-client global filter, so the capture globals stay
20// advertised to sandboxed clients and their requests are refused; so does
21// the manager global, which a sandboxed client may bind (binding every global
22// is common) but not use: create_listener is the nested protocol error. Only capture is
23// restricted for sandboxed clients so far: other privileged protocols
24// (virtual keyboard, output management, data control, foreign toplevels...)
25// are not.
26//
27// Each committed context runs one goroutine that polls its listening socket
28// (the event loop of the display is not reachable from Go), accepts, and hands
29// each connection to the display goroutine, which adopts it. The goroutine
30// stops when close_fd hangs up or when the compositor stops, and then closes
31// both descriptors. It keeps serving when the client that committed the
32// context goes away (the protocol says so).
33
34// sandbox is what a sandboxed client was committed with. Engine, app and
35// instance ids are the sandbox's own claims; they label logs and nothing else.
36type sandbox struct {
37 engine, appID, instanceID string
38}
39
40const (
41 maxSecurityMetadata = 1024 // bytes per metadata string
42 acceptRetry = 50 * time.Millisecond
43)
44
45func registerSecurityContext(d *server.Display, s *Server) error {
46 return securitycontext.NewWpSecurityContextManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
47 _, _ = securitycontext.NewWpSecurityContextManagerV1(c, int32(v), id, &securityManager{s: s})
48 })
49}
50
51type securityManager struct{ s *Server }
52
53func (*securityManager) Destroy(*securitycontext.WpSecurityContextManagerV1) {}
54
55func (m *securityManager) CreateListener(r *securitycontext.WpSecurityContextManagerV1, id uint32, listenFD, closeFD int) {
56 s := m.s
57 switch {
58 case s.sandboxed[r.Client()] != nil:
59 r.PostError(uint32(securitycontext.WpSecurityContextManagerV1ErrorNested), "nested security contexts are forbidden")
60 case !listeningUnixSocket(listenFD):
61 r.PostError(uint32(securitycontext.WpSecurityContextManagerV1ErrorInvalidListenFd), "listen_fd is not a listening unix stream socket")
62 default:
63 ctx := &securityContext{s: s, listenFD: listenFD, closeFD: closeFD}
64 res, err := securitycontext.NewWpSecurityContextV1(r.Client(), r.Version(), id, ctx)
65 if err == nil {
66 // The client went away, or destroyed the object, before commit.
67 res.OnDestroy = ctx.release
68 return
69 }
70 }
71 _ = unix.Close(listenFD)
72 _ = unix.Close(closeFD)
73}
74
75// listeningUnixSocket reports whether fd is a unix stream socket that listens.
76func listeningUnixSocket(fd int) bool {
77 if fd < 0 {
78 return false
79 }
80 if v, err := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_ACCEPTCONN); err != nil || v == 0 {
81 return false
82 }
83 if v, err := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_DOMAIN); err != nil || v != unix.AF_UNIX {
84 return false
85 }
86 v, err := unix.GetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_TYPE)
87 return err == nil && v == unix.SOCK_STREAM
88}
89
90// securityContext is one wp_security_context_v1. It owns the descriptors
91// until commit hands them to the listener.
92type securityContext struct {
93 s *Server
94 listenFD, closeFD int
95 info sandbox
96 engine, app, inst bool // set once
97 committed bool
98}
99
100// release closes the descriptors of a context that was never committed.
101func (c *securityContext) release() {
102 if c.committed {
103 return
104 }
105 c.committed = true
106 _ = unix.Close(c.listenFD)
107 _ = unix.Close(c.closeFD)
108}
109
110func (c *securityContext) Destroy(*securitycontext.WpSecurityContextV1) {}
111
112// usable rejects any request after commit.
113func (c *securityContext) usable(r *securitycontext.WpSecurityContextV1) bool {
114 if c.committed {
115 r.PostError(uint32(securitycontext.WpSecurityContextV1ErrorAlreadyUsed), "security context already committed")
116 return false
117 }
118 return true
119}
120
121// validMetadata: non-empty, bounded, printable-enough (no NUL or control).
122func validMetadata(v string) bool {
123 return v != "" && len(v) <= maxSecurityMetadata && !strings.ContainsFunc(v, func(r rune) bool { return r < 0x20 || r == 0x7f })
124}
125
126func (c *securityContext) set(r *securitycontext.WpSecurityContextV1, done *bool, dst *string, v string) {
127 switch {
128 case !c.usable(r):
129 case *done:
130 r.PostError(uint32(securitycontext.WpSecurityContextV1ErrorAlreadySet), "metadata already set")
131 case !validMetadata(v):
132 r.PostError(uint32(securitycontext.WpSecurityContextV1ErrorInvalidMetadata), "metadata must be a non-empty printable string of at most 1024 bytes")
133 default:
134 *done, *dst = true, v
135 }
136}
137
138func (c *securityContext) SetSandboxEngine(r *securitycontext.WpSecurityContextV1, name string) {
139 c.set(r, &c.engine, &c.info.engine, name)
140}
141func (c *securityContext) SetAppId(r *securitycontext.WpSecurityContextV1, id string) {
142 c.set(r, &c.app, &c.info.appID, id)
143}
144func (c *securityContext) SetInstanceId(r *securitycontext.WpSecurityContextV1, id string) {
145 c.set(r, &c.inst, &c.info.instanceID, id)
146}
147
148// Commit starts serving the listening socket.
149func (c *securityContext) Commit(r *securitycontext.WpSecurityContextV1) {
150 if !c.usable(r) {
151 return
152 }
153 // The engine name is the one thing every sandbox has; without it a
154 // context says nothing about who is behind it.
155 if !c.engine {
156 r.PostError(uint32(securitycontext.WpSecurityContextV1ErrorInvalidMetadata), "sandbox engine not set")
157 return
158 }
159 c.committed = true
160 l := &securityListener{s: c.s, listenFD: c.listenFD, closeFD: c.closeFD, info: c.info}
161 c.s.securityLive.Add(1)
162 c.s.securityWG.Add(1)
163 go l.run()
164 c.s.log.Info().Str("engine", c.info.engine).Str("app_id", c.info.appID).Msg("security context listener started")
165}
166
167// securityListener accepts the connections of one committed context.
168type securityListener struct {
169 s *Server
170 listenFD, closeFD int
171 info sandbox
172}
173
174func (l *securityListener) run() {
175 s := l.s
176 defer s.securityWG.Done()
177 defer s.securityLive.Add(-1)
178 defer unix.Close(l.closeFD)
179 defer unix.Close(l.listenFD)
180 // Only this listener accepts on the socket: a blocked accept must not
181 // outlast a stop request.
182 if err := unix.SetNonblock(l.listenFD, true); err != nil {
183 s.log.Warn().Err(err).Str("engine", l.info.engine).Msg("security context listener: socket not usable")
184 return
185 }
186 for {
187 fds := []unix.PollFd{{Fd: int32(l.listenFD), Events: unix.POLLIN}, {Fd: int32(l.closeFD), Events: unix.POLLIN}, {Fd: int32(s.securityStop), Events: unix.POLLIN}}
188 if _, err := unix.Poll(fds, -1); err != nil {
189 if err == unix.EINTR {
190 continue
191 }
192 s.log.Warn().Err(err).Str("engine", l.info.engine).Msg("security context listener: poll")
193 return
194 }
195 switch {
196 case fds[2].Revents != 0:
197 return // the compositor stops
198 case fds[1].Revents != 0: // readable or hung up
199 s.log.Info().Str("engine", l.info.engine).Str("app_id", l.info.appID).Msg("security context listener closed")
200 return
201 case fds[0].Revents&(unix.POLLERR|unix.POLLHUP|unix.POLLNVAL) != 0:
202 s.log.Warn().Str("engine", l.info.engine).Msg("security context listener: socket failed")
203 return
204 }
205 if fds[0].Revents&unix.POLLIN != 0 && !l.acceptAll() {
206 return
207 }
208 }
209}
210
211// acceptAll adopts every pending connection; it returns false to stop.
212func (l *securityListener) acceptAll() bool {
213 s := l.s
214 for {
215 fd, _, err := unix.Accept4(l.listenFD, unix.SOCK_CLOEXEC)
216 switch {
217 case err == nil:
218 l.adopt(fd)
219 case errors.Is(err, unix.EAGAIN), errors.Is(err, unix.ECONNABORTED):
220 return true
221 case errors.Is(err, unix.EINTR):
222 case errors.Is(err, unix.EMFILE), errors.Is(err, unix.ENFILE), errors.Is(err, unix.ENOMEM), errors.Is(err, unix.ENOBUFS):
223 // Out of descriptors: wait for some, unless told to stop.
224 s.log.Warn().Err(err).Msg("security context listener: accept")
225 fds := []unix.PollFd{{Fd: int32(s.securityStop), Events: unix.POLLIN}}
226 if _, perr := unix.Poll(fds, int(acceptRetry.Milliseconds())); perr == nil && fds[0].Revents != 0 {
227 return false
228 }
229 return true
230 default:
231 s.log.Warn().Err(err).Msg("security context listener: accept")
232 return false
233 }
234 }
235}
236
237// adopt hands a connection to the display goroutine as a sandboxed client.
238func (l *securityListener) adopt(fd int) {
239 s := l.s
240 ran := s.display.Do(func() {
241 c, err := s.display.CreateClient(fd)
242 if err != nil {
243 s.log.Warn().Err(err).Msg("security context: connection not adopted")
244 _ = unix.Close(fd)
245 return
246 }
247 info := l.info
248 s.sandboxed[c] = &info
249 c.OnDestroy(func() { delete(s.sandboxed, c) })
250 s.log.Debug().Str("engine", info.engine).Str("app_id", info.appID).Msg("sandboxed client connected")
251 })
252 if !ran {
253 _ = unix.Close(fd) // the display stopped first
254 }
255}
256
257// stopSecurityContexts ends every listener and waits for them. Called once
258// the display has stopped.
259func (s *Server) stopSecurityContexts() {
260 var one = [8]byte{1}
261 _, _ = unix.Write(s.securityStop, one[:])
262 s.securityWG.Wait()
263}