1 /* PipeWire
2 *
3 * Copyright © 2018 Wim Taymans
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a
6 * copy of this software and associated documentation files (the "Software"),
7 * to deal in the Software without restriction, including without limitation
8 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
9 * and/or sell copies of the Software, and to permit persons to whom the
10 * Software is furnished to do so, subject to the following conditions:
11 *
12 * The above copyright notice and this permission notice (including the next
13 * paragraph) shall be included in all copies or substantial portions of the
14 * Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
19 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 * DEALINGS IN THE SOFTWARE.
23 */
24
25 #include <string.h>
26 #include <stdio.h>
27 #include <errno.h>
28 #include <sys/types.h>
29 #include <sys/stat.h>
30 #include <fcntl.h>
31 #include <unistd.h>
32
33 #include "config.h"
34
35 #if HAVE_SYS_VFS_H
36 #include <sys/vfs.h>
37 #endif
38 #if HAVE_SYS_MOUNT_H
39 #include <sys/mount.h>
40 #endif
41
42 #include <spa/utils/result.h>
43 #include <spa/utils/string.h>
44 #include <spa/utils/json.h>
45
46 #include <pipewire/impl.h>
47 #include <pipewire/private.h>
48
49 /** \page page_module_access PipeWire Module: Access
50 *
51 *
52 * The `access` module performs access checks on clients. The access check
53 * is only performed once per client, subsequent checks return the same
54 * resolution.
55 *
56 * Permissions assigned to a client are configured as arguments to this
57 * module, see the example configuration below. A special use-case is Flatpak
58 * where the permission management is delegated.
59 *
60 * This module sets the \ref PW_KEY_ACCESS property to one of
61 * - `allowed`: the client is explicitly allowed to access all resources
62 * - `rejected`: the client does not have access to any resources and a
63 * resource error is generated
64 * - `restricted`: the client is restricted, see note below
65 * - `flatpak`: restricted, special case for clients running inside flatpak,
66 * see note below
67 * - `$access.force`: the value of the `access.force` argument given in the
68 * module configuration.
69 * - `unrestricted`: the client is allowed to access all resources. This is the
70 * default for clients not listed in any of the `access.*` options
71 * unless the client requested reduced permissions in \ref
72 * PW_KEY_CLIENT_ACCESS.
73 *
74 * \note Clients with a resolution other than `allowed` or `rejected` rely
75 * on an external actor to update that property once permission is
76 * granted or rejected.
77 *
78 *
79 * ## Module Options
80 *
81 * Options specific to the behavior of this module
82 *
83 * - ``access.allowed = []``: an array of paths of allowed applications
84 * - ``access.rejected = []``: an array of paths of rejected applications
85 * - ``access.restricted = []``: an array of paths of restricted applications
86 * - ``access.force = <str>``: forces an external permissions check (e.g. a flatpak
87 * portal)
88 *
89 * ## General options
90 *
91 * Options with well-known behavior:
92 *
93 * - \ref PW_KEY_ACCESS
94 * - \ref PW_KEY_CLIENT_ACCESS
95 *
96 * ## Example configuration
97 *
98 *\code{.unparsed}
99 * context.modules = [
100 * { name = libpipewire-module-access
101 * args = {
102 * access.allowed = [
103 * /usr/bin/pipewire-media-session
104 * /usr/bin/important-thing
105 * ]
106 *
107 * access.rejected = [
108 * /usr/bin/microphone-snooper
109 * ]
110 *
111 * #access.restricted = [ ]
112 *
113 * # Anything not in the above lists gets assigned the
114 * # access.force permission.
115 * #access.force = flatpak
116 * }
117 * }
118 *]
119 *\endcode
120 *
121 * \see pw_resource_error
122 * \see pw_impl_client_update_permissions
123 */
124
125 #define NAME "access"
126
127 PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
128 #define PW_LOG_TOPIC_DEFAULT mod_topic
129
130 #define MODULE_USAGE "[ access.force=flatpak ] " \
131 "[ access.allowed=<cmd-line> ] " \
132 "[ access.rejected=<cmd-line> ] " \
133 "[ access.restricted=<cmd-line> ] " \
134
135 static const struct spa_dict_item module_props[] = {
136 { PW_KEY_MODULE_AUTHOR, "Wim Taymans <wim.taymans@gmail.com>" },
137 { PW_KEY_MODULE_DESCRIPTION, "Perform access check" },
138 { PW_KEY_MODULE_USAGE, MODULE_USAGE },
139 { PW_KEY_MODULE_VERSION, PACKAGE_VERSION },
140 };
141
142 struct impl {
143 struct pw_context *context;
144 struct pw_properties *properties;
145
146 struct spa_hook context_listener;
147 struct spa_hook module_listener;
148 };
149
check_cmdline(struct pw_impl_client * client,int pid,const char * str)150 static int check_cmdline(struct pw_impl_client *client, int pid, const char *str)
151 {
152 char path[2048], key[1024];
153 ssize_t len;
154 int fd, res;
155 struct spa_json it[2];
156
157 sprintf(path, "/proc/%u/cmdline", pid);
158
159 fd = open(path, O_RDONLY);
160 if (fd < 0) {
161 res = -errno;
162 goto exit;
163 }
164 if ((len = read(fd, path, sizeof(path)-1)) < 0) {
165 res = -errno;
166 goto exit_close;
167 }
168 path[len] = '\0';
169
170 spa_json_init(&it[0], str, strlen(str));
171 if ((res = spa_json_enter_array(&it[0], &it[1])) <= 0)
172 goto exit_close;
173
174 while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) {
175 if (spa_streq(path, key)) {
176 res = 1;
177 goto exit_close;
178 }
179 }
180 res = 0;
181 exit_close:
182 close(fd);
183 exit:
184 return res;
185 }
186
187 #if defined(__linux__)
check_flatpak(struct pw_impl_client * client,int pid)188 static int check_flatpak(struct pw_impl_client *client, int pid)
189 {
190 char root_path[2048];
191 int root_fd, info_fd, res;
192 struct stat stat_buf;
193
194 sprintf(root_path, "/proc/%u/root", pid);
195 root_fd = openat (AT_FDCWD, root_path, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY);
196 if (root_fd == -1) {
197 res = -errno;
198 if (res == -EACCES) {
199 struct statfs buf;
200 /* Access to the root dir isn't allowed. This can happen if the root is on a fuse
201 * filesystem, such as in a toolbox container. We will never have a fuse rootfs
202 * in the flatpak case, so in that case its safe to ignore this and
203 * continue to detect other types of apps. */
204 if (statfs(root_path, &buf) == 0 &&
205 buf.f_type == 0x65735546) /* FUSE_SUPER_MAGIC */
206 return 0;
207 }
208 /* Not able to open the root dir shouldn't happen. Probably the app died and
209 * we're failing due to /proc/$pid not existing. In that case fail instead
210 * of treating this as privileged. */
211 pw_log_info("failed to open \"%s\": %s", root_path, spa_strerror(res));
212 return res;
213 }
214 info_fd = openat (root_fd, ".flatpak-info", O_RDONLY | O_CLOEXEC | O_NOCTTY);
215 close (root_fd);
216 if (info_fd == -1) {
217 if (errno == ENOENT) {
218 pw_log_debug("no .flatpak-info, client on the host");
219 /* No file => on the host */
220 return 0;
221 }
222 res = -errno;
223 pw_log_error("error opening .flatpak-info: %m");
224 return res;
225 }
226 if (fstat (info_fd, &stat_buf) != 0 || !S_ISREG (stat_buf.st_mode)) {
227 /* Some weird fd => failure, assume sandboxed */
228 pw_log_error("error fstat .flatpak-info: %m");
229 }
230 close(info_fd);
231 return 1;
232 }
233 #endif
234
235 static void
context_check_access(void * data,struct pw_impl_client * client)236 context_check_access(void *data, struct pw_impl_client *client)
237 {
238 struct impl *impl = data;
239 struct pw_permission permissions[1];
240 struct spa_dict_item items[2];
241 const struct pw_properties *props;
242 const char *str, *access;
243 int pid, res;
244
245 pid = -EINVAL;
246 if ((props = pw_impl_client_get_properties(client)) != NULL) {
247 if ((str = pw_properties_get(props, PW_KEY_ACCESS)) != NULL) {
248 pw_log_info("client %p: has already access: '%s'", client, str);
249 return;
250 }
251 pw_properties_fetch_int32(props, PW_KEY_SEC_PID, &pid);
252 }
253
254 if (pid < 0) {
255 pw_log_info("client %p: no trusted pid found, assuming not sandboxed", client);
256 access = "no-pid";
257 goto granted;
258 } else {
259 pw_log_info("client %p has trusted pid %d", client, pid);
260 }
261
262 if (impl->properties && (str = pw_properties_get(impl->properties, "access.allowed")) != NULL) {
263 res = check_cmdline(client, pid, str);
264 if (res < 0) {
265 pw_log_warn("%p: client %p allowed check failed: %s",
266 impl, client, spa_strerror(res));
267 } else if (res > 0) {
268 access = "allowed";
269 goto granted;
270 }
271 }
272
273 if (impl->properties && (str = pw_properties_get(impl->properties, "access.rejected")) != NULL) {
274 res = check_cmdline(client, pid, str);
275 if (res < 0) {
276 pw_log_warn("%p: client %p rejected check failed: %s",
277 impl, client, spa_strerror(res));
278 } else if (res > 0) {
279 res = -EACCES;
280 access = "rejected";
281 goto rejected;
282 }
283 }
284
285 if (impl->properties && (str = pw_properties_get(impl->properties, "access.restricted")) != NULL) {
286 res = check_cmdline(client, pid, str);
287 if (res < 0) {
288 pw_log_warn("%p: client %p restricted check failed: %s",
289 impl, client, spa_strerror(res));
290 }
291 else if (res > 0) {
292 pw_log_debug(" %p: restricted client %p added", impl, client);
293 access = "restricted";
294 goto wait_permissions;
295 }
296 }
297 if (impl->properties &&
298 (access = pw_properties_get(impl->properties, "access.force")) != NULL)
299 goto wait_permissions;
300
301 #if defined(__linux__)
302 res = check_flatpak(client, pid);
303 if (res != 0) {
304 if (res < 0) {
305 if (res == -EACCES) {
306 access = "unrestricted";
307 goto granted;
308 }
309 pw_log_warn("%p: client %p sandbox check failed: %s",
310 impl, client, spa_strerror(res));
311 }
312 else if (res > 0) {
313 pw_log_debug(" %p: flatpak client %p added", impl, client);
314 }
315 access = "flatpak";
316 goto wait_permissions;
317 }
318 #endif
319 if ((access = pw_properties_get(props, PW_KEY_CLIENT_ACCESS)) == NULL)
320 access = "unrestricted";
321
322 if (spa_streq(access, "unrestricted") || spa_streq(access, "allowed"))
323 goto granted;
324 else
325 goto wait_permissions;
326
327 granted:
328 pw_log_info("%p: client %p '%s' access granted", impl, client, access);
329 items[0] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, access);
330 pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, 1));
331
332 permissions[0] = PW_PERMISSION_INIT(PW_ID_ANY, PW_PERM_ALL);
333 pw_impl_client_update_permissions(client, 1, permissions);
334 return;
335
336 wait_permissions:
337 pw_log_info("%p: client %p wait for '%s' permissions",
338 impl, client, access);
339 items[0] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, access);
340 pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, 1));
341 return;
342
343 rejected:
344 pw_resource_error(pw_impl_client_get_core_resource(client), res, access);
345 items[0] = SPA_DICT_ITEM_INIT(PW_KEY_ACCESS, access);
346 pw_impl_client_update_properties(client, &SPA_DICT_INIT(items, 1));
347 return;
348 }
349
350 static const struct pw_context_events context_events = {
351 PW_VERSION_CONTEXT_EVENTS,
352 .check_access = context_check_access,
353 };
354
module_destroy(void * data)355 static void module_destroy(void *data)
356 {
357 struct impl *impl = data;
358
359 spa_hook_remove(&impl->context_listener);
360 spa_hook_remove(&impl->module_listener);
361
362 pw_properties_free(impl->properties);
363
364 free(impl);
365 }
366
367 static const struct pw_impl_module_events module_events = {
368 PW_VERSION_IMPL_MODULE_EVENTS,
369 .destroy = module_destroy,
370 };
371
372 SPA_EXPORT
pipewire__module_init(struct pw_impl_module * module,const char * args)373 int pipewire__module_init(struct pw_impl_module *module, const char *args)
374 {
375 struct pw_context *context = pw_impl_module_get_context(module);
376 struct pw_properties *props;
377 struct impl *impl;
378
379 PW_LOG_TOPIC_INIT(mod_topic);
380
381 impl = calloc(1, sizeof(struct impl));
382 if (impl == NULL)
383 return -errno;
384
385 pw_log_debug("module %p: new %s", impl, args);
386
387 if (args)
388 props = pw_properties_new_string(args);
389 else
390 props = NULL;
391
392 impl->context = context;
393 impl->properties = props;
394
395 pw_context_add_listener(context, &impl->context_listener, &context_events, impl);
396 pw_impl_module_add_listener(module, &impl->module_listener, &module_events, impl);
397
398 pw_impl_module_update_properties(module, &SPA_DICT_INIT_ARRAY(module_props));
399
400 return 0;
401 }
402