| From dee5990eb9700a71604a298e0003be6a68092c34 Mon Sep 17 00:00:00 2001
|
| From: Rahul Sandhu <nvraxn@gmail.com>
|
| Date: Mon, 24 Feb 2025 18:38:08 +0000
|
| Subject: [PATCH] sway: implement selinux based filtering for globals
|
|
|
| This patch exposes privileged globals to SELinux so access to priv
|
| globals can be mediated by SELinux.
|
|
|
| Signed-off-by: Rahul Sandhu <nvraxn@gmail.com>
|
| ---
|
| meson.build | 2 +
|
| meson_options.txt | 1 +
|
| sway/meson.build | 1 +
|
| sway/server.c | 198 ++++++++++++++++++++++++++++++++++++++++++++++
|
| 4 files changed, 202 insertions(+)
|
|
|
| diff --git a/meson.build b/meson.build
|
| index 9ce5723e..45d94c23 100644
|
| --- a/meson.build
|
| +++ b/meson.build
|
| @@ -73,6 +73,7 @@ gdk_pixbuf = dependency('gdk-pixbuf-2.0', required: get_option('gdk-pixbuf'))
|
| pixman = dependency('pixman-1')
|
| libevdev = dependency('libevdev')
|
| libinput = wlroots_features['libinput_backend'] ? dependency('libinput', version: '>=1.26.0') : null_dep
|
| +libselinux = dependency('libselinux', required: get_option('selinux'))
|
| xcb = wlroots_features['xwayland'] ? dependency('xcb') : null_dep
|
| drm = dependency('libdrm')
|
| libudev = wlroots_features['libinput_backend'] ? dependency('libudev') : null_dep
|
| @@ -109,6 +110,7 @@ conf_data.set10('HAVE_LIBSYSTEMD', sdbus.found() and sdbus.name() == 'libsystemd
|
| conf_data.set10('HAVE_LIBELOGIND', sdbus.found() and sdbus.name() == 'libelogind')
|
| conf_data.set10('HAVE_BASU', sdbus.found() and sdbus.name() == 'basu')
|
| conf_data.set10('HAVE_TRAY', have_tray)
|
| +conf_data.set10('HAVE_SELINUX', libselinux.found())
|
| foreach sym : ['LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM', 'LIBINPUT_CONFIG_DRAG_LOCK_ENABLED_STICKY']
|
| conf_data.set10('HAVE_' + sym, cc.has_header_symbol('libinput.h', sym, dependencies: libinput))
|
| endforeach
|
| diff --git a/meson_options.txt b/meson_options.txt
|
| index 506ecc9a..ca217a4e 100644
|
| --- a/meson_options.txt
|
| +++ b/meson_options.txt
|
| @@ -8,3 +8,4 @@ option('tray', type: 'feature', value: 'auto', description: 'Enable support for
|
| option('gdk-pixbuf', type: 'feature', value: 'auto', description: 'Enable support for more image formats in swaybar tray')
|
| option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages')
|
| option('sd-bus-provider', type: 'combo', choices: ['auto', 'libsystemd', 'libelogind', 'basu'], value: 'auto', description: 'Provider of the sd-bus library')
|
| +option('selinux', type: 'boolean', value: false, description: 'Enable support for SELinux access control')
|
| diff --git a/sway/meson.build b/sway/meson.build
|
| index 8042c89b..0d4f12af 100644
|
| --- a/sway/meson.build
|
| +++ b/sway/meson.build
|
| @@ -223,6 +223,7 @@ sway_deps = [
|
| jsonc,
|
| libevdev,
|
| libinput,
|
| + libselinux,
|
| libudev,
|
| math,
|
| pango,
|
| diff --git a/sway/server.c b/sway/server.c
|
| index c7fc2a6d..1c15f886 100644
|
| --- a/sway/server.c
|
| +++ b/sway/server.c
|
| @@ -1,7 +1,12 @@
|
| +#ifdef __linux__
|
| +#define _DEFAULT_SOURCE
|
| +#endif
|
| +
|
| #include <assert.h>
|
| #include <stdbool.h>
|
| #include <stdlib.h>
|
| #include <string.h>
|
| +#include <sys/socket.h>
|
| #include <wayland-server-core.h>
|
| #include <wlr/backend.h>
|
| #include <wlr/backend/headless.h>
|
| @@ -69,6 +74,10 @@
|
| #include <wlr/types/wlr_drm_lease_v1.h>
|
| #endif
|
|
|
| +#if HAVE_SELINUX
|
| +#include <selinux/selinux.h>
|
| +#endif
|
| +
|
| #define SWAY_XDG_SHELL_VERSION 5
|
| #define SWAY_LAYER_SHELL_VERSION 4
|
| #define SWAY_FOREIGN_TOPLEVEL_LIST_VERSION 1
|
| @@ -90,6 +99,191 @@ static void handle_drm_lease_request(struct wl_listener *listener, void *data) {
|
| }
|
| #endif
|
|
|
| +#if HAVE_SELINUX
|
| +static const char *get_selinux_protocol(const struct wl_global *global) {
|
| +#if WLR_HAS_DRM_BACKEND
|
| + if (server.drm_lease_manager != NULL) {
|
| + struct wlr_drm_lease_device_v1 *drm_lease_dev;
|
| + wl_list_for_each(drm_lease_dev, &server.drm_lease_manager->devices, link) {
|
| + if (drm_lease_dev->global == global) {
|
| + return "drm_lease";
|
| + }
|
| + }
|
| + }
|
| +#endif
|
| +
|
| + if (global == server.output_manager_v1->global) {
|
| + return "output_manager";
|
| + }
|
| + if (global == server.output_power_manager_v1->global) {
|
| + return "output_power_manager";
|
| + }
|
| + if (global == server.input_method->global) {
|
| + return "input_method";
|
| + }
|
| + if (global == server.foreign_toplevel_list->global) {
|
| + return "foreign_toplevel_list";
|
| + }
|
| + if (global == server.foreign_toplevel_manager->global) {
|
| + return "foreign_toplevel_manager";
|
| + }
|
| + if (global == server.data_control_manager_v1->global) {
|
| + return "data_control_manager";
|
| + }
|
| + if (global == server.screencopy_manager_v1->global) {
|
| + return "screencopy_manager";
|
| + }
|
| + if (global == server.ext_image_copy_capture_manager_v1->global) {
|
| + return "ext_image_copy_capture_manager";
|
| + }
|
| + if (global == server.export_dmabuf_manager_v1->global) {
|
| + return "export_dmabuf_manager";
|
| + }
|
| + if (global == server.security_context_manager_v1->global) {
|
| + return "security_context_manager";
|
| + }
|
| + if (global == server.gamma_control_manager_v1->global) {
|
| + return "gamma_control_manager";
|
| + }
|
| + if (global == server.layer_shell->global) {
|
| + return "layer_shell";
|
| + }
|
| + if (global == server.session_lock.manager->global) {
|
| + return "session_lock_manager";
|
| + }
|
| + if (global == server.input->keyboard_shortcuts_inhibit->global) {
|
| + return "keyboard_shortcuts_inhibit";
|
| + }
|
| + if (global == server.input->virtual_keyboard->global) {
|
| + return "virtual_keyboard";
|
| + }
|
| + if (global == server.input->virtual_pointer->global) {
|
| + return "virtual_pointer";
|
| + }
|
| + if (global == server.input->transient_seat_manager->global) {
|
| + return "transient_seat_manager";
|
| + }
|
| + if (global == server.xdg_output_manager_v1->global) {
|
| + return "xdg_output_manager";
|
| + }
|
| +
|
| + return NULL;
|
| +}
|
| +
|
| +static sway_log_importance_t convert_selinux_log_importance(int type) {
|
| + switch (type) {
|
| + case SELINUX_ERROR:
|
| + return SWAY_ERROR;
|
| + case SELINUX_WARNING:
|
| + case SELINUX_AVC:
|
| + case SELINUX_INFO:
|
| + return SWAY_INFO;
|
| + default:
|
| + return SWAY_DEBUG;
|
| + }
|
| +}
|
| +
|
| +static int log_callback_selinux(int type, const char *fmt, ...) {
|
| + va_list args;
|
| + va_start(args, fmt);
|
| +
|
| + int space_needed = snprintf(NULL, 0, "[selinux] %s", fmt);
|
| + if (space_needed < 0) {
|
| + return -1;
|
| + }
|
| + char *buffer = calloc(space_needed + 1, sizeof(*buffer));
|
| + if (buffer == NULL) {
|
| + return -1;
|
| + }
|
| + sprintf(buffer, "[selinux %s]", fmt);
|
| +
|
| + _sway_vlog(convert_selinux_log_importance(type), buffer, args);
|
| +
|
| + free(buffer);
|
| + va_end(args);
|
| + return 0;
|
| +}
|
| +#endif
|
| +
|
| +static bool check_access_selinux(const struct wl_global *global,
|
| + const struct wl_client *client) {
|
| +#if HAVE_SELINUX
|
| + if (is_selinux_enabled() == 0) {
|
| + // SELinux not running
|
| + return true;
|
| + }
|
| +
|
| + const char *protocol = get_selinux_protocol(global);
|
| + if (protocol == NULL) {
|
| + return true; // Not a privileged protocol, access granted
|
| + }
|
| +
|
| + char *client_context = NULL;
|
| + socklen_t len = NAME_MAX;
|
| + int r;
|
| +
|
| + int sockfd = wl_client_get_fd((struct wl_client *)client);
|
| +
|
| + do {
|
| + char *new_context = realloc(client_context, len);
|
| + if (new_context == NULL) {
|
| + free(client_context);
|
| + return false;
|
| + }
|
| + client_context = new_context;
|
| +
|
| + r = getsockopt(sockfd, SOL_SOCKET, SO_PEERSEC, client_context, &len);
|
| + if (r < 0 && errno != ERANGE) {
|
| + free(client_context);
|
| + return false;
|
| + }
|
| + } while (r < 0 && errno == ERANGE);
|
| +
|
| + if (client_context == NULL) {
|
| + return true; // Getting NULL back for SO_PEERSEC means that an LSM
|
| + // that provides security contexts is not running.
|
| + }
|
| +
|
| + selinux_set_callback(SELINUX_CB_LOG, (union selinux_callback) { .func_log = log_callback_selinux });
|
| +
|
| + r = security_getenforce();
|
| + // If we can't determine if SELinux is enforcing or not,
|
| + // proceed as if enforcing
|
| + bool enforcing = !r;
|
| +
|
| + char *compositor_context = NULL;
|
| + if (getcon_raw(&compositor_context) < 0) {
|
| + _sway_log(SWAY_ERROR, "[selinux] getcon_raw() failed: %s", strerror(errno));
|
| + // We can't get our own context. Only allow
|
| + // the access if not in enforcing mode.
|
| + free(client_context);
|
| + return !enforcing;
|
| + }
|
| + if (compositor_context == NULL) {
|
| + _sway_log(SWAY_ERROR, "[selinux] getcon_raw() returned NULL");
|
| + // We can't get our own context. Only allow
|
| + // the access if not in enforcing mode.
|
| + free(client_context);
|
| + return !enforcing;
|
| + }
|
| +
|
| + const char *tclass = "wayland_protocol";
|
| + errno = 0;
|
| + r = selinux_check_access(client_context, compositor_context, tclass, protocol, NULL);
|
| + if (r < 0) {
|
| + _sway_log(SWAY_INFO, "[selinux] access check denied: %s", strerror(errno));
|
| + }
|
| + _sway_log(SWAY_DEBUG, "[selinux] access check scon=%s tcon=%s tclass=%s perm=%s",
|
| + client_context, compositor_context, tclass, protocol);
|
| + free(client_context);
|
| + free(compositor_context);
|
| +
|
| + return enforcing ? (r == 0) : true;
|
| +#else
|
| + return true;
|
| +#endif
|
| +}
|
| +
|
| static bool is_privileged(const struct wl_global *global) {
|
| #if WLR_HAS_DRM_BACKEND
|
| if (server.drm_lease_manager != NULL) {
|
| @@ -132,6 +326,10 @@ static bool filter_global(const struct wl_client *client,
|
| }
|
| #endif
|
|
|
| + if (!check_access_selinux(global, client)) {
|
| + return false;
|
| + }
|
| +
|
| // Restrict usage of privileged protocols to unsandboxed clients
|
| // TODO: add a way for users to configure an allow-list
|
| const struct wlr_security_context_v1_state *security_context =
|
| --
|
| 2.49.0
|