v0.4.0 b6be3a8

securitycontext.go RawHistoryFolder

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}

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