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}