| 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 | |