1/*
2 * Copyright (C) 2018 Igalia S.L.
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18#include "config.h"
19#include "BubblewrapLauncher.h"
20
21#if ENABLE(BUBBLEWRAP_SANDBOX)
22
23#include <WebCore/FileSystem.h>
24#include <WebCore/PlatformDisplay.h>
25#include <fcntl.h>
26#include <glib.h>
27#include <seccomp.h>
28#include <sys/ioctl.h>
29#include <wtf/glib/GLibUtilities.h>
30#include <wtf/glib/GRefPtr.h>
31#include <wtf/glib/GUniquePtr.h>
32
33namespace WebKit {
34using namespace WebCore;
35
36static int memfd_create(const char* name, unsigned flags)
37{
38 return syscall(__NR_memfd_create, name, flags);
39}
40
41#define MFD_ALLOW_SEALING 2U
42
43static int
44argsToFd(const Vector<CString>& args, const char *name)
45{
46 GString* buffer = g_string_new(nullptr);
47
48 for (const auto& arg : args)
49 g_string_append_len(buffer, arg.data(), arg.length() + 1); // Include NUL
50
51 GRefPtr<GBytes> bytes = adoptGRef(g_string_free_to_bytes(buffer));
52
53 int memfd = memfd_create(name, MFD_ALLOW_SEALING);
54 if (memfd == -1)
55 g_error("memfd_create failed: %s", g_strerror(errno));
56
57 size_t size;
58 gconstpointer data = g_bytes_get_data(bytes.get(), &size);
59
60 ssize_t bytesWritten = write(memfd, data, size);
61 if (bytesWritten < 0)
62 g_error("Writing args to memfd failed: %s", g_strerror(errno));
63
64 if (static_cast<size_t>(bytesWritten) != size)
65 g_error("Failed to write all args to memfd");
66
67 if (lseek(memfd, 0, SEEK_SET) == -1)
68 g_error("lseek failed: %s", g_strerror(errno));
69
70 if (fcntl(memfd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE | F_SEAL_SEAL) == -1)
71 g_error("Failed to seal memfd: %s", g_strerror(errno));
72
73 return memfd;
74}
75
76enum class DBusAddressType {
77 Normal,
78 Abstract,
79};
80
81class XDGDBusProxyLauncher {
82public:
83 void setAddress(const char* dbusAddress, DBusAddressType addressType)
84 {
85 GUniquePtr<char> dbusPath = dbusAddressToPath(dbusAddress, addressType);
86 if (!dbusPath.get())
87 return;
88
89 GUniquePtr<char> appRunDir(g_build_filename(g_get_user_runtime_dir(), g_get_prgname(), nullptr));
90 m_proxyPath = makeProxyPath(appRunDir.get()).get();
91
92 m_socket = dbusAddress;
93 m_path = dbusPath.get();
94 }
95
96 bool isRunning() const { return m_process.get() && g_subprocess_get_if_exited(m_process.get()); };
97 const CString& path() const { return m_path; };
98 const CString& proxyPath() const { return m_proxyPath; };
99
100 void setPermissions(Vector<CString>&& permissions)
101 {
102 RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!isRunning());
103 m_permissions = permissions;
104 };
105
106 void launch()
107 {
108 RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!isRunning());
109
110 if (m_socket.isNull() || m_path.isNull() || m_proxyPath.isNull())
111 return;
112
113 int syncFds[2];
114 if (pipe2 (syncFds, O_CLOEXEC) == -1)
115 g_error("Failed to make syncfds for dbus-proxy: %s", g_strerror(errno));
116
117 GUniquePtr<char> syncFdStr(g_strdup_printf("--fd=%d", syncFds[1]));
118
119 Vector<CString> proxyArgs = {
120 m_socket, m_proxyPath,
121 "--filter",
122 syncFdStr.get(),
123 };
124
125 if (!g_strcmp0(g_getenv("WEBKIT_ENABLE_DBUS_PROXY_LOGGING"), "1"))
126 proxyArgs.append("--log");
127
128 proxyArgs.appendVector(m_permissions);
129
130
131 int proxyFd = argsToFd(proxyArgs, "dbus-proxy");
132 GUniquePtr<char> proxyArgsStr(g_strdup_printf("--args=%d", proxyFd));
133
134 Vector<CString> args = {
135 DBUS_PROXY_EXECUTABLE,
136 proxyArgsStr.get(),
137 };
138
139 int nargs = args.size() + 1;
140 int i = 0;
141 char** argv = g_newa(char*, nargs);
142 for (const auto& arg : args)
143 argv[i++] = const_cast<char*>(arg.data());
144 argv[i] = nullptr;
145
146 GRefPtr<GSubprocessLauncher> launcher = adoptGRef(g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_INHERIT_FDS));
147 g_subprocess_launcher_set_child_setup(launcher.get(), childSetupFunc, GINT_TO_POINTER(syncFds[1]), nullptr);
148 g_subprocess_launcher_take_fd(launcher.get(), proxyFd, proxyFd);
149 g_subprocess_launcher_take_fd(launcher.get(), syncFds[1], syncFds[1]);
150 // We are purposefully leaving syncFds[0] open here.
151 // xdg-dbus-proxy will exit() itself once that is closed on our exit
152
153 GUniqueOutPtr<GError> error;
154 m_process = adoptGRef(g_subprocess_launcher_spawnv(launcher.get(), argv, &error.outPtr()));
155 if (error.get())
156 g_error("Failed to start dbus proxy: %s", error.get()->message);
157
158 char out;
159 // We need to ensure the proxy has created the socket.
160 // FIXME: This is more blocking IO.
161 if (read (syncFds[0], &out, 1) != 1)
162 g_error("Failed to fully launch dbus-proxy %s", g_strerror(errno));
163 };
164
165private:
166 static void childSetupFunc(gpointer userdata)
167 {
168 int fd = GPOINTER_TO_INT(userdata);
169 fcntl(fd, F_SETFD, 0); // Unset CLOEXEC
170 }
171
172 static GUniquePtr<char> makeProxyPath(const char* appRunDir)
173 {
174 if (g_mkdir_with_parents(appRunDir, 0700) == -1) {
175 g_warning("Failed to mkdir for dbus proxy (%s): %s", appRunDir, g_strerror(errno));
176 return GUniquePtr<char>(nullptr);
177 }
178
179 char* proxySocketTemplate = g_build_filename(appRunDir, "dbus-proxy-XXXXXX", nullptr);
180 int fd;
181 if ((fd = g_mkstemp(proxySocketTemplate)) == -1) {
182 g_free(proxySocketTemplate);
183 g_warning("Failed to make socket file for dbus proxy: %s", g_strerror(errno));
184 return GUniquePtr<char>(nullptr);
185 }
186
187 close(fd);
188 return GUniquePtr<char>(proxySocketTemplate);
189 };
190
191 static GUniquePtr<char> dbusAddressToPath(const char* address, DBusAddressType addressType = DBusAddressType::Normal)
192 {
193 if (!address)
194 return nullptr;
195
196 if (!g_str_has_prefix(address, "unix:"))
197 return nullptr;
198
199 const char* path = strstr(address, addressType == DBusAddressType::Abstract ? "abstract=" : "path=");
200 if (!path)
201 return nullptr;
202
203 path += strlen(addressType == DBusAddressType::Abstract ? "abstract=" : "path=");
204 const char* pathEnd = path;
205 while (*pathEnd && *pathEnd != ',')
206 pathEnd++;
207
208 return GUniquePtr<char>(g_strndup(path, pathEnd - path));
209}
210
211 CString m_socket;
212 CString m_path;
213 CString m_proxyPath;
214 GRefPtr<GSubprocess> m_process;
215 Vector<CString> m_permissions;
216};
217
218enum class BindFlags {
219 ReadOnly,
220 ReadWrite,
221 Device,
222};
223
224static void bindIfExists(Vector<CString>& args, const char* path, BindFlags bindFlags = BindFlags::ReadOnly)
225{
226 if (!path)
227 return;
228
229 const char* bindType;
230 if (bindFlags == BindFlags::Device)
231 bindType = "--dev-bind-try";
232 else if (bindFlags == BindFlags::ReadOnly)
233 bindType = "--ro-bind-try";
234 else
235 bindType = "--bind-try";
236 args.appendVector(Vector<CString>({ bindType, path, path }));
237}
238
239static void bindDBusSession(Vector<CString>& args, XDGDBusProxyLauncher& proxy)
240{
241 if (!proxy.isRunning())
242 proxy.setAddress(g_getenv("DBUS_SESSION_BUS_ADDRESS"), DBusAddressType::Normal);
243
244 args.appendVector(Vector<CString>({
245 "--bind", proxy.proxyPath(), proxy.path(),
246 }));
247}
248
249static void bindX11(Vector<CString>& args)
250{
251 const char* display = g_getenv("DISPLAY");
252 if (!display || display[0] != ':' || !g_ascii_isdigit(const_cast<char*>(display)[1]))
253 display = ":0";
254 GUniquePtr<char> x11File(g_strdup_printf("/tmp/.X11-unix/X%s", display + 1));
255 bindIfExists(args, x11File.get(), BindFlags::ReadWrite);
256
257 const char* xauth = g_getenv("XAUTHORITY");
258 if (!xauth) {
259 const char* homeDir = g_get_home_dir();
260 GUniquePtr<char> xauthFile(g_build_filename(homeDir, ".Xauthority", nullptr));
261 bindIfExists(args, xauthFile.get());
262 } else
263 bindIfExists(args, xauth);
264}
265
266static void bindDconf(Vector<CString>& args)
267{
268 const char* runtimeDir = g_get_user_runtime_dir();
269 GUniquePtr<char> dconfRuntimeDir(g_build_filename(runtimeDir, "dconf", nullptr));
270 args.appendVector(Vector<CString>({ "--bind", dconfRuntimeDir.get(), dconfRuntimeDir.get() }));
271
272 const char* dconfDir = g_getenv("DCONF_USER_CONFIG_DIR");
273 if (dconfDir)
274 bindIfExists(args, dconfDir);
275 else {
276 const char* configDir = g_get_user_config_dir();
277 GUniquePtr<char> dconfConfigDir(g_build_filename(configDir, "dconf", nullptr));
278 bindIfExists(args, dconfConfigDir.get(), BindFlags::ReadWrite);
279 }
280}
281
282static void bindWayland(Vector<CString>& args)
283{
284 const char* display = g_getenv("WAYLAND_DISPLAY");
285 if (!display)
286 display = "wayland-0";
287
288 const char* runtimeDir = g_get_user_runtime_dir();
289 GUniquePtr<char> waylandRuntimeFile(g_build_filename(runtimeDir, display, nullptr));
290 bindIfExists(args, waylandRuntimeFile.get(), BindFlags::ReadWrite);
291}
292
293static void bindPulse(Vector<CString>& args)
294{
295 // FIXME: The server can be defined in config files we'd have to parse.
296 // They can also be set as X11 props but that is getting a bit ridiculous.
297 const char* pulseServer = g_getenv("PULSE_SERVER");
298 if (pulseServer) {
299 if (g_str_has_prefix(pulseServer, "unix:"))
300 bindIfExists(args, pulseServer + 5, BindFlags::ReadWrite);
301 // else it uses tcp
302 } else {
303 const char* runtimeDir = g_get_user_runtime_dir();
304 GUniquePtr<char> pulseRuntimeDir(g_build_filename(runtimeDir, "pulse", nullptr));
305 bindIfExists(args, pulseRuntimeDir.get(), BindFlags::ReadWrite);
306 }
307
308 const char* pulseConfig = g_getenv("PULSE_CLIENTCONFIG");
309 if (pulseConfig)
310 bindIfExists(args, pulseConfig);
311
312 const char* configDir = g_get_user_config_dir();
313 GUniquePtr<char> pulseConfigDir(g_build_filename(configDir, "pulse", nullptr));
314 bindIfExists(args, pulseConfigDir.get());
315
316 const char* homeDir = g_get_home_dir();
317 GUniquePtr<char> pulseHomeConfigDir(g_build_filename(homeDir, ".pulse", nullptr));
318 GUniquePtr<char> asoundHomeConfigDir(g_build_filename(homeDir, ".asoundrc", nullptr));
319 bindIfExists(args, pulseHomeConfigDir.get());
320 bindIfExists(args, asoundHomeConfigDir.get());
321
322 // This is the ultimate fallback to raw ALSA
323 bindIfExists(args, "/dev/snd", BindFlags::Device);
324}
325
326static void bindFonts(Vector<CString>& args)
327{
328 const char* configDir = g_get_user_config_dir();
329 const char* homeDir = g_get_home_dir();
330 const char* dataDir = g_get_user_data_dir();
331 const char* cacheDir = g_get_user_cache_dir();
332
333 // Configs can include custom dirs but then we have to parse them...
334 GUniquePtr<char> fontConfig(g_build_filename(configDir, "fontconfig", nullptr));
335 GUniquePtr<char> fontCache(g_build_filename(cacheDir, "fontconfig", nullptr));
336 GUniquePtr<char> fontHomeConfig(g_build_filename(homeDir, ".fonts.conf", nullptr));
337 GUniquePtr<char> fontHomeConfigDir(g_build_filename(configDir, ".fonts.conf.d", nullptr));
338 GUniquePtr<char> fontData(g_build_filename(dataDir, "fonts", nullptr));
339 GUniquePtr<char> fontHomeData(g_build_filename(homeDir, ".fonts", nullptr));
340 bindIfExists(args, fontConfig.get());
341 bindIfExists(args, fontCache.get(), BindFlags::ReadWrite);
342 bindIfExists(args, fontHomeConfig.get());
343 bindIfExists(args, fontHomeConfigDir.get());
344 bindIfExists(args, fontData.get());
345 bindIfExists(args, fontHomeData.get());
346}
347
348#if PLATFORM(GTK)
349static void bindGtkData(Vector<CString>& args)
350{
351 const char* configDir = g_get_user_config_dir();
352 const char* dataDir = g_get_user_data_dir();
353 const char* homeDir = g_get_home_dir();
354
355 GUniquePtr<char> gtkConfig(g_build_filename(configDir, "gtk-3.0", nullptr));
356 GUniquePtr<char> themeData(g_build_filename(dataDir, "themes", nullptr));
357 GUniquePtr<char> themeHomeData(g_build_filename(homeDir, ".themes", nullptr));
358 GUniquePtr<char> iconHomeData(g_build_filename(homeDir, ".icons", nullptr));
359 bindIfExists(args, gtkConfig.get());
360 bindIfExists(args, themeData.get());
361 bindIfExists(args, themeHomeData.get());
362 bindIfExists(args, iconHomeData.get());
363}
364
365static void bindA11y(Vector<CString>& args)
366{
367 static XDGDBusProxyLauncher proxy;
368
369 if (!proxy.isRunning()) {
370 // FIXME: Avoid blocking IO... (It is at least a one-time cost)
371 GRefPtr<GDBusConnection> sessionBus = adoptGRef(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr));
372 if (!sessionBus.get())
373 return;
374
375 GRefPtr<GDBusMessage> msg = adoptGRef(g_dbus_message_new_method_call(
376 "org.a11y.Bus", "/org/a11y/bus", "org.a11y.Bus", "GetAddress"));
377 g_dbus_message_set_body(msg.get(), g_variant_new("()"));
378 GRefPtr<GDBusMessage> reply = adoptGRef(g_dbus_connection_send_message_with_reply_sync(
379 sessionBus.get(), msg.get(),
380 G_DBUS_SEND_MESSAGE_FLAGS_NONE,
381 30000,
382 nullptr,
383 nullptr,
384 nullptr));
385
386 if (reply.get()) {
387 GUniqueOutPtr<GError> error;
388 if (g_dbus_message_to_gerror(reply.get(), &error.outPtr())) {
389 if (!g_error_matches(error.get(), G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN))
390 g_warning("Can't find a11y bus: %s", error->message);
391 } else {
392 GUniqueOutPtr<char> a11yAddress;
393 g_variant_get(g_dbus_message_get_body(reply.get()), "(s)", &a11yAddress.outPtr());
394 proxy.setAddress(a11yAddress.get(), DBusAddressType::Abstract);
395 }
396 }
397
398 proxy.setPermissions({
399 "--sloppy-names",
400 "--call=org.a11y.atspi.Registry=org.a11y.atspi.Socket.Embed@/org/a11y/atspi/accessible/root",
401 "--call=org.a11y.atspi.Registry=org.a11y.atspi.Socket.Unembed@/org/a11y/atspi/accessible/root",
402 "--call=org.a11y.atspi.Registry=org.a11y.atspi.Registry.GetRegisteredEvents@/org/a11y/atspi/registry",
403 "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.GetKeystrokeListeners@/org/a11y/atspi/registry/deviceeventcontroller",
404 "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.GetDeviceEventListeners@/org/a11y/atspi/registry/deviceeventcontroller",
405 "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.NotifyListenersSync@/org/a11y/atspi/registry/deviceeventcontroller",
406 "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.NotifyListenersAsync@/org/a11y/atspi/registry/deviceeventcontroller",
407 });
408
409 proxy.launch();
410 }
411
412 args.appendVector(Vector<CString>({
413 "--bind", proxy.proxyPath(), proxy.path(),
414 }));
415}
416#endif
417
418static bool bindPathVar(Vector<CString>& args, const char* varname)
419{
420 const char* pathValue = g_getenv(varname);
421 if (!pathValue)
422 return false;
423
424 GUniquePtr<char*> splitPaths(g_strsplit(pathValue, ":", -1));
425 for (size_t i; splitPaths.get()[i]; ++i)
426 bindIfExists(args, splitPaths.get()[i]);
427
428 return true;
429}
430
431static void bindGStreamerData(Vector<CString>& args)
432{
433 if (!bindPathVar(args, "GST_PLUGIN_PATH_1_0"))
434 bindPathVar(args, "GST_PLUGIN_PATH");
435
436 if (!bindPathVar(args, "GST_PLUGIN_SYSTEM_PATH_1_0")) {
437 if (!bindPathVar(args, "GST_PLUGIN_SYSTEM_PATH")) {
438 GUniquePtr<char> gstData(g_build_filename(g_get_user_data_dir(), "gstreamer-1.0", nullptr));
439 bindIfExists(args, gstData.get());
440 }
441 }
442
443 GUniquePtr<char> gstCache(g_build_filename(g_get_user_cache_dir(), "gstreamer-1.0", nullptr));
444 bindIfExists(args, gstCache.get(), BindFlags::ReadWrite);
445
446 // /usr/lib is already added so this is only requried for other dirs
447 const char* scannerPath = g_getenv("GST_PLUGIN_SCANNER") ?: "/usr/libexec/gstreamer-1.0/gst-plugin-scanner";
448 const char* helperPath = g_getenv("GST_INSTALL_PLUGINS_HELPER ") ?: "/usr/libexec/gst-install-plugins-helper";
449
450 bindIfExists(args, scannerPath);
451 bindIfExists(args, helperPath);
452}
453
454static void bindOpenGL(Vector<CString>& args)
455{
456 args.appendVector(Vector<CString>({
457 "--dev-bind-try", "/dev/dri", "/dev/dri",
458 // Mali
459 "--dev-bind-try", "/dev/mali", "/dev/mali",
460 "--dev-bind-try", "/dev/mali0", "/dev/mali0",
461 "--dev-bind-try", "/dev/umplock", "/dev/umplock",
462 // Nvidia
463 "--dev-bind-try", "/dev/nvidiactl", "/dev/nvidiactl",
464 "--dev-bind-try", "/dev/nvidia0", "/dev/nvidia0",
465 "--dev-bind-try", "/dev/nvidia", "/dev/nvidia",
466 // Adreno
467 "--dev-bind-try", "/dev/kgsl-3d0", "/dev/kgsl-3d0",
468 "--dev-bind-try", "/dev/ion", "/dev/ion",
469#if PLATFORM(WPE)
470 "--dev-bind-try", "/dev/fb0", "/dev/fb0",
471 "--dev-bind-try", "/dev/fb1", "/dev/fb1",
472#endif
473 }));
474}
475
476static void bindV4l(Vector<CString>& args)
477{
478 args.appendVector(Vector<CString>({
479 "--dev-bind-try", "/dev/v4l", "/dev/v4l",
480 // Not pretty but a stop-gap for pipewire anyway.
481 "--dev-bind-try", "/dev/video0", "/dev/video0",
482 "--dev-bind-try", "/dev/video1", "/dev/video1",
483 }));
484}
485
486static int setupSeccomp()
487{
488 // NOTE: This is shared code (flatpak-run.c - LGPLv2.1+)
489 // There are today a number of different Linux container
490 // implementations. That will likely continue for long into the
491 // future. But we can still try to share code, and it's important
492 // to do so because it affects what library and application writers
493 // can do, and we should support code portability between different
494 // container tools.
495 //
496 // This syscall blacklist is copied from linux-user-chroot, which was in turn
497 // clearly influenced by the Sandstorm.io blacklist.
498 //
499 // If you make any changes here, I suggest sending the changes along
500 // to other sandbox maintainers. Using the libseccomp list is also
501 // an appropriate venue:
502 // https://groups.google.com/forum/#!topic/libseccomp
503 //
504 // A non-exhaustive list of links to container tooling that might
505 // want to share this blacklist:
506 //
507 // https://github.com/sandstorm-io/sandstorm
508 // in src/sandstorm/supervisor.c++
509 // http://cgit.freedesktop.org/xdg-app/xdg-app/
510 // in common/flatpak-run.c
511 // https://git.gnome.org/browse/linux-user-chroot
512 // in src/setup-seccomp.c
513 struct scmp_arg_cmp cloneArg = SCMP_A0(SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER);
514 struct scmp_arg_cmp ttyArg = SCMP_A1(SCMP_CMP_EQ, (int)TIOCSTI);
515 struct {
516 int scall;
517 struct scmp_arg_cmp* arg;
518 } syscallBlacklist[] = {
519 // Block dmesg
520 { SCMP_SYS(syslog), nullptr },
521 // Useless old syscall
522 { SCMP_SYS(uselib), nullptr },
523 // Don't allow disabling accounting
524 { SCMP_SYS(acct), nullptr },
525 // 16-bit code is unnecessary in the sandbox, and modify_ldt is a
526 // historic source of interesting information leaks.
527 { SCMP_SYS(modify_ldt), nullptr },
528 // Don't allow reading current quota use
529 { SCMP_SYS(quotactl), nullptr },
530
531 // Don't allow access to the kernel keyring
532 { SCMP_SYS(add_key), nullptr },
533 { SCMP_SYS(keyctl), nullptr },
534 { SCMP_SYS(request_key), nullptr },
535
536 // Scary VM/NUMA ops
537 { SCMP_SYS(move_pages), nullptr },
538 { SCMP_SYS(mbind), nullptr },
539 { SCMP_SYS(get_mempolicy), nullptr },
540 { SCMP_SYS(set_mempolicy), nullptr },
541 { SCMP_SYS(migrate_pages), nullptr },
542
543 // Don't allow subnamespace setups:
544 { SCMP_SYS(unshare), nullptr },
545 { SCMP_SYS(mount), nullptr },
546 { SCMP_SYS(pivot_root), nullptr },
547 { SCMP_SYS(clone), &cloneArg },
548
549 // Don't allow faking input to the controlling tty (CVE-2017-5226)
550 { SCMP_SYS(ioctl), &ttyArg },
551
552 // Profiling operations; we expect these to be done by tools from outside
553 // the sandbox. In particular perf has been the source of many CVEs.
554 { SCMP_SYS(perf_event_open), nullptr },
555 // Don't allow you to switch to bsd emulation or whatnot.
556 { SCMP_SYS(personality), nullptr },
557 { SCMP_SYS(ptrace), nullptr }
558 };
559
560 scmp_filter_ctx seccomp = seccomp_init(SCMP_ACT_ALLOW);
561 if (!seccomp)
562 g_error("Failed to init seccomp");
563
564 for (auto& rule : syscallBlacklist) {
565 int scall = rule.scall;
566 int r;
567 if (rule.arg)
568 r = seccomp_rule_add(seccomp, SCMP_ACT_ERRNO(EPERM), scall, 1, rule.arg);
569 else
570 r = seccomp_rule_add(seccomp, SCMP_ACT_ERRNO(EPERM), scall, 0);
571 if (r == -EFAULT) {
572 seccomp_release(seccomp);
573 g_error("Failed to add seccomp rule");
574 }
575 }
576
577 int tmpfd = memfd_create("seccomp-bpf", 0);
578 if (tmpfd == -1) {
579 seccomp_release(seccomp);
580 g_error("Failed to create memfd: %s", g_strerror(errno));
581 }
582
583 if (seccomp_export_bpf(seccomp, tmpfd)) {
584 seccomp_release(seccomp);
585 close(tmpfd);
586 g_error("Failed to export seccomp bpf");
587 }
588
589 if (lseek(tmpfd, 0, SEEK_SET) < 0)
590 g_error("lseek failed: %s", g_strerror(errno));
591
592 seccomp_release(seccomp);
593 return tmpfd;
594}
595
596GRefPtr<GSubprocess> bubblewrapSpawn(GRefPtr<GSubprocessLauncher> launcher, const ProcessLauncher::LaunchOptions& launchOptions, char** argv, GError **error)
597{
598 // It is impossible to know what access arbitrary plugins need and since it is for legacy
599 // reasons lets just leave it unsandboxed.
600 if (launchOptions.processType == ProcessLauncher::ProcessType::Plugin64
601 || launchOptions.processType == ProcessLauncher::ProcessType::Plugin32)
602 return g_subprocess_launcher_spawnv(launcher.get(), argv, error);
603
604 // NOTE: This is not a great solution but we just assume that applications create this directory
605 // ahead of time if they require it.
606 GUniquePtr<char> configDir(g_build_filename(g_get_user_config_dir(), g_get_prgname(), nullptr));
607
608 Vector<CString> sandboxArgs = {
609 "--die-with-parent",
610 "--unshare-pid",
611 "--unshare-uts",
612
613 // We assume /etc has safe permissions.
614 // At a later point we can start masking privacy-concerning files.
615 "--ro-bind", "/etc", "/etc",
616 "--dev", "/dev",
617 "--proc", "/proc",
618 "--tmpfs", "/tmp",
619 "--unsetenv", "TMPDIR",
620 "--dir", "/run",
621 "--symlink", "../run", "/var/run",
622 "--symlink", "../tmp", "/var/tmp",
623 "--ro-bind", "/sys/block", "/sys/block",
624 "--ro-bind", "/sys/bus", "/sys/bus",
625 "--ro-bind", "/sys/class", "/sys/class",
626 "--ro-bind", "/sys/dev", "/sys/dev",
627 "--ro-bind", "/sys/devices", "/sys/devices",
628
629 "--ro-bind-try", "/usr/share", "/usr/share",
630 "--ro-bind-try", "/usr/local/share", "/usr/local/share",
631 "--ro-bind-try", DATADIR, DATADIR,
632
633 // We only grant access to the libdirs webkit is built with and
634 // guess system libdirs. This will always have some edge cases.
635 "--ro-bind-try", "/lib", "/lib",
636 "--ro-bind-try", "/usr/lib", "/usr/lib",
637 "--ro-bind-try", "/usr/local/lib", "/usr/local/lib",
638 "--ro-bind-try", LIBDIR, LIBDIR,
639 "--ro-bind-try", "/lib64", "/lib64",
640 "--ro-bind-try", "/usr/lib64", "/usr/lib64",
641 "--ro-bind-try", "/usr/local/lib64", "/usr/local/lib64",
642
643 "--ro-bind-try", PKGLIBEXECDIR, PKGLIBEXECDIR,
644
645 "--setenv", "GTK_USE_PORTAL", "1",
646
647 "--bind-try", configDir.get(), configDir.get(),
648 };
649 // We would have to parse ld config files for more info.
650 bindPathVar(sandboxArgs, "LD_LIBRARY_PATH");
651
652 if (launchOptions.processType == ProcessLauncher::ProcessType::Network) {
653 static XDGDBusProxyLauncher proxy;
654
655 // FIXME: The network process is used for `file://` URIs and
656 // we would have to pass through most paths for this to work.
657
658 // FIXME: HTTP credentials talks to libsecret.
659
660 // glib-networking can use dbus for proxy information.
661 // FIXME: Find and add the permissions it needs for this (pacrunner).
662 bindDBusSession(sandboxArgs, proxy);
663
664 if (!proxy.isRunning())
665 proxy.launch();
666 }
667
668 // NOTE: This has network access for HLS via GStreamer.
669 if (launchOptions.processType == ProcessLauncher::ProcessType::Web) {
670 static XDGDBusProxyLauncher proxy;
671
672 // If Wayland in use don't grant X11
673#if PLATFORM(WAYLAND) && USE(EGL)
674 if (PlatformDisplay::sharedDisplay().type() == PlatformDisplay::Type::Wayland) {
675 bindWayland(sandboxArgs);
676 sandboxArgs.append("--unshare-ipc");
677 } else
678#endif
679 bindX11(sandboxArgs);
680
681 bindDBusSession(sandboxArgs, proxy);
682 // FIXME: This needs to be restricted, upstream is working on it.
683 bindDconf(sandboxArgs);
684 // FIXME: We should move to Pipewire as soon as viable, Pulse doesn't restrict clients atm.
685 bindPulse(sandboxArgs);
686 bindFonts(sandboxArgs);
687 bindGStreamerData(sandboxArgs);
688 bindOpenGL(sandboxArgs);
689 // FIXME: This is also fixed by Pipewire once in use.
690 bindV4l(sandboxArgs);
691#if PLATFORM(GTK)
692 bindA11y(sandboxArgs);
693 bindGtkData(sandboxArgs);
694#endif
695
696 if (!proxy.isRunning()) {
697 proxy.setPermissions({
698 // FIXME: Used by GTK on Wayland.
699 "--talk=ca.desrt.dconf",
700 // xdg-desktop-portal used by GTK and us.
701 "--talk=org.freedesktop.portal.Desktop",
702 // GStreamers plugin install helper.
703 "--call=org.freedesktop.PackageKit=org.freedesktop.PackageKit.Modify2.InstallGStreamerResources@/org/freedesktop/PackageKit"
704 });
705 proxy.launch();
706 }
707
708
709 } else {
710 // Only X11 users need this for XShm which is only the Web process.
711 sandboxArgs.append("--unshare-ipc");
712 }
713
714 // Only process without any network access for now.
715 if (launchOptions.processType == ProcessLauncher::ProcessType::Storage)
716 sandboxArgs.append("--unshare-net");
717
718#if ENABLE(DEVELOPER_MODE)
719 const char* execDirectory = g_getenv("WEBKIT_EXEC_PATH");
720 if (execDirectory) {
721 String parentDir = FileSystem::directoryName(FileSystem::stringFromFileSystemRepresentation(execDirectory));
722 bindIfExists(sandboxArgs, parentDir.utf8().data());
723 }
724
725 CString executablePath = getCurrentExecutablePath();
726 if (!executablePath.isNull()) {
727 // Our executable is `/foo/bar/bin/Process`, we want `/foo/bar` as a usable prefix
728 String parentDir = FileSystem::directoryName(FileSystem::directoryName(FileSystem::stringFromFileSystemRepresentation(executablePath.data())));
729 bindIfExists(sandboxArgs, parentDir.utf8().data());
730 }
731#endif
732
733 int seccompFd = setupSeccomp();
734 GUniquePtr<char> fdStr(g_strdup_printf("%d", seccompFd));
735 g_subprocess_launcher_take_fd(launcher.get(), seccompFd, seccompFd);
736 sandboxArgs.appendVector(Vector<CString>({ "--seccomp", fdStr.get() }));
737
738 for (const String& path : launchOptions.extraSandboxPaths) {
739 if (path.isEmpty())
740 g_warning("Empty path passed to sandbox, this is probably a bug");
741 else if (!FileSystem::makeAllDirectories(path)) // FIXME: Blocking
742 g_warning("Could not create directory \"%s\": %s", path.utf8().data(), g_strerror(errno));
743 else
744 sandboxArgs.appendVector(Vector<CString>({ "--bind", path.utf8(), path.utf8() }));
745 }
746
747 int bwrapFd = argsToFd(sandboxArgs, "bwrap");
748 GUniquePtr<char> bwrapFdStr(g_strdup_printf("%d", bwrapFd));
749 g_subprocess_launcher_take_fd(launcher.get(), bwrapFd, bwrapFd);
750
751 Vector<CString> bwrapArgs = {
752 BWRAP_EXECUTABLE,
753 "--args",
754 bwrapFdStr.get(),
755 "--",
756 };
757
758 char** newArgv = g_newa(char*, g_strv_length(argv) + bwrapArgs.size() + 1);
759 size_t i = 0;
760
761 for (auto& arg : bwrapArgs)
762 newArgv[i++] = const_cast<char*>(arg.data());
763 for (size_t x = 0; argv[x]; x++)
764 newArgv[i++] = argv[x];
765 newArgv[i++] = nullptr;
766
767 return adoptGRef(g_subprocess_launcher_spawnv(launcher.get(), newArgv, error));
768}
769
770};
771
772#endif // ENABLE(BUBBLEWRAP_SANDBOX)