From c292679c5c62d363396436741e2b38edbfbec61e Mon Sep 17 00:00:00 2001 From: Daniel De Graaf Date: Thu, 18 Feb 2021 23:03:51 -0500 Subject: [PATCH] Label-based security on privileged globals This adds a command (security_label) which is used to allow or deny access to privileged interfaces on a client-by-client basis. If no security configuration it present, all privileged operations are allowed to all clients. - "security_label deny default *" will deny clients access to all privileged operations - "security_label set default layer_shell" will overwrite the access list for the label "default" and only allow access to the layer_shell interface - "security_label permit recorder screencopy_manager" allows connections that have the label "recorder" access to screencopy_manager in addition to the current permissions of the "recorder" label If a client does not have a label or if the label's permissions were not defined using security_label, the permissions for the "default" label are used; if no definition for "default" is present, all interfaces are allowed. Using permit or deny on a new label does not copy the default. The security configuration state is reset on a config reload (similar to the assign and for_window lists). Currently, the security policy is only enforced during the binding or enumeration of global resources; existing handles to privileged interfaces are not invalided by a change in policy, and existing clients are not informed of the presence of newly available interfaces. --- include/sway/commands.h | 1 + include/sway/config.h | 7 ++ include/sway/security.h | 27 +++++++ include/sway/server.h | 5 ++ sway/commands.c | 1 + sway/commands/security_label.c | 72 +++++++++++++++++++ sway/config.c | 11 +++ sway/meson.build | 2 + sway/security.c | 125 +++++++++++++++++++++++++++++++++ sway/server.c | 11 +-- sway/sway.5.scd | 59 ++++++++++++++++ 11 files changed, 317 insertions(+), 4 deletions(-) create mode 100644 include/sway/security.h create mode 100644 sway/commands/security_label.c create mode 100644 sway/security.c diff --git a/include/sway/commands.h b/include/sway/commands.h index 116c8681e..af813d24d 100644 --- a/include/sway/commands.h +++ b/include/sway/commands.h @@ -308,5 +308,6 @@ sway_cmd cmd_ipc_events; sway_cmd cmd_ipc_event_cmd; sway_cmd cmd_sandbox_socket; +sway_cmd cmd_security_label; #endif diff --git a/include/sway/config.h b/include/sway/config.h index f38e7e351..3e3b1171b 100644 --- a/include/sway/config.h +++ b/include/sway/config.h @@ -14,6 +14,7 @@ #include "swaynag.h" #include "tree/container.h" #include "sway/input/tablet.h" +#include "sway/security.h" #include "sway/tree/root.h" #include "wlr-layer-shell-unstable-v1-protocol.h" @@ -286,6 +287,11 @@ struct output_config { enum config_dpms dpms_state; }; +struct security_config { + char* name; + security_perm_mask_t permitted; +}; + /** * Stores size of gaps for each side */ @@ -488,6 +494,7 @@ struct sway_config { list_t *criteria; list_t *no_focus; list_t *active_bar_modifiers; + list_t *security_configs; struct sway_mode *current_mode; struct bar_config *current_bar; uint32_t floating_mod; diff --git a/include/sway/security.h b/include/sway/security.h new file mode 100644 index 000000000..987e2fe10 --- /dev/null +++ b/include/sway/security.h @@ -0,0 +1,27 @@ +#pragma once + +enum security_perm { + PRIV_DATA_CONTROL_MANAGER, + PRIV_FOREIGN_TOPLEVEL_MANAGER, + PRIV_GAMMA_CONTROL_MANAGER, + PRIV_INPUT_INHIBIT, + PRIV_INPUT_KEYBOARD_SHORTCUTS_INHIBIT, + PRIV_INPUT_METHOD, + PRIV_INPUT_VIRTUAL_KEYBOARD, + PRIV_INPUT_VIRTUAL_POINTER, + PRIV_LAYER_SHELL, + PRIV_OUTPUT_MANAGER, + PRIV_OUTPUT_POWER_MANAGER, + PRIV_SCREENSHOT, + PRIV_SESSION_LOCK, + PRIV_LAST, /* not an actual permission */ +}; + +typedef uint32_t security_perm_mask_t; + +bool security_global_filter(const struct wl_client *client, const struct wl_global *global, void *data); + +/** + * Get permission value associated with the given name, or PRIV_LAST if there is none + */ +enum security_perm security_get_perm_by_name(const char *name); diff --git a/include/sway/server.h b/include/sway/server.h index 0bd860b24..83045efbc 100644 --- a/include/sway/server.h +++ b/include/sway/server.h @@ -44,6 +44,7 @@ struct sway_server { struct wlr_linux_dmabuf_v1 *linux_dmabuf_v1; struct wlr_data_device_manager *data_device_manager; + struct wlr_gamma_control_manager_v1 *gamma_control_manager; struct sway_input_manager *input; @@ -98,6 +99,10 @@ struct sway_server { struct wlr_xdg_activation_v1 *xdg_activation_v1; struct wl_listener xdg_activation_v1_request_activate; + struct wlr_export_dmabuf_manager_v1 *dmabuf_manager; + struct wlr_screencopy_manager_v1 *screencopy_manager; + struct wlr_data_control_manager_v1 *data_control_manager; + // The timeout for transactions, after which a transaction is applied // regardless of readiness. size_t txn_timeout_ms; diff --git a/sway/commands.c b/sway/commands.c index aef1afafc..f8b390805 100644 --- a/sway/commands.c +++ b/sway/commands.c @@ -83,6 +83,7 @@ static const struct cmd_handler handlers[] = { { "popup_during_fullscreen", cmd_popup_during_fullscreen }, { "sandbox_socket", cmd_sandbox_socket }, { "seat", cmd_seat }, + { "security_label", cmd_security_label }, { "set", cmd_set }, { "show_marks", cmd_show_marks }, { "smart_borders", cmd_smart_borders }, diff --git a/sway/commands/security_label.c b/sway/commands/security_label.c new file mode 100644 index 000000000..3134a37bc --- /dev/null +++ b/sway/commands/security_label.c @@ -0,0 +1,72 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include "sway/commands.h" +#include "sway/security.h" +#include "sway/tree/container.h" +#include "sway/tree/view.h" +#include "list.h" +#include "log.h" + +struct cmd_results *cmd_security_label(int argc, char **argv) { + int i; + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "security_label", EXPECTED_AT_LEAST, 2))) { + return error; + } + + char* op = argv[0]; + char* who = argv[1]; + uint32_t privs = 0; + enum { SET, PERMIT, DENY } mode; + if (strcmp(op, "set") == 0) { + mode = SET; + } else if (strcmp(op, "permit") == 0) { + mode = PERMIT; + } else if (strcmp(op, "deny") == 0) { + mode = DENY; + } else { + return cmd_results_new(CMD_INVALID, "Invalid operation: %s", op); + } + + for(i = 2; i < argc; i++) { + char* arg = argv[i]; + enum security_perm priv = security_get_perm_by_name(arg); + if (priv != PRIV_LAST) + privs |= 1 << priv; + else if (strcmp(arg, "*") == 0) + privs |= ~0; + else + return cmd_results_new(CMD_INVALID, "Unknown permission: %s", arg); + } + + if (!config->security_configs) + config->security_configs = create_list(); + + for (i = 0; i < config->security_configs->length; i++) { + struct security_config *cfg = config->security_configs->items[i]; + if (strcmp(cfg->name, who) == 0) + break; + } + if (i == config->security_configs->length) { + struct security_config *cfg = calloc(1, sizeof(*cfg)); + cfg->name = strdup(who); + list_add(config->security_configs, cfg); + } + + struct security_config *cfg = config->security_configs->items[i]; + switch (mode) { + case SET: + cfg->permitted = privs; + break; + case PERMIT: + cfg->permitted |= privs; + break; + case DENY: + cfg->permitted &= ~privs; + break; + } + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/config.c b/sway/config.c index bd528c763..385b048a5 100644 --- a/sway/config.c +++ b/sway/config.c @@ -85,6 +85,11 @@ static void free_mode(struct sway_mode *mode) { free(mode); } +static void free_security_config(struct security_config *cfg) { + free(cfg->name); + free(cfg); +} + void free_config(struct sway_config *config) { if (!config) { return; @@ -151,6 +156,12 @@ void free_config(struct sway_config *config) { } list_free(config->criteria); } + if (config->security_configs) { + for (int i = 0; i < config->security_configs->length; ++i) { + free_security_config(config->security_configs->items[i]); + } + list_free(config->security_configs); + } list_free(config->no_focus); list_free(config->active_bar_modifiers); list_free_items_and_destroy(config->config_chain); diff --git a/sway/meson.build b/sway/meson.build index b990fc36f..0ae080469 100644 --- a/sway/meson.build +++ b/sway/meson.build @@ -6,6 +6,7 @@ sway_sources = files( 'ipc-json.c', 'ipc-server.c', 'main.c', + 'security.c', 'server.c', 'swaynag.c', 'xdg_activation_v1.c', @@ -98,6 +99,7 @@ sway_sources = files( 'commands/seat/pointer_constraint.c', 'commands/seat/shortcuts_inhibitor.c', 'commands/seat/xcursor_theme.c', + 'commands/security_label.c', 'commands/set.c', 'commands/show_marks.c', 'commands/shortcuts_inhibitor.c', diff --git a/sway/security.c b/sway/security.c new file mode 100644 index 000000000..34a6b0c69 --- /dev/null +++ b/sway/security.c @@ -0,0 +1,125 @@ +#define _XOPEN_SOURCE 500 // for strdup +#define _POSIX_C_SOURCE 200112L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "list.h" +#include "log.h" +#include "stringop.h" +#include "sway/client_label.h" +#include "sway/config.h" +#include "sway/desktop/idle_inhibit_v1.h" +#include "sway/input/input-manager.h" +#include "sway/output.h" +#include "sway/server.h" +#include "sway/security.h" +#include "sway/tree/root.h" + +bool security_global_filter(const struct wl_client *client, const struct wl_global *global, void *data) { + struct sway_server *server = data; + enum security_perm perm; + + // with no security config, quickly default to allow all + if (config->security_configs == NULL || config->security_configs->length == 0) + return true; + + if (global == server->layer_shell->global) + perm = PRIV_LAYER_SHELL; + else if (global == server->output_manager_v1->global) + perm = PRIV_OUTPUT_MANAGER; + else if (global == server->input_method->global) + perm = PRIV_INPUT_METHOD; + else if (global == server->foreign_toplevel_manager->global) + perm = PRIV_FOREIGN_TOPLEVEL_MANAGER; + else if (global == server->dmabuf_manager->global) + perm = PRIV_SCREENSHOT; + else if (global == server->screencopy_manager->global ) + perm = PRIV_SCREENSHOT; + else if (global == server->data_control_manager->global) + perm = PRIV_DATA_CONTROL_MANAGER; + else if (global == server->gamma_control_manager->global) + perm = PRIV_GAMMA_CONTROL_MANAGER; + else if (global == server->input->inhibit->global) + perm = PRIV_INPUT_INHIBIT; + else if (global == server->input->keyboard_shortcuts_inhibit->global) + perm = PRIV_INPUT_KEYBOARD_SHORTCUTS_INHIBIT; + else if (global == server->input->virtual_keyboard->global) + perm = PRIV_INPUT_VIRTUAL_KEYBOARD; + else if (global == server->input->virtual_pointer->global) + perm = PRIV_INPUT_VIRTUAL_POINTER; + else if (global == server->output_power_manager_v1->global) + perm = PRIV_OUTPUT_POWER_MANAGER; + else if (global == server->session_lock->global) + perm = PRIV_SESSION_LOCK; + else + return true; + + security_perm_mask_t priv_bit = 1 << perm; + const char* label = wl_client_label_get((struct wl_client*)client); + if (label != NULL) { + for (int i = 0; i < config->security_configs->length; i++) { + struct security_config *cfg = config->security_configs->items[i]; + if (lenient_strcmp(cfg->name, label) == 0) { + return !!(priv_bit & cfg->permitted); + } + } + } + + for (int i = 0; i < config->security_configs->length; i++) { + struct security_config *cfg = config->security_configs->items[i]; + if (lenient_strcmp(cfg->name, "default") == 0) { + return !!(priv_bit & cfg->permitted); + } + } + return true; +} + +static const char* const perm_names[PRIV_LAST] = { + "data_control", + "foreign_toplevel_manager", + "gamma_control", + "input_inhibit", + "input_keyboard_shortcuts_inhibit", + "input_method", + "input_virtual_keyboard", + "input_virtual_pointer", + "layer_shell", + "output_manager", + "output_power_manager", + "screenshot", + "session_lock", +}; + +enum security_perm security_get_perm_by_name(const char *name) +{ + int i; + for(i = 0; i < PRIV_LAST; i++) { + if (0 == strcmp(name, perm_names[i])) + return i; + } + return PRIV_LAST; +} diff --git a/sway/server.c b/sway/server.c index 8de9f6294..9872dea31 100644 --- a/sway/server.c +++ b/sway/server.c @@ -42,6 +42,7 @@ #include "sway/input/input-manager.h" #include "sway/output.h" #include "sway/server.h" +#include "sway/security.h" #include "sway/tree/root.h" #if HAVE_XWAYLAND #include "sway/xwayland.h" @@ -107,7 +108,7 @@ bool server_init(struct sway_server *server) { server->data_device_manager = wlr_data_device_manager_create(server->wl_display); - wlr_gamma_control_manager_v1_create(server->wl_display); + server->gamma_control_manager = wlr_gamma_control_manager_v1_create(server->wl_display); server->new_output.notify = handle_new_output; wl_signal_add(&server->backend->events.new_output, &server->new_output); @@ -194,9 +195,9 @@ bool server_init(struct sway_server *server) { sway_log(SWAY_INFO, "VR will not be available"); } - wlr_export_dmabuf_manager_v1_create(server->wl_display); - wlr_screencopy_manager_v1_create(server->wl_display); - wlr_data_control_manager_v1_create(server->wl_display); + server->dmabuf_manager = wlr_export_dmabuf_manager_v1_create(server->wl_display); + server->screencopy_manager = wlr_screencopy_manager_v1_create(server->wl_display); + server->data_control_manager = wlr_data_control_manager_v1_create(server->wl_display); wlr_primary_selection_v1_device_manager_create(server->wl_display); wlr_viewporter_create(server->wl_display); @@ -211,6 +212,8 @@ bool server_init(struct sway_server *server) { wl_signal_add(&server->xdg_activation_v1->events.request_activate, &server->xdg_activation_v1_request_activate); + wl_display_set_global_filter(server->wl_display, security_global_filter, server); + // Avoid using "wayland-0" as display socket char name_candidate[16]; for (int i = 1; i <= 32; ++i) { diff --git a/sway/sway.5.scd b/sway/sway.5.scd index 168761e1a..a1910346b 100644 --- a/sway/sway.5.scd +++ b/sway/sway.5.scd @@ -746,6 +746,12 @@ The default colors are: dialog will not be rendered. If _leave_fullscreen_, the view will exit fullscreen mode and the dialog will be rendered. +*security_label* [set|permit|deny]