| /* |
| * Copyright (C) 2018 Igalia S.L. |
| * |
| * This program is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Lesser General Public |
| * License as published by the Free Software Foundation; either |
| * version 2.1 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Lesser General Public License for more details. |
| * |
| * You should have received a copy of the GNU Lesser General Public |
| * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
| */ |
| |
| #include "config.h" |
| #include "BubblewrapLauncher.h" |
| |
| #if ENABLE(BUBBLEWRAP_SANDBOX) |
| |
| #include <WebCore/PlatformDisplay.h> |
| #include <fcntl.h> |
| #include <glib.h> |
| #include <seccomp.h> |
| #include <sys/ioctl.h> |
| #include <wtf/FileSystem.h> |
| #include <wtf/glib/GLibUtilities.h> |
| #include <wtf/glib/GRefPtr.h> |
| #include <wtf/glib/GUniquePtr.h> |
| |
| #if PLATFORM(GTK) |
| #define BASE_DIRECTORY "webkitgtk" |
| #elif PLATFORM(WPE) |
| #define BASE_DIRECTORY "wpe" |
| #endif |
| |
| #if __has_include(<sys/memfd.h>) |
| |
| #include <sys/memfd.h> |
| |
| #else |
| |
| // These defines were added in glibc 2.27, the same release that added memfd_create. |
| // But the kernel added all of this in Linux 3.17. So it's totally safe for us to |
| // depend on, as long as we define it all ourselves. Remove this once we depend on |
| // glibc 2.27. |
| |
| #define F_ADD_SEALS 1033 |
| #define F_GET_SEALS 1034 |
| |
| #define F_SEAL_SEAL 0x0001 |
| #define F_SEAL_SHRINK 0x0002 |
| #define F_SEAL_GROW 0x0004 |
| #define F_SEAL_WRITE 0x0008 |
| |
| #define MFD_ALLOW_SEALING 2U |
| |
| static int memfd_create(const char* name, unsigned flags) |
| { |
| return syscall(__NR_memfd_create, name, flags); |
| } |
| #endif |
| |
| namespace WebKit { |
| using namespace WebCore; |
| |
| static int createSealedMemFdWithData(const char* name, gconstpointer data, size_t size) |
| { |
| int fd = memfd_create(name, MFD_ALLOW_SEALING); |
| if (fd == -1) { |
| g_warning("memfd_create failed: %s", g_strerror(errno)); |
| return -1; |
| } |
| |
| ssize_t bytesWritten = write(fd, data, size); |
| if (bytesWritten < 0) { |
| g_warning("Writing args to memfd failed: %s", g_strerror(errno)); |
| close(fd); |
| return -1; |
| } |
| |
| if (static_cast<size_t>(bytesWritten) != size) { |
| g_warning("Failed to write all args to memfd"); |
| close(fd); |
| return -1; |
| } |
| |
| if (lseek(fd, 0, SEEK_SET) == -1) { |
| g_warning("lseek failed: %s", g_strerror(errno)); |
| close(fd); |
| return -1; |
| } |
| |
| if (fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE | F_SEAL_SEAL) == -1) { |
| g_warning("Failed to seal memfd: %s", g_strerror(errno)); |
| close(fd); |
| return -1; |
| } |
| |
| return fd; |
| } |
| |
| static int |
| argsToFd(const Vector<CString>& args, const char *name) |
| { |
| GString* buffer = g_string_new(nullptr); |
| |
| for (const auto& arg : args) |
| g_string_append_len(buffer, arg.data(), arg.length() + 1); // Include NUL |
| |
| GRefPtr<GBytes> bytes = adoptGRef(g_string_free_to_bytes(buffer)); |
| |
| size_t size; |
| gconstpointer data = g_bytes_get_data(bytes.get(), &size); |
| |
| int memfd = createSealedMemFdWithData(name, data, size); |
| if (memfd == -1) |
| g_error("Failed to write memfd"); |
| |
| return memfd; |
| } |
| |
| enum class DBusAddressType { |
| Normal, |
| Abstract, |
| }; |
| |
| class XDGDBusProxyLauncher { |
| public: |
| void setAddress(const char* dbusAddress, DBusAddressType addressType) |
| { |
| CString dbusPath = dbusAddressToPath(dbusAddress, addressType); |
| if (dbusPath.isNull()) |
| return; |
| |
| GUniquePtr<char> appRunDir(g_build_filename(g_get_user_runtime_dir(), BASE_DIRECTORY, nullptr)); |
| m_proxyPath = makeProxyPath(appRunDir.get()); |
| |
| m_socket = dbusAddress; |
| m_path = WTFMove(dbusPath); |
| } |
| |
| bool isRunning() const { return m_isRunning; }; |
| const CString& path() const { return m_path; }; |
| const CString& proxyPath() const { return m_proxyPath; }; |
| |
| void setPermissions(Vector<CString>&& permissions) |
| { |
| RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!isRunning()); |
| m_permissions = WTFMove(permissions); |
| }; |
| |
| void launch() |
| { |
| RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(!isRunning()); |
| |
| if (m_socket.isNull() || m_path.isNull() || m_proxyPath.isNull()) |
| return; |
| |
| int syncFds[2]; |
| if (pipe2(syncFds, O_CLOEXEC) == -1) |
| g_error("Failed to make syncfds for dbus-proxy: %s", g_strerror(errno)); |
| |
| GUniquePtr<char> syncFdStr(g_strdup_printf("--fd=%d", syncFds[1])); |
| |
| Vector<CString> proxyArgs = { |
| m_socket, m_proxyPath, |
| "--filter", |
| syncFdStr.get(), |
| }; |
| |
| if (!g_strcmp0(g_getenv("WEBKIT_ENABLE_DBUS_PROXY_LOGGING"), "1")) |
| proxyArgs.append("--log"); |
| |
| proxyArgs.appendVector(m_permissions); |
| |
| int proxyFd = argsToFd(proxyArgs, "dbus-proxy"); |
| GUniquePtr<char> proxyArgsStr(g_strdup_printf("--args=%d", proxyFd)); |
| |
| Vector<CString> args = { |
| DBUS_PROXY_EXECUTABLE, |
| proxyArgsStr.get(), |
| }; |
| |
| int nargs = args.size() + 1; |
| int i = 0; |
| char** argv = g_newa(char*, nargs); |
| for (const auto& arg : args) |
| argv[i++] = const_cast<char*>(arg.data()); |
| argv[i] = nullptr; |
| |
| GRefPtr<GSubprocessLauncher> launcher = adoptGRef(g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_INHERIT_FDS)); |
| g_subprocess_launcher_set_child_setup(launcher.get(), childSetupFunc, GINT_TO_POINTER(syncFds[1]), nullptr); |
| g_subprocess_launcher_take_fd(launcher.get(), proxyFd, proxyFd); |
| g_subprocess_launcher_take_fd(launcher.get(), syncFds[1], syncFds[1]); |
| // We are purposefully leaving syncFds[0] open here. |
| // xdg-dbus-proxy will exit() itself once that is closed on our exit |
| |
| GUniqueOutPtr<GError> error; |
| GRefPtr<GSubprocess> process = adoptGRef(g_subprocess_launcher_spawnv(launcher.get(), argv, &error.outPtr())); |
| if (!process.get()) |
| g_error("Failed to start dbus proxy: %s", error->message); |
| |
| char out; |
| // We need to ensure the proxy has created the socket. |
| // FIXME: This is more blocking IO. |
| if (read(syncFds[0], &out, 1) != 1) |
| g_error("Failed to fully launch dbus-proxy %s", g_strerror(errno)); |
| |
| m_isRunning = true; |
| }; |
| |
| private: |
| static void childSetupFunc(gpointer userdata) |
| { |
| int fd = GPOINTER_TO_INT(userdata); |
| fcntl(fd, F_SETFD, 0); // Unset CLOEXEC |
| } |
| |
| static CString makeProxyPath(const char* appRunDir) |
| { |
| if (g_mkdir_with_parents(appRunDir, 0700) == -1) { |
| g_warning("Failed to mkdir for dbus proxy (%s): %s", appRunDir, g_strerror(errno)); |
| return { }; |
| } |
| |
| GUniquePtr<char> proxySocketTemplate(g_build_filename(appRunDir, "dbus-proxy-XXXXXX", nullptr)); |
| int fd; |
| if ((fd = g_mkstemp(proxySocketTemplate.get())) == -1) { |
| g_warning("Failed to make socket file for dbus proxy: %s", g_strerror(errno)); |
| return { }; |
| } |
| |
| close(fd); |
| return CString(proxySocketTemplate.get()); |
| }; |
| |
| static CString dbusAddressToPath(const char* address, DBusAddressType addressType = DBusAddressType::Normal) |
| { |
| if (!address) |
| return { }; |
| |
| if (!g_str_has_prefix(address, "unix:")) |
| return { }; |
| |
| const char* path = strstr(address, addressType == DBusAddressType::Abstract ? "abstract=" : "path="); |
| if (!path) |
| return { }; |
| |
| path += strlen(addressType == DBusAddressType::Abstract ? "abstract=" : "path="); |
| const char* pathEnd = path; |
| while (*pathEnd && *pathEnd != ',') |
| pathEnd++; |
| |
| return CString(path, pathEnd - path); |
| } |
| |
| CString m_socket; |
| CString m_path; |
| CString m_proxyPath; |
| bool m_isRunning; |
| Vector<CString> m_permissions; |
| }; |
| |
| enum class BindFlags { |
| ReadOnly, |
| ReadWrite, |
| Device, |
| }; |
| |
| static void bindIfExists(Vector<CString>& args, const char* path, BindFlags bindFlags = BindFlags::ReadOnly) |
| { |
| if (!path) |
| return; |
| |
| const char* bindType; |
| if (bindFlags == BindFlags::Device) |
| bindType = "--dev-bind-try"; |
| else if (bindFlags == BindFlags::ReadOnly) |
| bindType = "--ro-bind-try"; |
| else |
| bindType = "--bind-try"; |
| args.appendVector(Vector<CString>({ bindType, path, path })); |
| } |
| |
| static void bindDBusSession(Vector<CString>& args, XDGDBusProxyLauncher& proxy) |
| { |
| if (!proxy.isRunning()) |
| proxy.setAddress(g_getenv("DBUS_SESSION_BUS_ADDRESS"), DBusAddressType::Normal); |
| |
| if (proxy.proxyPath().data()) { |
| args.appendVector(Vector<CString>({ |
| "--bind", proxy.proxyPath(), proxy.path(), |
| })); |
| } |
| } |
| |
| static void bindX11(Vector<CString>& args) |
| { |
| const char* display = g_getenv("DISPLAY"); |
| if (display && display[0] == ':' && g_ascii_isdigit(const_cast<char*>(display)[1])) { |
| const char* displayNumber = &display[1]; |
| const char* displayNumberEnd = displayNumber; |
| while (g_ascii_isdigit(*displayNumberEnd)) |
| displayNumberEnd++; |
| |
| GUniquePtr<char> displayString(g_strndup(displayNumber, displayNumberEnd - displayNumber)); |
| GUniquePtr<char> x11File(g_strdup_printf("/tmp/.X11-unix/X%s", displayString.get())); |
| bindIfExists(args, x11File.get(), BindFlags::ReadWrite); |
| } |
| |
| const char* xauth = g_getenv("XAUTHORITY"); |
| if (!xauth) { |
| const char* homeDir = g_get_home_dir(); |
| GUniquePtr<char> xauthFile(g_build_filename(homeDir, ".Xauthority", nullptr)); |
| bindIfExists(args, xauthFile.get()); |
| } else |
| bindIfExists(args, xauth); |
| } |
| |
| #if PLATFORM(WAYLAND) && USE(EGL) |
| static void bindWayland(Vector<CString>& args) |
| { |
| const char* display = g_getenv("WAYLAND_DISPLAY"); |
| if (!display) |
| display = "wayland-0"; |
| |
| const char* runtimeDir = g_get_user_runtime_dir(); |
| GUniquePtr<char> waylandRuntimeFile(g_build_filename(runtimeDir, display, nullptr)); |
| bindIfExists(args, waylandRuntimeFile.get(), BindFlags::ReadWrite); |
| } |
| #endif |
| |
| static void bindPulse(Vector<CString>& args) |
| { |
| // FIXME: The server can be defined in config files we'd have to parse. |
| // They can also be set as X11 props but that is getting a bit ridiculous. |
| const char* pulseServer = g_getenv("PULSE_SERVER"); |
| if (pulseServer) { |
| if (g_str_has_prefix(pulseServer, "unix:")) |
| bindIfExists(args, pulseServer + 5, BindFlags::ReadWrite); |
| // else it uses tcp |
| } else { |
| const char* runtimeDir = g_get_user_runtime_dir(); |
| GUniquePtr<char> pulseRuntimeDir(g_build_filename(runtimeDir, "pulse", nullptr)); |
| bindIfExists(args, pulseRuntimeDir.get(), BindFlags::ReadWrite); |
| } |
| |
| const char* pulseConfig = g_getenv("PULSE_CLIENTCONFIG"); |
| if (pulseConfig) |
| bindIfExists(args, pulseConfig); |
| |
| const char* configDir = g_get_user_config_dir(); |
| GUniquePtr<char> pulseConfigDir(g_build_filename(configDir, "pulse", nullptr)); |
| bindIfExists(args, pulseConfigDir.get()); |
| |
| const char* homeDir = g_get_home_dir(); |
| GUniquePtr<char> pulseHomeConfigDir(g_build_filename(homeDir, ".pulse", nullptr)); |
| GUniquePtr<char> asoundHomeConfigDir(g_build_filename(homeDir, ".asoundrc", nullptr)); |
| bindIfExists(args, pulseHomeConfigDir.get()); |
| bindIfExists(args, asoundHomeConfigDir.get()); |
| |
| // This is the ultimate fallback to raw ALSA |
| bindIfExists(args, "/dev/snd", BindFlags::Device); |
| } |
| |
| static void bindFonts(Vector<CString>& args) |
| { |
| const char* configDir = g_get_user_config_dir(); |
| const char* homeDir = g_get_home_dir(); |
| const char* dataDir = g_get_user_data_dir(); |
| const char* cacheDir = g_get_user_cache_dir(); |
| |
| // Configs can include custom dirs but then we have to parse them... |
| GUniquePtr<char> fontConfig(g_build_filename(configDir, "fontconfig", nullptr)); |
| GUniquePtr<char> fontCache(g_build_filename(cacheDir, "fontconfig", nullptr)); |
| GUniquePtr<char> fontHomeConfig(g_build_filename(homeDir, ".fonts.conf", nullptr)); |
| GUniquePtr<char> fontHomeConfigDir(g_build_filename(configDir, ".fonts.conf.d", nullptr)); |
| GUniquePtr<char> fontData(g_build_filename(dataDir, "fonts", nullptr)); |
| GUniquePtr<char> fontHomeData(g_build_filename(homeDir, ".fonts", nullptr)); |
| bindIfExists(args, fontConfig.get()); |
| bindIfExists(args, fontCache.get(), BindFlags::ReadWrite); |
| bindIfExists(args, fontHomeConfig.get()); |
| bindIfExists(args, fontHomeConfigDir.get()); |
| bindIfExists(args, fontData.get()); |
| bindIfExists(args, fontHomeData.get()); |
| } |
| |
| #if PLATFORM(GTK) |
| static void bindGtkData(Vector<CString>& args) |
| { |
| const char* configDir = g_get_user_config_dir(); |
| const char* dataDir = g_get_user_data_dir(); |
| const char* homeDir = g_get_home_dir(); |
| |
| GUniquePtr<char> gtkConfig(g_build_filename(configDir, "gtk-3.0", nullptr)); |
| GUniquePtr<char> themeData(g_build_filename(dataDir, "themes", nullptr)); |
| GUniquePtr<char> themeHomeData(g_build_filename(homeDir, ".themes", nullptr)); |
| GUniquePtr<char> iconHomeData(g_build_filename(homeDir, ".icons", nullptr)); |
| bindIfExists(args, gtkConfig.get()); |
| bindIfExists(args, themeData.get()); |
| bindIfExists(args, themeHomeData.get()); |
| bindIfExists(args, iconHomeData.get()); |
| } |
| |
| static void bindA11y(Vector<CString>& args) |
| { |
| static XDGDBusProxyLauncher proxy; |
| |
| if (!proxy.isRunning()) { |
| // FIXME: Avoid blocking IO... (It is at least a one-time cost) |
| GRefPtr<GDBusConnection> sessionBus = adoptGRef(g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr)); |
| if (!sessionBus.get()) |
| return; |
| |
| GRefPtr<GDBusMessage> msg = adoptGRef(g_dbus_message_new_method_call( |
| "org.a11y.Bus", "/org/a11y/bus", "org.a11y.Bus", "GetAddress")); |
| g_dbus_message_set_body(msg.get(), g_variant_new("()")); |
| GRefPtr<GDBusMessage> reply = adoptGRef(g_dbus_connection_send_message_with_reply_sync( |
| sessionBus.get(), msg.get(), |
| G_DBUS_SEND_MESSAGE_FLAGS_NONE, |
| 30000, |
| nullptr, |
| nullptr, |
| nullptr)); |
| |
| if (reply.get()) { |
| GUniqueOutPtr<GError> error; |
| if (g_dbus_message_to_gerror(reply.get(), &error.outPtr())) { |
| if (!g_error_matches(error.get(), G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN)) |
| g_warning("Can't find a11y bus: %s", error->message); |
| } else { |
| GUniqueOutPtr<char> a11yAddress; |
| g_variant_get(g_dbus_message_get_body(reply.get()), "(s)", &a11yAddress.outPtr()); |
| proxy.setAddress(a11yAddress.get(), DBusAddressType::Abstract); |
| } |
| } |
| |
| proxy.setPermissions({ |
| "--sloppy-names", |
| "--call=org.a11y.atspi.Registry=org.a11y.atspi.Socket.Embed@/org/a11y/atspi/accessible/root", |
| "--call=org.a11y.atspi.Registry=org.a11y.atspi.Socket.Unembed@/org/a11y/atspi/accessible/root", |
| "--call=org.a11y.atspi.Registry=org.a11y.atspi.Registry.GetRegisteredEvents@/org/a11y/atspi/registry", |
| "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.GetKeystrokeListeners@/org/a11y/atspi/registry/deviceeventcontroller", |
| "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.GetDeviceEventListeners@/org/a11y/atspi/registry/deviceeventcontroller", |
| "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.NotifyListenersSync@/org/a11y/atspi/registry/deviceeventcontroller", |
| "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.NotifyListenersAsync@/org/a11y/atspi/registry/deviceeventcontroller", |
| }); |
| |
| proxy.launch(); |
| } |
| |
| if (proxy.proxyPath().data()) { |
| GUniquePtr<char> proxyAddress(g_strdup_printf("unix:path=%s", proxy.proxyPath().data())); |
| args.appendVector(Vector<CString>({ |
| "--ro-bind", proxy.proxyPath(), proxy.proxyPath(), |
| "--setenv", "AT_SPI_BUS_ADDRESS", proxyAddress.get(), |
| })); |
| } |
| } |
| #endif |
| |
| static bool bindPathVar(Vector<CString>& args, const char* varname) |
| { |
| const char* pathValue = g_getenv(varname); |
| if (!pathValue) |
| return false; |
| |
| GUniquePtr<char*> splitPaths(g_strsplit(pathValue, ":", -1)); |
| for (size_t i = 0; splitPaths.get()[i]; ++i) |
| bindIfExists(args, splitPaths.get()[i]); |
| |
| return true; |
| } |
| |
| static void bindGStreamerData(Vector<CString>& args) |
| { |
| if (!bindPathVar(args, "GST_PLUGIN_PATH_1_0")) |
| bindPathVar(args, "GST_PLUGIN_PATH"); |
| |
| if (!bindPathVar(args, "GST_PLUGIN_SYSTEM_PATH_1_0")) { |
| if (!bindPathVar(args, "GST_PLUGIN_SYSTEM_PATH")) { |
| GUniquePtr<char> gstData(g_build_filename(g_get_user_data_dir(), "gstreamer-1.0", nullptr)); |
| bindIfExists(args, gstData.get()); |
| } |
| } |
| |
| GUniquePtr<char> gstCache(g_build_filename(g_get_user_cache_dir(), "gstreamer-1.0", nullptr)); |
| bindIfExists(args, gstCache.get(), BindFlags::ReadWrite); |
| |
| // /usr/lib is already added so this is only requried for other dirs |
| const char* scannerPath = g_getenv("GST_PLUGIN_SCANNER") ?: "/usr/libexec/gstreamer-1.0/gst-plugin-scanner"; |
| const char* helperPath = g_getenv("GST_INSTALL_PLUGINS_HELPER ") ?: "/usr/libexec/gst-install-plugins-helper"; |
| |
| bindIfExists(args, scannerPath); |
| bindIfExists(args, helperPath); |
| } |
| |
| static void bindOpenGL(Vector<CString>& args) |
| { |
| args.appendVector(Vector<CString>({ |
| "--dev-bind-try", "/dev/dri", "/dev/dri", |
| // Mali |
| "--dev-bind-try", "/dev/mali", "/dev/mali", |
| "--dev-bind-try", "/dev/mali0", "/dev/mali0", |
| "--dev-bind-try", "/dev/umplock", "/dev/umplock", |
| // Nvidia |
| "--dev-bind-try", "/dev/nvidiactl", "/dev/nvidiactl", |
| "--dev-bind-try", "/dev/nvidia0", "/dev/nvidia0", |
| "--dev-bind-try", "/dev/nvidia", "/dev/nvidia", |
| // Adreno |
| "--dev-bind-try", "/dev/kgsl-3d0", "/dev/kgsl-3d0", |
| "--dev-bind-try", "/dev/ion", "/dev/ion", |
| #if PLATFORM(WPE) |
| "--dev-bind-try", "/dev/fb0", "/dev/fb0", |
| "--dev-bind-try", "/dev/fb1", "/dev/fb1", |
| #endif |
| })); |
| } |
| |
| static void bindV4l(Vector<CString>& args) |
| { |
| args.appendVector(Vector<CString>({ |
| "--dev-bind-try", "/dev/v4l", "/dev/v4l", |
| // Not pretty but a stop-gap for pipewire anyway. |
| "--dev-bind-try", "/dev/video0", "/dev/video0", |
| "--dev-bind-try", "/dev/video1", "/dev/video1", |
| })); |
| } |
| |
| static void bindSymlinksRealPath(Vector<CString>& args, const char* path) |
| { |
| char realPath[PATH_MAX]; |
| |
| if (realpath(path, realPath) && strcmp(path, realPath)) { |
| args.appendVector(Vector<CString>({ |
| "--ro-bind", realPath, realPath, |
| })); |
| } |
| } |
| |
| static int setupSeccomp() |
| { |
| // NOTE: This is shared code (flatpak-run.c - LGPLv2.1+) |
| // There are today a number of different Linux container |
| // implementations. That will likely continue for long into the |
| // future. But we can still try to share code, and it's important |
| // to do so because it affects what library and application writers |
| // can do, and we should support code portability between different |
| // container tools. |
| // |
| // This syscall blacklist is copied from linux-user-chroot, which was in turn |
| // clearly influenced by the Sandstorm.io blacklist. |
| // |
| // If you make any changes here, I suggest sending the changes along |
| // to other sandbox maintainers. Using the libseccomp list is also |
| // an appropriate venue: |
| // https://groups.google.com/forum/#!topic/libseccomp |
| // |
| // A non-exhaustive list of links to container tooling that might |
| // want to share this blacklist: |
| // |
| // https://github.com/sandstorm-io/sandstorm |
| // in src/sandstorm/supervisor.c++ |
| // http://cgit.freedesktop.org/xdg-app/xdg-app/ |
| // in common/flatpak-run.c |
| // https://git.gnome.org/browse/linux-user-chroot |
| // in src/setup-seccomp.c |
| struct scmp_arg_cmp cloneArg = SCMP_A0(SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER); |
| struct scmp_arg_cmp ttyArg = SCMP_A1(SCMP_CMP_MASKED_EQ, 0xFFFFFFFFu, TIOCSTI); |
| struct { |
| int scall; |
| struct scmp_arg_cmp* arg; |
| } syscallBlacklist[] = { |
| // Block dmesg |
| { SCMP_SYS(syslog), nullptr }, |
| // Useless old syscall. |
| { SCMP_SYS(uselib), nullptr }, |
| // Don't allow disabling accounting. |
| { SCMP_SYS(acct), nullptr }, |
| // 16-bit code is unnecessary in the sandbox, and modify_ldt is a |
| // historic source of interesting information leaks. |
| { SCMP_SYS(modify_ldt), nullptr }, |
| // Don't allow reading current quota use. |
| { SCMP_SYS(quotactl), nullptr }, |
| |
| // Don't allow access to the kernel keyring. |
| { SCMP_SYS(add_key), nullptr }, |
| { SCMP_SYS(keyctl), nullptr }, |
| { SCMP_SYS(request_key), nullptr }, |
| |
| // Scary VM/NUMA ops |
| { SCMP_SYS(move_pages), nullptr }, |
| { SCMP_SYS(mbind), nullptr }, |
| { SCMP_SYS(get_mempolicy), nullptr }, |
| { SCMP_SYS(set_mempolicy), nullptr }, |
| { SCMP_SYS(migrate_pages), nullptr }, |
| |
| // Don't allow subnamespace setups: |
| { SCMP_SYS(unshare), nullptr }, |
| { SCMP_SYS(mount), nullptr }, |
| { SCMP_SYS(pivot_root), nullptr }, |
| { SCMP_SYS(clone), &cloneArg }, |
| |
| // Don't allow faking input to the controlling tty (CVE-2017-5226) |
| { SCMP_SYS(ioctl), &ttyArg }, |
| |
| // Profiling operations; we expect these to be done by tools from outside |
| // the sandbox. In particular perf has been the source of many CVEs. |
| { SCMP_SYS(perf_event_open), nullptr }, |
| // Don't allow you to switch to bsd emulation or whatnot. |
| { SCMP_SYS(personality), nullptr }, |
| { SCMP_SYS(ptrace), nullptr } |
| }; |
| |
| scmp_filter_ctx seccomp = seccomp_init(SCMP_ACT_ALLOW); |
| if (!seccomp) |
| g_error("Failed to init seccomp"); |
| |
| for (auto& rule : syscallBlacklist) { |
| int scall = rule.scall; |
| int r; |
| if (rule.arg) |
| r = seccomp_rule_add(seccomp, SCMP_ACT_ERRNO(EPERM), scall, 1, rule.arg); |
| else |
| r = seccomp_rule_add(seccomp, SCMP_ACT_ERRNO(EPERM), scall, 0); |
| if (r == -EFAULT) { |
| seccomp_release(seccomp); |
| g_error("Failed to add seccomp rule"); |
| } |
| } |
| |
| int tmpfd = memfd_create("seccomp-bpf", 0); |
| if (tmpfd == -1) { |
| seccomp_release(seccomp); |
| g_error("Failed to create memfd: %s", g_strerror(errno)); |
| } |
| |
| if (seccomp_export_bpf(seccomp, tmpfd)) { |
| seccomp_release(seccomp); |
| close(tmpfd); |
| g_error("Failed to export seccomp bpf"); |
| } |
| |
| if (lseek(tmpfd, 0, SEEK_SET) < 0) |
| g_error("lseek failed: %s", g_strerror(errno)); |
| |
| seccomp_release(seccomp); |
| return tmpfd; |
| } |
| |
| static int createFlatpakInfo() |
| { |
| GUniquePtr<GKeyFile> keyFile(g_key_file_new()); |
| |
| // xdg-desktop-portal relates your name to certain permissions so we want |
| // them to be application unique which is best done via GApplication. |
| GApplication* app = g_application_get_default(); |
| if (!app) { |
| g_warning("GApplication is required for xdg-desktop-portal access in the WebKit sandbox. Actions that require xdg-desktop-portal will be broken."); |
| return -1; |
| } |
| g_key_file_set_string(keyFile.get(), "Application", "name", g_application_get_application_id(app)); |
| |
| size_t size; |
| GUniqueOutPtr<GError> error; |
| GUniquePtr<char> data(g_key_file_to_data(keyFile.get(), &size, &error.outPtr())); |
| if (error.get()) { |
| g_warning("%s", error->message); |
| return -1; |
| } |
| |
| return createSealedMemFdWithData("flatpak-info", data.get(), size); |
| } |
| |
| GRefPtr<GSubprocess> bubblewrapSpawn(GSubprocessLauncher* launcher, const ProcessLauncher::LaunchOptions& launchOptions, char** argv, GError **error) |
| { |
| ASSERT(launcher); |
| |
| #if ENABLE(NETSCAPE_PLUGIN_API) |
| // It is impossible to know what access arbitrary plugins need and since it is for legacy |
| // reasons lets just leave it unsandboxed. |
| if (launchOptions.processType == ProcessLauncher::ProcessType::Plugin64 |
| || launchOptions.processType == ProcessLauncher::ProcessType::Plugin32) |
| return adoptGRef(g_subprocess_launcher_spawnv(launcher, argv, error)); |
| #endif |
| |
| // For now we are just considering the network process trusted as it |
| // requires a lot of access but doesn't execute arbitrary code like |
| // the WebProcess where our focus lies. |
| if (launchOptions.processType == ProcessLauncher::ProcessType::Network) |
| return adoptGRef(g_subprocess_launcher_spawnv(launcher, argv, error)); |
| |
| Vector<CString> sandboxArgs = { |
| "--die-with-parent", |
| "--unshare-pid", |
| "--unshare-uts", |
| "--unshare-net", |
| |
| // We assume /etc has safe permissions. |
| // At a later point we can start masking privacy-concerning files. |
| "--ro-bind", "/etc", "/etc", |
| "--dev", "/dev", |
| "--proc", "/proc", |
| "--tmpfs", "/tmp", |
| "--unsetenv", "TMPDIR", |
| "--dir", "/run", |
| "--symlink", "../run", "/var/run", |
| "--symlink", "../tmp", "/var/tmp", |
| "--ro-bind", "/sys/block", "/sys/block", |
| "--ro-bind", "/sys/bus", "/sys/bus", |
| "--ro-bind", "/sys/class", "/sys/class", |
| "--ro-bind", "/sys/dev", "/sys/dev", |
| "--ro-bind", "/sys/devices", "/sys/devices", |
| |
| "--ro-bind-try", "/usr/share", "/usr/share", |
| "--ro-bind-try", "/usr/local/share", "/usr/local/share", |
| "--ro-bind-try", DATADIR, DATADIR, |
| |
| // We only grant access to the libdirs webkit is built with and |
| // guess system libdirs. This will always have some edge cases. |
| "--ro-bind-try", "/lib", "/lib", |
| "--ro-bind-try", "/usr/lib", "/usr/lib", |
| "--ro-bind-try", "/usr/local/lib", "/usr/local/lib", |
| "--ro-bind-try", LIBDIR, LIBDIR, |
| "--ro-bind-try", "/lib64", "/lib64", |
| "--ro-bind-try", "/usr/lib64", "/usr/lib64", |
| "--ro-bind-try", "/usr/local/lib64", "/usr/local/lib64", |
| |
| "--ro-bind-try", PKGLIBEXECDIR, PKGLIBEXECDIR, |
| }; |
| // We would have to parse ld config files for more info. |
| bindPathVar(sandboxArgs, "LD_LIBRARY_PATH"); |
| |
| const char* libraryPath = g_getenv("LD_LIBRARY_PATH"); |
| if (libraryPath && libraryPath[0]) { |
| // On distros using a suid bwrap it drops this env var |
| // so we have to pass it through to the children. |
| sandboxArgs.appendVector(Vector<CString>({ |
| "--setenv", "LD_LIBRARY_PATH", libraryPath, |
| })); |
| } |
| |
| bindSymlinksRealPath(sandboxArgs, "/etc/resolv.conf"); |
| bindSymlinksRealPath(sandboxArgs, "/etc/localtime"); |
| |
| // xdg-desktop-portal defaults to assuming you are host application with |
| // full permissions unless it can identify you as a snap or flatpak. |
| // The easiest method is for us to pretend to be a flatpak and if that |
| // fails just blocking portals entirely as it just becomes a sandbox escape. |
| int flatpakInfoFd = createFlatpakInfo(); |
| if (flatpakInfoFd != -1) { |
| g_subprocess_launcher_take_fd(launcher, flatpakInfoFd, flatpakInfoFd); |
| GUniquePtr<char> flatpakInfoFdStr(g_strdup_printf("%d", flatpakInfoFd)); |
| |
| sandboxArgs.appendVector(Vector<CString>({ |
| "--ro-bind-data", flatpakInfoFdStr.get(), "/.flatpak-info" |
| })); |
| } |
| |
| if (launchOptions.processType == ProcessLauncher::ProcessType::Web) { |
| static XDGDBusProxyLauncher proxy; |
| |
| // If Wayland in use don't grant X11 |
| #if PLATFORM(WAYLAND) && USE(EGL) |
| if (PlatformDisplay::sharedDisplay().type() == PlatformDisplay::Type::Wayland) { |
| bindWayland(sandboxArgs); |
| sandboxArgs.append("--unshare-ipc"); |
| } else |
| #endif |
| bindX11(sandboxArgs); |
| |
| for (const auto& pathAndPermission : launchOptions.extraWebProcessSandboxPaths) { |
| sandboxArgs.appendVector(Vector<CString>({ |
| pathAndPermission.value == SandboxPermission::ReadOnly ? "--ro-bind-try": "--bind-try", |
| pathAndPermission.key, pathAndPermission.key |
| })); |
| } |
| |
| Vector<String> extraPaths = { "applicationCacheDirectory", "mediaKeysDirectory", "waylandSocket", "webSQLDatabaseDirectory" }; |
| for (const auto& path : extraPaths) { |
| String extraPath = launchOptions.extraInitializationData.get(path); |
| if (!extraPath.isEmpty()) |
| sandboxArgs.appendVector(Vector<CString>({ "--bind-try", extraPath.utf8(), extraPath.utf8() })); |
| } |
| |
| bindDBusSession(sandboxArgs, proxy); |
| // FIXME: We should move to Pipewire as soon as viable, Pulse doesn't restrict clients atm. |
| bindPulse(sandboxArgs); |
| bindFonts(sandboxArgs); |
| bindGStreamerData(sandboxArgs); |
| bindOpenGL(sandboxArgs); |
| // FIXME: This is also fixed by Pipewire once in use. |
| bindV4l(sandboxArgs); |
| #if PLATFORM(GTK) |
| bindA11y(sandboxArgs); |
| bindGtkData(sandboxArgs); |
| #endif |
| |
| if (!proxy.isRunning()) { |
| Vector<CString> permissions = { |
| // GStreamers plugin install helper. |
| "--call=org.freedesktop.PackageKit=org.freedesktop.PackageKit.Modify2.InstallGStreamerResources@/org/freedesktop/PackageKit" |
| }; |
| if (flatpakInfoFd != -1) { |
| // xdg-desktop-portal used by GTK and us. |
| permissions.append("--talk=org.freedesktop.portal.Desktop"); |
| } |
| proxy.setPermissions(WTFMove(permissions)); |
| proxy.launch(); |
| } |
| } else { |
| // Only X11 users need this for XShm which is only the Web process. |
| sandboxArgs.append("--unshare-ipc"); |
| } |
| |
| #if ENABLE(DEVELOPER_MODE) |
| const char* execDirectory = g_getenv("WEBKIT_EXEC_PATH"); |
| if (execDirectory) { |
| String parentDir = FileSystem::directoryName(FileSystem::stringFromFileSystemRepresentation(execDirectory)); |
| bindIfExists(sandboxArgs, parentDir.utf8().data()); |
| } |
| |
| CString executablePath = getCurrentExecutablePath(); |
| if (!executablePath.isNull()) { |
| // Our executable is `/foo/bar/bin/Process`, we want `/foo/bar` as a usable prefix |
| String parentDir = FileSystem::directoryName(FileSystem::directoryName(FileSystem::stringFromFileSystemRepresentation(executablePath.data()))); |
| bindIfExists(sandboxArgs, parentDir.utf8().data()); |
| } |
| #endif |
| |
| int seccompFd = setupSeccomp(); |
| GUniquePtr<char> fdStr(g_strdup_printf("%d", seccompFd)); |
| g_subprocess_launcher_take_fd(launcher, seccompFd, seccompFd); |
| sandboxArgs.appendVector(Vector<CString>({ "--seccomp", fdStr.get() })); |
| |
| int bwrapFd = argsToFd(sandboxArgs, "bwrap"); |
| GUniquePtr<char> bwrapFdStr(g_strdup_printf("%d", bwrapFd)); |
| g_subprocess_launcher_take_fd(launcher, bwrapFd, bwrapFd); |
| |
| Vector<CString> bwrapArgs = { |
| BWRAP_EXECUTABLE, |
| "--args", |
| bwrapFdStr.get(), |
| "--", |
| }; |
| |
| char** newArgv = g_newa(char*, g_strv_length(argv) + bwrapArgs.size() + 1); |
| size_t i = 0; |
| |
| for (auto& arg : bwrapArgs) |
| newArgv[i++] = const_cast<char*>(arg.data()); |
| for (size_t x = 0; argv[x]; x++) |
| newArgv[i++] = argv[x]; |
| newArgv[i++] = nullptr; |
| |
| return adoptGRef(g_subprocess_launcher_spawnv(launcher, newArgv, error)); |
| } |
| |
| }; |
| |
| #endif // ENABLE(BUBBLEWRAP_SANDBOX) |