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 args.appendVector(Vector<CString>({
445 "--bind", proxy.proxyPath(), proxy.path(),
446 }));
447 }
448}
449#endif
450
451static bool bindPathVar(Vector<CString>& args, const char* varname)
452{
453 const char* pathValue = g_getenv(varname);
454 if (!pathValue)
455 return false;
456
457 GUniquePtr<char*> splitPaths(g_strsplit(pathValue, ":", -1));
458 for (size_t i = 0; splitPaths.get()[i]; ++i)
459 bindIfExists(args, splitPaths.get()[i]);
460
461 return true;
462}
463
464static void bindGStreamerData(Vector<CString>& args)
465{
466 if (!bindPathVar(args, "GST_PLUGIN_PATH_1_0"))
467 bindPathVar(args, "GST_PLUGIN_PATH");
468
469 if (!bindPathVar(args, "GST_PLUGIN_SYSTEM_PATH_1_0")) {
470 if (!bindPathVar(args, "GST_PLUGIN_SYSTEM_PATH")) {
471 GUniquePtr<char> gstData(g_build_filename(g_get_user_data_dir(), "gstreamer-1.0", nullptr));
472 bindIfExists(args, gstData.get());
473 }
474 }
475
476 GUniquePtr<char> gstCache(g_build_filename(g_get_user_cache_dir(), "gstreamer-1.0", nullptr));
477 bindIfExists(args, gstCache.get(), BindFlags::ReadWrite);
478
479 // /usr/lib is already added so this is only requried for other dirs
480 const char* scannerPath = g_getenv("GST_PLUGIN_SCANNER") ?: "/usr/libexec/gstreamer-1.0/gst-plugin-scanner";
481 const char* helperPath = g_getenv("GST_INSTALL_PLUGINS_HELPER ") ?: "/usr/libexec/gst-install-plugins-helper";
482
483 bindIfExists(args, scannerPath);
484 bindIfExists(args, helperPath);
485}
486
487static void bindOpenGL(Vector<CString>& args)
488{
489 args.appendVector(Vector<CString>({
490 "--dev-bind-try", "/dev/dri", "/dev/dri",
491 // Mali
492 "--dev-bind-try", "/dev/mali", "/dev/mali",
493 "--dev-bind-try", "/dev/mali0", "/dev/mali0",
494 "--dev-bind-try", "/dev/umplock", "/dev/umplock",
495 // Nvidia
496 "--dev-bind-try", "/dev/nvidiactl", "/dev/nvidiactl",
497 "--dev-bind-try", "/dev/nvidia0", "/dev/nvidia0",
498 "--dev-bind-try", "/dev/nvidia", "/dev/nvidia",
499 // Adreno
500 "--dev-bind-try", "/dev/kgsl-3d0", "/dev/kgsl-3d0",
501 "--dev-bind-try", "/dev/ion", "/dev/ion",
502#if PLATFORM(WPE)
503 "--dev-bind-try", "/dev/fb0", "/dev/fb0",
504 "--dev-bind-try", "/dev/fb1", "/dev/fb1",
505#endif
506 }));
507}
508
509static void bindV4l(Vector<CString>& args)
510{
511 args.appendVector(Vector<CString>({
512 "--dev-bind-try", "/dev/v4l", "/dev/v4l",
513 // Not pretty but a stop-gap for pipewire anyway.
514 "--dev-bind-try", "/dev/video0", "/dev/video0",
515 "--dev-bind-try", "/dev/video1", "/dev/video1",
516 }));
517}
518
519static void bindSymlinksRealPath(Vector<CString>& args, const char* path)
520{
521 char realPath[PATH_MAX];
522
523 if (realpath(path, realPath) && strcmp(path, realPath)) {
524 args.appendVector(Vector<CString>({
525 "--ro-bind", realPath, realPath,
526 }));
527 }
528}
529
530static int setupSeccomp()
531{
532 // NOTE: This is shared code (flatpak-run.c - LGPLv2.1+)
533 // There are today a number of different Linux container
534 // implementations. That will likely continue for long into the
535 // future. But we can still try to share code, and it's important
536 // to do so because it affects what library and application writers
537 // can do, and we should support code portability between different
538 // container tools.
539 //
540 // This syscall blacklist is copied from linux-user-chroot, which was in turn
541 // clearly influenced by the Sandstorm.io blacklist.
542 //
543 // If you make any changes here, I suggest sending the changes along
544 // to other sandbox maintainers. Using the libseccomp list is also
545 // an appropriate venue:
546 // https://groups.google.com/forum/#!topic/libseccomp
547 //
548 // A non-exhaustive list of links to container tooling that might
549 // want to share this blacklist:
550 //
551 // https://github.com/sandstorm-io/sandstorm
552 // in src/sandstorm/supervisor.c++
553 // http://cgit.freedesktop.org/xdg-app/xdg-app/
554 // in common/flatpak-run.c
555 // https://git.gnome.org/browse/linux-user-chroot
556 // in src/setup-seccomp.c
557 struct scmp_arg_cmp cloneArg = SCMP_A0(SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER);
558 struct scmp_arg_cmp ttyArg = SCMP_A1(SCMP_CMP_MASKED_EQ, 0xFFFFFFFFu, TIOCSTI);
559 struct {
560 int scall;
561 struct scmp_arg_cmp* arg;
562 } syscallBlacklist[] = {
563 // Block dmesg
564 { SCMP_SYS(syslog), nullptr },
565 // Useless old syscall.
566 { SCMP_SYS(uselib), nullptr },
567 // Don't allow disabling accounting.
568 { SCMP_SYS(acct), nullptr },
569 // 16-bit code is unnecessary in the sandbox, and modify_ldt is a
570 // historic source of interesting information leaks.
571 { SCMP_SYS(modify_ldt), nullptr },
572 // Don't allow reading current quota use.
573 { SCMP_SYS(quotactl), nullptr },
574
575 // Don't allow access to the kernel keyring.
576 { SCMP_SYS(add_key), nullptr },
577 { SCMP_SYS(keyctl), nullptr },
578 { SCMP_SYS(request_key), nullptr },
579
580 // Scary VM/NUMA ops
581 { SCMP_SYS(move_pages), nullptr },
582 { SCMP_SYS(mbind), nullptr },
583 { SCMP_SYS(get_mempolicy), nullptr },
584 { SCMP_SYS(set_mempolicy), nullptr },
585 { SCMP_SYS(migrate_pages), nullptr },
586
587 // Don't allow subnamespace setups:
588 { SCMP_SYS(unshare), nullptr },
589 { SCMP_SYS(mount), nullptr },
590 { SCMP_SYS(pivot_root), nullptr },
591 { SCMP_SYS(clone), &cloneArg },
592
593 // Don't allow faking input to the controlling tty (CVE-2017-5226)
594 { SCMP_SYS(ioctl), &ttyArg },
595
596 // Profiling operations; we expect these to be done by tools from outside
597 // the sandbox. In particular perf has been the source of many CVEs.
598 { SCMP_SYS(perf_event_open), nullptr },
599 // Don't allow you to switch to bsd emulation or whatnot.
600 { SCMP_SYS(personality), nullptr },
601 { SCMP_SYS(ptrace), nullptr }
602 };
603
604 scmp_filter_ctx seccomp = seccomp_init(SCMP_ACT_ALLOW);
605 if (!seccomp)
606 g_error("Failed to init seccomp");
607
608 for (auto& rule : syscallBlacklist) {
609 int scall = rule.scall;
610 int r;
611 if (rule.arg)
612 r = seccomp_rule_add(seccomp, SCMP_ACT_ERRNO(EPERM), scall, 1, rule.arg);
613 else
614 r = seccomp_rule_add(seccomp, SCMP_ACT_ERRNO(EPERM), scall, 0);
615 if (r == -EFAULT) {
616 seccomp_release(seccomp);
617 g_error("Failed to add seccomp rule");
618 }
619 }
620
621 int tmpfd = memfd_create("seccomp-bpf", 0);
622 if (tmpfd == -1) {
623 seccomp_release(seccomp);
624 g_error("Failed to create memfd: %s", g_strerror(errno));
625 }
626
627 if (seccomp_export_bpf(seccomp, tmpfd)) {
628 seccomp_release(seccomp);
629 close(tmpfd);
630 g_error("Failed to export seccomp bpf");
631 }
632
633 if (lseek(tmpfd, 0, SEEK_SET) < 0)
634 g_error("lseek failed: %s", g_strerror(errno));
635
636 seccomp_release(seccomp);
637 return tmpfd;
638}
639
640static int createFlatpakInfo()
641{
642 GUniquePtr<GKeyFile> keyFile(g_key_file_new());
643
644 // xdg-desktop-portal relates your name to certain permissions so we want
645 // them to be application unique which is best done via GApplication.
646 GApplication* app = g_application_get_default();
647 if (!app) {
648 g_warning("GApplication is required for xdg-desktop-portal access in the WebKit sandbox. Actions that require xdg-desktop-portal will be broken.");
649 return -1;
650 }
651 g_key_file_set_string(keyFile.get(), "Application", "name", g_application_get_application_id(app));
652
653 size_t size;
654 GUniqueOutPtr<GError> error;
655 GUniquePtr<char> data(g_key_file_to_data(keyFile.get(), &size, &error.outPtr()));
656 if (error.get()) {
657 g_warning("%s", error->message);
658 return -1;
659 }
660
661 return createSealedMemFdWithData("flatpak-info", data.get(), size);
662}
663
664GRefPtr<GSubprocess> bubblewrapSpawn(GSubprocessLauncher* launcher, const ProcessLauncher::LaunchOptions& launchOptions, char** argv, GError **error)
665{
666 ASSERT(launcher);
667
668#if ENABLE(NETSCAPE_PLUGIN_API)
669 // It is impossible to know what access arbitrary plugins need and since it is for legacy
670 // reasons lets just leave it unsandboxed.
671 if (launchOptions.processType == ProcessLauncher::ProcessType::Plugin64
672 || launchOptions.processType == ProcessLauncher::ProcessType::Plugin32)
673 return adoptGRef(g_subprocess_launcher_spawnv(launcher, argv, error));
674#endif
675
676 // For now we are just considering the network process trusted as it
677 // requires a lot of access but doesn't execute arbitrary code like
678 // the WebProcess where our focus lies.
679 if (launchOptions.processType == ProcessLauncher::ProcessType::Network)
680 return adoptGRef(g_subprocess_launcher_spawnv(launcher, argv, error));
681
682 Vector<CString> sandboxArgs = {
683 "--die-with-parent",
684 "--unshare-pid",
685 "--unshare-uts",
686 "--unshare-net",
687
688 // We assume /etc has safe permissions.
689 // At a later point we can start masking privacy-concerning files.
690 "--ro-bind", "/etc", "/etc",
691 "--dev", "/dev",
692 "--proc", "/proc",
693 "--tmpfs", "/tmp",
694 "--unsetenv", "TMPDIR",
695 "--dir", "/run",
696 "--symlink", "../run", "/var/run",
697 "--symlink", "../tmp", "/var/tmp",
698 "--ro-bind", "/sys/block", "/sys/block",
699 "--ro-bind", "/sys/bus", "/sys/bus",
700 "--ro-bind", "/sys/class", "/sys/class",
701 "--ro-bind", "/sys/dev", "/sys/dev",
702 "--ro-bind", "/sys/devices", "/sys/devices",
703
704 "--ro-bind-try", "/usr/share", "/usr/share",
705 "--ro-bind-try", "/usr/local/share", "/usr/local/share",
706 "--ro-bind-try", DATADIR, DATADIR,
707
708 // We only grant access to the libdirs webkit is built with and
709 // guess system libdirs. This will always have some edge cases.
710 "--ro-bind-try", "/lib", "/lib",
711 "--ro-bind-try", "/usr/lib", "/usr/lib",
712 "--ro-bind-try", "/usr/local/lib", "/usr/local/lib",
713 "--ro-bind-try", LIBDIR, LIBDIR,
714 "--ro-bind-try", "/lib64", "/lib64",
715 "--ro-bind-try", "/usr/lib64", "/usr/lib64",
716 "--ro-bind-try", "/usr/local/lib64", "/usr/local/lib64",
717
718 "--ro-bind-try", PKGLIBEXECDIR, PKGLIBEXECDIR,
719 };
720 // We would have to parse ld config files for more info.
721 bindPathVar(sandboxArgs, "LD_LIBRARY_PATH");
722
723 const char* libraryPath = g_getenv("LD_LIBRARY_PATH");
724 if (libraryPath && libraryPath[0]) {
725 // On distros using a suid bwrap it drops this env var
726 // so we have to pass it through to the children.
727 sandboxArgs.appendVector(Vector<CString>({
728 "--setenv", "LD_LIBRARY_PATH", libraryPath,
729 }));
730 }
731
732 bindSymlinksRealPath(sandboxArgs, "/etc/resolv.conf");
733 bindSymlinksRealPath(sandboxArgs, "/etc/localtime");
734
735 // xdg-desktop-portal defaults to assuming you are host application with
736 // full permissions unless it can identify you as a snap or flatpak.
737 // The easiest method is for us to pretend to be a flatpak and if that
738 // fails just blocking portals entirely as it just becomes a sandbox escape.
739 int flatpakInfoFd = createFlatpakInfo();
740 if (flatpakInfoFd != -1) {
741 g_subprocess_launcher_take_fd(launcher, flatpakInfoFd, flatpakInfoFd);
742 GUniquePtr<char> flatpakInfoFdStr(g_strdup_printf("%d", flatpakInfoFd));
743
744 sandboxArgs.appendVector(Vector<CString>({
745 "--ro-bind-data", flatpakInfoFdStr.get(), "/.flatpak-info"
746 }));
747 }
748
749 if (launchOptions.processType == ProcessLauncher::ProcessType::Web) {
750 static XDGDBusProxyLauncher proxy;
751
752 // If Wayland in use don't grant X11
753#if PLATFORM(WAYLAND) && USE(EGL)
754 if (PlatformDisplay::sharedDisplay().type() == PlatformDisplay::Type::Wayland) {
755 bindWayland(sandboxArgs);
756 sandboxArgs.append("--unshare-ipc");
757 } else
758#endif
759 bindX11(sandboxArgs);
760
761 for (const auto& pathAndPermission : launchOptions.extraWebProcessSandboxPaths) {
762 sandboxArgs.appendVector(Vector<CString>({
763 pathAndPermission.value == SandboxPermission::ReadOnly ? "--ro-bind-try": "--bind-try",
764 pathAndPermission.key, pathAndPermission.key
765 }));
766 }
767
768 Vector<String> extraPaths = { "applicationCacheDirectory", "mediaKeysDirectory", "waylandSocket", "webSQLDatabaseDirectory" };
769 for (const auto& path : extraPaths) {
770 String extraPath = launchOptions.extraInitializationData.get(path);
771 if (!extraPath.isEmpty())
772 sandboxArgs.appendVector(Vector<CString>({ "--bind-try", extraPath.utf8(), extraPath.utf8() }));
773 }
774
775 bindDBusSession(sandboxArgs, proxy);
776 // FIXME: We should move to Pipewire as soon as viable, Pulse doesn't restrict clients atm.
777 bindPulse(sandboxArgs);
778 bindFonts(sandboxArgs);
779 bindGStreamerData(sandboxArgs);
780 bindOpenGL(sandboxArgs);
781 // FIXME: This is also fixed by Pipewire once in use.
782 bindV4l(sandboxArgs);
783#if PLATFORM(GTK)
784 bindA11y(sandboxArgs);
785 bindGtkData(sandboxArgs);
786#endif
787
788 if (!proxy.isRunning()) {
789 Vector<CString> permissions = {
790 // GStreamers plugin install helper.
791 "--call=org.freedesktop.PackageKit=org.freedesktop.PackageKit.Modify2.InstallGStreamerResources@/org/freedesktop/PackageKit"
792 };
793 if (flatpakInfoFd != -1) {
794 // xdg-desktop-portal used by GTK and us.
795 permissions.append("--talk=org.freedesktop.portal.Desktop");
796 }
797 proxy.setPermissions(WTFMove(permissions));
798 proxy.launch();
799 }
800 } else {
801 // Only X11 users need this for XShm which is only the Web process.
802 sandboxArgs.append("--unshare-ipc");
803 }
804
805#if ENABLE(DEVELOPER_MODE)
806 const char* execDirectory = g_getenv("WEBKIT_EXEC_PATH");
807 if (execDirectory) {
808 String parentDir = FileSystem::directoryName(FileSystem::stringFromFileSystemRepresentation(execDirectory));
809 bindIfExists(sandboxArgs, parentDir.utf8().data());
810 }
811
812 CString executablePath = getCurrentExecutablePath();
813 if (!executablePath.isNull()) {
814 // Our executable is `/foo/bar/bin/Process`, we want `/foo/bar` as a usable prefix
815 String parentDir = FileSystem::directoryName(FileSystem::directoryName(FileSystem::stringFromFileSystemRepresentation(executablePath.data())));
816 bindIfExists(sandboxArgs, parentDir.utf8().data());
817 }
818#endif
819
820 int seccompFd = setupSeccomp();
821 GUniquePtr<char> fdStr(g_strdup_printf("%d", seccompFd));
822 g_subprocess_launcher_take_fd(launcher, seccompFd, seccompFd);
823 sandboxArgs.appendVector(Vector<CString>({ "--seccomp", fdStr.get() }));
824
825 int bwrapFd = argsToFd(sandboxArgs, "bwrap");
826 GUniquePtr<char> bwrapFdStr(g_strdup_printf("%d", bwrapFd));
827 g_subprocess_launcher_take_fd(launcher, bwrapFd, bwrapFd);
828
829 Vector<CString> bwrapArgs = {
830 BWRAP_EXECUTABLE,
831 "--args",
832 bwrapFdStr.get(),
833 "--",
834 };
835
836 char** newArgv = g_newa(char*, g_strv_length(argv) + bwrapArgs.size() + 1);
837 size_t i = 0;
838
839 for (auto& arg : bwrapArgs)
840 newArgv[i++] = const_cast<char*>(arg.data());
841 for (size_t x = 0; argv[x]; x++)
842 newArgv[i++] = argv[x];
843 newArgv[i++] = nullptr;
844
845 return adoptGRef(g_subprocess_launcher_spawnv(launcher, newArgv, error));
846}
847
848};
849
850#endif // ENABLE(BUBBLEWRAP_SANDBOX)
851