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 | |
54 | static int memfd_create(const char* name, unsigned flags) |
55 | { |
56 | return syscall(__NR_memfd_create, name, flags); |
57 | } |
58 | #endif |
59 | |
60 | namespace WebKit { |
61 | using namespace WebCore; |
62 | |
63 | static 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 | |
99 | static int |
100 | argsToFd(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 | |
119 | enum class DBusAddressType { |
120 | Normal, |
121 | Abstract, |
122 | }; |
123 | |
124 | class XDGDBusProxyLauncher { |
125 | public: |
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 | |
209 | private: |
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 | |
261 | enum class BindFlags { |
262 | ReadOnly, |
263 | ReadWrite, |
264 | Device, |
265 | }; |
266 | |
267 | static 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 | |
282 | static 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 | |
294 | static 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) |
312 | static 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 | |
324 | static 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 | |
357 | static 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) |
380 | static 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 | |
396 | static 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 | |
453 | static 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 | |
466 | static 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 | |
489 | static 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 | |
511 | static 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 | |
521 | static 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 | |
532 | static 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 | |
642 | static 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 | |
666 | GRefPtr<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 | |