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