main f2d166b

caller.go RawHistoryFolder

1package screencast
2
3import (
4	"os"
5
6	"github.com/bnema/zerobus"
7)
8
9// The caller check proves which executable a D-Bus peer runs, with the pidfd
10// proof the wayland adapter uses for its clients (package peerexe):
11//
12//  1. GetConnectionUnixProcessID(name) names a pid;
13//  2. pidfd_open(pid) pins that process;
14//  3. a second GetConnectionUnixProcessID must name the same pid: the name
15//     was connected throughout, so the pidfd names the peer and not a process
16//     that recycled the pid;
17//  4. the exe is read through the pidfd.
18//
19// The bus calls are sent with Send and their replies come back through
20// ReadMessage, matched by serial: nothing here waits. A request that arrives
21// while its sender is being checked waits in the pending table.
22
23// callerState is how far the proof of a D-Bus peer's executable got.
24type callerState uint8
25
26const (
27	callerFree callerState = iota
28	// callerPid: waiting for the bus to name the peer's pid.
29	callerPid
30	// callerConfirm: a pidfd was opened; waiting for the bus to name the same
31	// pid again.
32	callerConfirm
33	// callerKnown: exe is the executable of the peer, and the allowlist
34	// admitted it when it was proven. Only such peers are cached.
35	callerKnown
36)
37
38// caller is one checked unique bus name. The pidfd stays open for as long
39// as the name is cached, so the executable can be read again at the
40// privileged entry points.
41type caller struct {
42	state  callerState
43	name   string // unique name, ":1.42"
44	serial uint32 // the outstanding GetConnectionUnixProcessID request
45	pid    int
46	pidfd  *os.File
47	exe    string
48}
49
50func (c *caller) release() {
51	if c.pidfd != nil {
52		_ = c.pidfd.Close()
53	}
54	*c = caller{}
55}
56
57// pending is a request parked until its sender's check ends.
58type pending struct {
59	used bool
60	req  request
61}
62
63// callerOf returns the cache entry of a unique name.
64func (s *Service) callerOf(name string) *caller {
65	for i := range s.callers {
66		if s.callers[i].state != callerFree && s.callers[i].name == name {
67			return &s.callers[i]
68		}
69	}
70	return nil
71}
72
73// admit takes a decoded call: a checked sender is served at once, any other
74// waits for its check.
75func (s *Service) admit(req *request) {
76	c := s.callerOf(req.sender)
77	if c != nil && c.state == callerKnown {
78		if !s.allowed(c.exe) {
79			// The policy closed on a cached peer: refuse and forget it.
80			s.deny(req)
81			c.release()
82			return
83		}
84		s.dispatch(c, req)
85		return
86	}
87	slot := -1
88	for i := range s.pending {
89		if !s.pending[i].used {
90			slot = i
91			break
92		}
93	}
94	if slot < 0 {
95		s.replyErr(req, errLimits, "too many requests wait for a caller check")
96		return
97	}
98	if c == nil {
99		for i := range s.callers {
100			if s.callers[i].state == callerFree {
101				c = &s.callers[i]
102				break
103			}
104		}
105		if c == nil {
106			s.replyErr(req, errLimits, "too many callers")
107			return
108		}
109		*c = caller{state: callerPid, name: req.sender}
110		if !s.askPid(c) {
111			c.release()
112			s.deny(req)
113			return
114		}
115	}
116	s.pending[slot] = pending{used: true, req: *req}
117}
118
119// askPid sends GetConnectionUnixProcessID for c and keeps its serial.
120func (s *Service) askPid(c *caller) bool {
121	e, serial := s.conn.NewMethodCall(busDaemon, busDaemonPath, busDaemon, "GetConnectionUnixProcessID", "s")
122	e.Str(c.name)
123	if _, err := s.conn.Send(); err != nil {
124		s.fatal = err
125		return false
126	}
127	c.serial = serial
128	return true
129}
130
131// onBusReply advances the check of the caller the reply answers; any other
132// reply of the bus is not ours.
133func (s *Service) onBusReply(m *zerobus.Message) {
134	var c *caller
135	for i := range s.callers {
136		cc := &s.callers[i]
137		if (cc.state == callerPid || cc.state == callerConfirm) && cc.serial == m.ReplySerial {
138			c = cc
139			break
140		}
141	}
142	if c == nil {
143		return
144	}
145	pid := -1
146	if m.Type == zerobus.TypeMethodReturn && m.Signature == "u" {
147		r := m.Body()
148		v := r.Uint32()
149		if r.Err() == nil && v > 0 && v < 1<<31 {
150			pid = int(v)
151		}
152	}
153	switch {
154	case pid < 0:
155		s.failCaller(c, "pid lookup failed")
156	case c.state == callerPid:
157		pidfd, err := s.peers.Open(pid)
158		if err != nil {
159			s.log.Info().Err(err).Str("sender", c.name).Msg("caller refused: no pidfd")
160			s.failCaller(c, "")
161			return
162		}
163		c.pid, c.pidfd, c.state = pid, pidfd, callerConfirm
164		if !s.askPid(c) {
165			s.failCaller(c, "")
166		}
167	default:
168		if pid != c.pid {
169			s.log.Info().Str("sender", c.name).Msg("caller refused: pid changed during the check")
170			s.failCaller(c, "")
171			return
172		}
173		exe, got, err := s.peers.Exe(c.pidfd)
174		if err != nil || got != c.pid {
175			s.log.Info().Err(err).Str("sender", c.name).Msg("caller refused: executable not proven")
176			s.failCaller(c, "")
177			return
178		}
179		if !s.allowed(exe) {
180			// A refused peer is not cached: it would hold one of the
181			// maxCallers entries until it leaves the bus, and enough of
182			// them would lock the real portal out. It is checked again,
183			// from a free entry, if it calls again.
184			s.log.Info().Str("sender", c.name).Str("exe", exe).Msg("caller refused: executable not allowed")
185			s.failCaller(c, "")
186			return
187		}
188		c.exe, c.state = exe, callerKnown
189		s.flush(c)
190	}
191}
192
193// failCaller refuses every request that waits for c and forgets it.
194func (s *Service) failCaller(c *caller, why string) {
195	if why != "" {
196		s.log.Info().Str("sender", c.name).Msg("caller refused: " + why)
197	}
198	name := c.name
199	c.release()
200	for i := range s.pending {
201		if p := &s.pending[i]; p.used && p.req.sender == name {
202			req := p.req
203			*p = pending{}
204			s.deny(&req)
205		}
206	}
207}
208
209// flush serves the requests that waited for c, in arrival order.
210func (s *Service) flush(c *caller) {
211	for i := range s.pending {
212		p := &s.pending[i]
213		if !p.used || p.req.sender != c.name {
214			continue
215		}
216		req := p.req
217		*p = pending{}
218		s.dispatch(c, &req)
219	}
220}
221
222// onGone forgets a peer that left the bus: its check, its parked requests
223// and its sessions.
224func (s *Service) onGone(name string) {
225	if c := s.callerOf(name); c != nil {
226		c.release()
227	}
228	for i := range s.pending {
229		if s.pending[i].used && s.pending[i].req.sender == name {
230			s.pending[i] = pending{}
231		}
232	}
233	for i := range s.sessions {
234		if sess := &s.sessions[i]; sess.used && sess.owner == name {
235			s.closeSession(sess, false)
236		}
237	}
238}
239
240// allowed reports whether the allowlist in force admits exe.
241func (s *Service) allowed(exe string) bool {
242	p := s.policy()
243	return p != nil && p.Allows(exe)
244}
245
246// recheck reads the executable behind the pidfd again, so an exec after the
247// first check is caught, and applies the allowlist to it.
248func (s *Service) recheck(c *caller) bool {
249	exe, pid, err := s.peers.Exe(c.pidfd)
250	if err != nil || pid != c.pid {
251		s.log.Info().Err(err).Str("sender", c.name).Msg("caller refused: executable not proven")
252		return false
253	}
254	if exe != c.exe {
255		s.log.Info().Str("sender", c.name).Str("was", c.exe).Str("now", exe).Msg("caller executable changed")
256		c.exe = exe
257	}
258	return s.allowed(exe)
259}

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