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 if (proxy.proxyPath().data()) {
245 args.appendVector(Vector<CString>({
246 "--bind", proxy.proxyPath(), proxy.path(),
247 }));
248 }
249}
250
251static void bindX11(Vector<CString>& args)
252{
253 const char* display = g_getenv("DISPLAY");
254 if (!display || display[0] != ':' || !g_ascii_isdigit(const_cast<char*>(display)[1]))
255 display = ":0";
256 GUniquePtr<char> x11File(g_strdup_printf("/tmp/.X11-unix/X%s", display + 1));
257 bindIfExists(args, x11File.get(), BindFlags::ReadWrite);
258
259 const char* xauth = g_getenv("XAUTHORITY");
260 if (!xauth) {
261 const char* homeDir = g_get_home_dir();
262 GUniquePtr<char> xauthFile(g_build_filename(homeDir, ".Xauthority", nullptr));
263 bindIfExists(args, xauthFile.get());
264 } else
265 bindIfExists(args, xauth);
266}
267
268static void bindDconf(Vector<CString>& args)
269{
270 const char* runtimeDir = g_get_user_runtime_dir();
271 GUniquePtr<char> dconfRuntimeDir(g_build_filename(runtimeDir, "dconf", nullptr));
272 args.appendVector(Vector<CString>({ "--bind", dconfRuntimeDir.get(), dconfRuntimeDir.get() }));
273
274 const char* dconfDir = g_getenv("DCONF_USER_CONFIG_DIR");
275 if (dconfDir)
276 bindIfExists(args, dconfDir);
277 else {
278 const char* configDir = g_get_user_config_dir();
279 GUniquePtr<char> dconfConfigDir(g_build_filename(configDir, "dconf", nullptr));
280 bindIfExists(args, dconfConfigDir.get(), BindFlags::ReadWrite);
281 }
282}
283
284static void bindWayland(Vector<CString>& args)
285{
286 const char* display = g_getenv("WAYLAND_DISPLAY");
287 if (!display)
288 display = "wayland-0";
289
290 const char* runtimeDir = g_get_user_runtime_dir();
291 GUniquePtr<char> waylandRuntimeFile(g_build_filename(runtimeDir, display, nullptr));
292 bindIfExists(args, waylandRuntimeFile.get(), BindFlags::ReadWrite);
293}
294
295static void bindPulse(Vector<CString>& args)
296{
297 // FIXME: The server can be defined in config files we'd have to parse.
298 // They can also be set as X11 props but that is getting a bit ridiculous.
299 const char* pulseServer = g_getenv("PULSE_SERVER");
300 if (pulseServer) {
301 if (g_str_has_prefix(pulseServer, "unix:"))
302 bindIfExists(args, pulseServer + 5, BindFlags::ReadWrite);
303 // else it uses tcp
304 } else {
305 const char* runtimeDir = g_get_user_runtime_dir();
306 GUniquePtr<char> pulseRuntimeDir(g_build_filename(runtimeDir, "pulse", nullptr));
307 bindIfExists(args, pulseRuntimeDir.get(), BindFlags::ReadWrite);
308 }
309
310 const char* pulseConfig = g_getenv("PULSE_CLIENTCONFIG");
311 if (pulseConfig)
312 bindIfExists(args, pulseConfig);
313
314 const char* configDir = g_get_user_config_dir();
315 GUniquePtr<char> pulseConfigDir(g_build_filename(configDir, "pulse", nullptr));
316 bindIfExists(args, pulseConfigDir.get());
317
318 const char* homeDir = g_get_home_dir();
319 GUniquePtr<char> pulseHomeConfigDir(g_build_filename(homeDir, ".pulse", nullptr));
320 GUniquePtr<char> asoundHomeConfigDir(g_build_filename(homeDir, ".asoundrc", nullptr));
321 bindIfExists(args, pulseHomeConfigDir.get());
322 bindIfExists(args, asoundHomeConfigDir.get());
323
324 // This is the ultimate fallback to raw ALSA
325 bindIfExists(args, "/dev/snd", BindFlags::Device);
326}
327
328static void bindFonts(Vector<CString>& args)
329{
330 const char* configDir = g_get_user_config_dir();
331 const char* homeDir = g_get_home_dir();
332 const char* dataDir = g_get_user_data_dir();
333 const char* cacheDir = g_get_user_cache_dir();
334
335 // Configs can include custom dirs but then we have to parse them...
336 GUniquePtr<char> fontConfig(g_build_filename(configDir, "fontconfig", nullptr));
337 GUniquePtr<char> fontCache(g_build_filename(cacheDir, "fontconfig", nullptr));
338 GUniquePtr<char> fontHomeConfig(g_build_filename(homeDir, ".fonts.conf", nullptr));
339 GUniquePtr<char> fontHomeConfigDir(g_build_filename(configDir, ".fonts.conf.d", nullptr));
340 GUniquePtr<char> fontData(g_build_filename(dataDir, "fonts", nullptr));
341 GUniquePtr<char> fontHomeData(g_build_filename(homeDir, ".fonts", nullptr));
342 bindIfExists(args, fontConfig.get());
343 bindIfExists(args, fontCache.get(), BindFlags::ReadWrite);
344 bindIfExists(args, fontHomeConfig.get());
345 bindIfExists(args, fontHomeConfigDir.get());
346 bindIfExists(args, fontData.get());
347 bindIfExists(args, fontHomeData.get());
348}
349
350#if PLATFORM(GTK)
351static void bindGtkData(Vector<CString>& args)
352{
353 const char* configDir = g_get_user_config_dir();
354 const char* dataDir = g_get_user_data_dir();
355 const char* homeDir = g_get_home_dir();
356
357 GUniquePtr<char> gtkConfig(g_build_filename(configDir, "gtk-3.0", nullptr));
358 GUniquePtr<char> themeData(g_build_filename(dataDir, "themes", nullptr));
359 GUniquePtr<char> themeHomeData(g_build_filename(homeDir, ".themes", nullptr));
360 GUniquePtr<char> iconHomeData(g_build_filename(homeDir, ".icons", nullptr));
361 bindIfExists(args, gtkConfig.get());
362 bindIfExists(args, themeData.get());
363 bindIfExists(args, themeHomeData.get());
364 bindIfExists(args, iconHomeData.get());
365}
366
367static void bindA11y(Vector<CString>& args)
368{
369 static XDGDBusProxyLauncher proxy;
370
371 if (!proxy.isRunning()) {
372 // FIXME: Avoid blocking IO... (It is at least a one-time cost)
373 GRefPtr<GDBusConnection> sessionBus = adoptGRef(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr));
374 if (!sessionBus.get())
375 return;
376
377 GRefPtr<GDBusMessage> msg = adoptGRef(g_dbus_message_new_method_call(
378 "org.a11y.Bus", "/org/a11y/bus", "org.a11y.Bus", "GetAddress"));
379 g_dbus_message_set_body(msg.get(), g_variant_new("()"));
380 GRefPtr<GDBusMessage> reply = adoptGRef(g_dbus_connection_send_message_with_reply_sync(
381 sessionBus.get(), msg.get(),
382 G_DBUS_SEND_MESSAGE_FLAGS_NONE,
383 30000,
384 nullptr,
385 nullptr,
386 nullptr));
387
388 if (reply.get()) {
389 GUniqueOutPtr<GError> error;
390 if (g_dbus_message_to_gerror(reply.get(), &error.outPtr())) {
391 if (!g_error_matches(error.get(), G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN))
392 g_warning("Can't find a11y bus: %s", error->message);
393 } else {
394 GUniqueOutPtr<char> a11yAddress;
395 g_variant_get(g_dbus_message_get_body(reply.get()), "(s)", &a11yAddress.outPtr());
396 proxy.setAddress(a11yAddress.get(), DBusAddressType::Abstract);
397 }
398 }
399
400 proxy.setPermissions({
401 "--sloppy-names",
402 "--call=org.a11y.atspi.Registry=org.a11y.atspi.Socket.Embed@/org/a11y/atspi/accessible/root",
403 "--call=org.a11y.atspi.Registry=org.a11y.atspi.Socket.Unembed@/org/a11y/atspi/accessible/root",
404 "--call=org.a11y.atspi.Registry=org.a11y.atspi.Registry.GetRegisteredEvents@/org/a11y/atspi/registry",
405 "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.GetKeystrokeListeners@/org/a11y/atspi/registry/deviceeventcontroller",
406 "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.GetDeviceEventListeners@/org/a11y/atspi/registry/deviceeventcontroller",
407 "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.NotifyListenersSync@/org/a11y/atspi/registry/deviceeventcontroller",
408 "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.NotifyListenersAsync@/org/a11y/atspi/registry/deviceeventcontroller",
409 });
410
411 proxy.launch();
412 }
413
414 if (proxy.proxyPath().data()) {
415 args.appendVector(Vector<CString>({
416 "--bind", proxy.proxyPath(), proxy.path(),
417 }));
418 }
419}
420#endif
421
422static bool bindPathVar(Vector<CString>& args, const char* varname)
423{
424 const char* pathValue = g_getenv(varname);
425 if (!pathValue)
426 return false;
427
428 GUniquePtr<char*> splitPaths(g_strsplit(pathValue, ":", -1));
429 for (size_t i; splitPaths.get()[i]; ++i)
430 bindIfExists(args, splitPaths.get()[i]);
431
432 return true;
433}
434
435static void bindGStreamerData(Vector<CString>& args)
436{
437 if (!bindPathVar(args, "GST_PLUGIN_PATH_1_0"))
438 bindPathVar(args, "GST_PLUGIN_PATH");
439
440 if (!bindPathVar(args, "GST_PLUGIN_SYSTEM_PATH_1_0")) {
441 if (!bindPathVar(args, "GST_PLUGIN_SYSTEM_PATH")) {
442 GUniquePtr<char> gstData(g_build_filename(g_get_user_data_dir(), "gstreamer-1.0", nullptr));
443 bindIfExists(args, gstData.get());
444 }
445 }
446
447 GUniquePtr<char> gstCache(g_build_filename(g_get_user_cache_dir(), "gstreamer-1.0", nullptr));
448 bindIfExists(args, gstCache.get(), BindFlags::ReadWrite);
449
450 // /usr/lib is already added so this is only requried for other dirs
451 const char* scannerPath = g_getenv("GST_PLUGIN_SCANNER") ?: "/usr/libexec/gstreamer-1.0/gst-plugin-scanner";
452 const char* helperPath = g_getenv("GST_INSTALL_PLUGINS_HELPER ") ?: "/usr/libexec/gst-install-plugins-helper";
453
454 bindIfExists(args, scannerPath);
455 bindIfExists(args, helperPath);
456}
457
458static void bindOpenGL(Vector<CString>& args)
459{
460 args.appendVector(Vector<CString>({
461 "--dev-bind-try", "/dev/dri", "/dev/dri",
462 // Mali
463 "--dev-bind-try", "/dev/mali", "/dev/mali",
464 "--dev-bind-try", "/dev/mali0", "/dev/mali0",
465 "--dev-bind-try", "/dev/umplock", "/dev/umplock",
466 // Nvidia
467 "--dev-bind-try", "/dev/nvidiactl", "/dev/nvidiactl",
468 "--dev-bind-try", "/dev/nvidia0", "/dev/nvidia0",
469 "--dev-bind-try", "/dev/nvidia", "/dev/nvidia",
470 // Adreno
471 "--dev-bind-try", "/dev/kgsl-3d0", "/dev/kgsl-3d0",
472 "--dev-bind-try", "/dev/ion", "/dev/ion",
473#if PLATFORM(WPE)
474 "--dev-bind-try", "/dev/fb0", "/dev/fb0",
475 "--dev-bind-try", "/dev/fb1", "/dev/fb1",
476#endif
477 }));
478}
479
480static void bindV4l(Vector<CString>& args)
481{
482 args.appendVector(Vector<CString>({
483 "--dev-bind-try", "/dev/v4l", "/dev/v4l",
484 // Not pretty but a stop-gap for pipewire anyway.
485 "--dev-bind-try", "/dev/video0", "/dev/video0",
486 "--dev-bind-try", "/dev/video1", "/dev/video1",
487 }));
488}
489
490static int setupSeccomp()
491{
492 // NOTE: This is shared code (flatpak-run.c - LGPLv2.1+)
493 // There are today a number of different Linux container
494 // implementations. That will likely continue for long into the
495 // future. But we can still try to share code, and it's important
496 // to do so because it affects what library and application writers
497 // can do, and we should support code portability between different
498 // container tools.
499 //
500 // This syscall blacklist is copied from linux-user-chroot, which was in turn
501 // clearly influenced by the Sandstorm.io blacklist.
502 //
503 // If you make any changes here, I suggest sending the changes along
504 // to other sandbox maintainers. Using the libseccomp list is also
505 // an appropriate venue:
506 // https://groups.google.com/forum/#!topic/libseccomp
507 //
508 // A non-exhaustive list of links to container tooling that might
509 // want to share this blacklist:
510 //
511 // https://github.com/sandstorm-io/sandstorm
512 // in src/sandstorm/supervisor.c++
513 // http://cgit.freedesktop.org/xdg-app/xdg-app/
514 // in common/flatpak-run.c
515 // https://git.gnome.org/browse/linux-user-chroot
516 // in src/setup-seccomp.c
517 struct scmp_arg_cmp cloneArg = SCMP_A0(SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER);
518 struct scmp_arg_cmp ttyArg = SCMP_A1(SCMP_CMP_EQ, (int)TIOCSTI);
519 struct {
520 int scall;
521 struct scmp_arg_cmp* arg;
522 } syscallBlacklist[] = {
523 // Block dmesg
524 { SCMP_SYS(syslog), nullptr },
525 // Useless old syscall
526 { SCMP_SYS(uselib), nullptr },
527 // Don't allow disabling accounting
528 { SCMP_SYS(acct), nullptr },
529 // 16-bit code is unnecessary in the sandbox, and modify_ldt is a
530 // historic source of interesting information leaks.
531 { SCMP_SYS(modify_ldt), nullptr },
532 // Don't allow reading current quota use
533 { SCMP_SYS(quotactl), nullptr },
534
535 // Don't allow access to the kernel keyring
536 { SCMP_SYS(add_key), nullptr },
537 { SCMP_SYS(keyctl), nullptr },
538 { SCMP_SYS(request_key), nullptr },
539
540 // Scary VM/NUMA ops
541 { SCMP_SYS(move_pages), nullptr },
542 { SCMP_SYS(mbind), nullptr },
543 { SCMP_SYS(get_mempolicy), nullptr },
544 { SCMP_SYS(set_mempolicy), nullptr },
545 { SCMP_SYS(migrate_pages), nullptr },
546
547 // Don't allow subnamespace setups:
548 { SCMP_SYS(unshare), nullptr },
549 { SCMP_SYS(mount), nullptr },
550 { SCMP_SYS(pivot_root), nullptr },
551 { SCMP_SYS(clone), &cloneArg },
552
553 // Don't allow faking input to the controlling tty (CVE-2017-5226)
554 { SCMP_SYS(ioctl), &ttyArg },
555
556 // Profiling operations; we expect these to be done by tools from outside
557 // the sandbox. In particular perf has been the source of many CVEs.
558 { SCMP_SYS(perf_event_open), nullptr },
559 // Don't allow you to switch to bsd emulation or whatnot.
560 { SCMP_SYS(personality), nullptr },
561 { SCMP_SYS(ptrace), nullptr }
562 };
563
564 scmp_filter_ctx seccomp = seccomp_init(SCMP_ACT_ALLOW);
565 if (!seccomp)
566 g_error("Failed to init seccomp");
567
568 for (auto& rule : syscallBlacklist) {
569 int scall = rule.scall;
570 int r;
571 if (rule.arg)
572 r = seccomp_rule_add(seccomp, SCMP_ACT_ERRNO(EPERM), scall, 1, rule.arg);
573 else
574 r = seccomp_rule_add(seccomp, SCMP_ACT_ERRNO(EPERM), scall, 0);
575 if (r == -EFAULT) {
576 seccomp_release(seccomp);
577 g_error("Failed to add seccomp rule");
578 }
579 }
580
581 int tmpfd = memfd_create("seccomp-bpf", 0);
582 if (tmpfd == -1) {
583 seccomp_release(seccomp);
584 g_error("Failed to create memfd: %s", g_strerror(errno));
585 }
586
587 if (seccomp_export_bpf(seccomp, tmpfd)) {
588 seccomp_release(seccomp);
589 close(tmpfd);
590 g_error("Failed to export seccomp bpf");
591 }
592
593 if (lseek(tmpfd, 0, SEEK_SET) < 0)
594 g_error("lseek failed: %s", g_strerror(errno));
595
596 seccomp_release(seccomp);
597 return tmpfd;
598}
599
600GRefPtr<GSubprocess> bubblewrapSpawn(GSubprocessLauncher* launcher, const ProcessLauncher::LaunchOptions& launchOptions, char** argv, GError **error)
601{
602 ASSERT(launcher);
603
604 // It is impossible to know what access arbitrary plugins need and since it is for legacy
605 // reasons lets just leave it unsandboxed.
606 if (launchOptions.processType == ProcessLauncher::ProcessType::Plugin64
607 || launchOptions.processType == ProcessLauncher::ProcessType::Plugin32)
608 return g_subprocess_launcher_spawnv(launcher, argv, error);
609
610 // NOTE: This is not a great solution but we just assume that applications create this directory
611 // ahead of time if they require it.
612 GUniquePtr<char> configDir(g_build_filename(g_get_user_config_dir(), g_get_prgname(), nullptr));
613
614 Vector<CString> sandboxArgs = {
615 "--die-with-parent",
616 "--unshare-pid",
617 "--unshare-uts",
618
619 // We assume /etc has safe permissions.
620 // At a later point we can start masking privacy-concerning files.
621 "--ro-bind", "/etc", "/etc",
622 "--dev", "/dev",
623 "--proc", "/proc",
624 "--tmpfs", "/tmp",
625 "--unsetenv", "TMPDIR",
626 "--dir", "/run",
627 "--symlink", "../run", "/var/run",
628 "--symlink", "../tmp", "/var/tmp",
629 "--ro-bind", "/sys/block", "/sys/block",
630 "--ro-bind", "/sys/bus", "/sys/bus",
631 "--ro-bind", "/sys/class", "/sys/class",
632 "--ro-bind", "/sys/dev", "/sys/dev",
633 "--ro-bind", "/sys/devices", "/sys/devices",
634
635 "--ro-bind-try", "/usr/share", "/usr/share",
636 "--ro-bind-try", "/usr/local/share", "/usr/local/share",
637 "--ro-bind-try", DATADIR, DATADIR,
638
639 // We only grant access to the libdirs webkit is built with and
640 // guess system libdirs. This will always have some edge cases.
641 "--ro-bind-try", "/lib", "/lib",
642 "--ro-bind-try", "/usr/lib", "/usr/lib",
643 "--ro-bind-try", "/usr/local/lib", "/usr/local/lib",
644 "--ro-bind-try", LIBDIR, LIBDIR,
645 "--ro-bind-try", "/lib64", "/lib64",
646 "--ro-bind-try", "/usr/lib64", "/usr/lib64",
647 "--ro-bind-try", "/usr/local/lib64", "/usr/local/lib64",
648
649 "--ro-bind-try", PKGLIBEXECDIR, PKGLIBEXECDIR,
650
651 "--setenv", "GTK_USE_PORTAL", "1",
652
653 "--bind-try", configDir.get(), configDir.get(),
654 };
655 // We would have to parse ld config files for more info.
656 bindPathVar(sandboxArgs, "LD_LIBRARY_PATH");
657
658 if (launchOptions.processType == ProcessLauncher::ProcessType::Network) {
659 static XDGDBusProxyLauncher proxy;
660
661 // FIXME: The network process is used for `file://` URIs and
662 // we would have to pass through most paths for this to work.
663
664 // FIXME: HTTP credentials talks to libsecret.
665
666 // glib-networking can use dbus for proxy information.
667 // FIXME: Find and add the permissions it needs for this (pacrunner).
668 bindDBusSession(sandboxArgs, proxy);
669
670 if (!proxy.isRunning())
671 proxy.launch();
672 }
673
674 // NOTE: This has network access for HLS via GStreamer.
675 if (launchOptions.processType == ProcessLauncher::ProcessType::Web) {
676 static XDGDBusProxyLauncher proxy;
677
678 // If Wayland in use don't grant X11
679#if PLATFORM(WAYLAND) && USE(EGL)
680 if (PlatformDisplay::sharedDisplay().type() == PlatformDisplay::Type::Wayland) {
681 bindWayland(sandboxArgs);
682 sandboxArgs.append("--unshare-ipc");
683 } else
684#endif
685 bindX11(sandboxArgs);
686
687 bindDBusSession(sandboxArgs, proxy);
688 // FIXME: This needs to be restricted, upstream is working on it.
689 bindDconf(sandboxArgs);
690 // FIXME: We should move to Pipewire as soon as viable, Pulse doesn't restrict clients atm.
691 bindPulse(sandboxArgs);
692 bindFonts(sandboxArgs);
693 bindGStreamerData(sandboxArgs);
694 bindOpenGL(sandboxArgs);
695 // FIXME: This is also fixed by Pipewire once in use.
696 bindV4l(sandboxArgs);
697#if PLATFORM(GTK)
698 bindA11y(sandboxArgs);
699 bindGtkData(sandboxArgs);
700#endif
701
702 if (!proxy.isRunning()) {
703 proxy.setPermissions({
704 // FIXME: Used by GTK on Wayland.
705 "--talk=ca.desrt.dconf",
706 // xdg-desktop-portal used by GTK and us.
707 "--talk=org.freedesktop.portal.Desktop",
708 // GStreamers plugin install helper.
709 "--call=org.freedesktop.PackageKit=org.freedesktop.PackageKit.Modify2.InstallGStreamerResources@/org/freedesktop/PackageKit"
710 });
711 proxy.launch();
712 }
713
714
715 } else {
716 // Only X11 users need this for XShm which is only the Web process.
717 sandboxArgs.append("--unshare-ipc");
718 }
719
720 // Only process without any network access for now.
721 if (launchOptions.processType == ProcessLauncher::ProcessType::Storage)
722 sandboxArgs.append("--unshare-net");
723
724#if ENABLE(DEVELOPER_MODE)
725 const char* execDirectory = g_getenv("WEBKIT_EXEC_PATH");
726 if (execDirectory) {
727 String parentDir = FileSystem::directoryName(FileSystem::stringFromFileSystemRepresentation(execDirectory));
728 bindIfExists(sandboxArgs, parentDir.utf8().data());
729 }
730
731 CString executablePath = getCurrentExecutablePath();
732 if (!executablePath.isNull()) {
733 // Our executable is `/foo/bar/bin/Process`, we want `/foo/bar` as a usable prefix
734 String parentDir = FileSystem::directoryName(FileSystem::directoryName(FileSystem::stringFromFileSystemRepresentation(executablePath.data())));
735 bindIfExists(sandboxArgs, parentDir.utf8().data());
736 }
737#endif
738
739 int seccompFd = setupSeccomp();
740 GUniquePtr<char> fdStr(g_strdup_printf("%d", seccompFd));
741 g_subprocess_launcher_take_fd(launcher, seccompFd, seccompFd);
742 sandboxArgs.appendVector(Vector<CString>({ "--seccomp", fdStr.get() }));
743
744 for (const String& path : launchOptions.extraSandboxPaths) {
745 if (path.isEmpty())
746 g_warning("Empty path passed to sandbox, this is probably a bug");
747 else if (!FileSystem::makeAllDirectories(path)) // FIXME: Blocking
748 g_warning("Could not create directory \"%s\": %s", path.utf8().data(), g_strerror(errno));
749 else
750 sandboxArgs.appendVector(Vector<CString>({ "--bind", path.utf8(), path.utf8() }));
751 }
752
753 int bwrapFd = argsToFd(sandboxArgs, "bwrap");
754 GUniquePtr<char> bwrapFdStr(g_strdup_printf("%d", bwrapFd));
755 g_subprocess_launcher_take_fd(launcher, bwrapFd, bwrapFd);
756
757 Vector<CString> bwrapArgs = {
758 BWRAP_EXECUTABLE,
759 "--args",
760 bwrapFdStr.get(),
761 "--",
762 };
763
764 char** newArgv = g_newa(char*, g_strv_length(argv) + bwrapArgs.size() + 1);
765 size_t i = 0;
766
767 for (auto& arg : bwrapArgs)
768 newArgv[i++] = const_cast<char*>(arg.data());
769 for (size_t x = 0; argv[x]; x++)
770 newArgv[i++] = argv[x];
771 newArgv[i++] = nullptr;
772
773 return adoptGRef(g_subprocess_launcher_spawnv(launcher, newArgv, error));
774}
775
776};
777
778#endif // ENABLE(BUBBLEWRAP_SANDBOX)