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 | args.appendVector(Vector<CString>({ |
445 | "--bind" , proxy.proxyPath(), proxy.path(), |
446 | })); |
447 | } |
448 | } |
449 | #endif |
450 | |
451 | static 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 | |
464 | static 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 | |
487 | static 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 | |
509 | static 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 | |
519 | static 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 | |
530 | static 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 | |
640 | static 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 | |
664 | GRefPtr<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> = { "applicationCacheDirectory" , "mediaKeysDirectory" , "waylandSocket" , "webSQLDatabaseDirectory" }; |
769 | for (const auto& path : extraPaths) { |
770 | String = 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 | |