From dee5990eb9700a71604a298e0003be6a68092c34 Mon Sep 17 00:00:00 2001 From: Rahul Sandhu 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 --- 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 #include #include #include +#include #include #include #include @@ -69,6 +74,10 @@ #include #endif +#if HAVE_SELINUX +#include +#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