From c9773491207d36d6f5e651adcb7a64c7a015bba3 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Fri, 28 Sep 2018 12:18:54 +0200 Subject: [PATCH] Add support for building swaylock without PAM This involves setuid'ing swaylock, which then forks and drops perms on the parent process. The child process remains root and listens on a pipe for requests to validate passwords against /etc/shadow. --- include/swaylock/swaylock.h | 3 + meson.build | 10 +-- swaylock/main.c | 5 +- swaylock/meson.build | 50 ++++++++------ swaylock/pam.c | 62 +++++++++++++++++ swaylock/password.c | 51 -------------- swaylock/shadow.c | 128 ++++++++++++++++++++++++++++++++++++ 7 files changed, 233 insertions(+), 76 deletions(-) create mode 100644 swaylock/pam.c create mode 100644 swaylock/shadow.c diff --git a/include/swaylock/swaylock.h b/include/swaylock/swaylock.h index 2f0cd34d..970e3cc9 100644 --- a/include/swaylock/swaylock.h +++ b/include/swaylock/swaylock.h @@ -101,5 +101,8 @@ void render_frame(struct swaylock_surface *surface); void render_frames(struct swaylock_state *state); void damage_surface(struct swaylock_surface *surface); void damage_state(struct swaylock_state *state); +void initialize_pw_backend(void); +bool attempt_password(struct swaylock_password *pw); +void clear_password_buffer(struct swaylock_password *pw); #endif diff --git a/meson.build b/meson.build index 76eaff20..de6573ea 100644 --- a/meson.build +++ b/meson.build @@ -74,6 +74,11 @@ if elogind.found() swayidle_deps += elogind endif +if not systemd.found() and not elogind.found() + warning('The sway binary must be setuid when compiled without (e)logind') + warning('You must do this manually post-install: chmod a+s /path/to/sway') +endif + scdoc = find_program('scdoc', required: false) if scdoc.found() @@ -139,10 +144,7 @@ subdir('swaybg') subdir('swaybar') subdir('swayidle') subdir('swaynag') - -if libpam.found() - subdir('swaylock') -endif +subdir('swaylock') config = configuration_data() config.set('sysconfdir', join_paths(prefix, sysconfdir)) diff --git a/swaylock/main.c b/swaylock/main.c index c25c8eec..693cbc10 100644 --- a/swaylock/main.c +++ b/swaylock/main.c @@ -845,6 +845,9 @@ static int load_config(char *path, struct swaylock_state *state, static struct swaylock_state state; int main(int argc, char **argv) { + wlr_log_init(WLR_DEBUG, NULL); + initialize_pw_backend(); + enum line_mode line_mode = LM_LINE; state.args = (struct swaylock_args){ .mode = BACKGROUND_MODE_SOLID_COLOR, @@ -857,8 +860,6 @@ int main(int argc, char **argv) { wl_list_init(&state.images); set_default_colors(&state.args.colors); - wlr_log_init(WLR_DEBUG, NULL); - char *config_path = NULL; int result = parse_options(argc, argv, NULL, NULL, &config_path); if (result != 0) { diff --git a/swaylock/meson.build b/swaylock/meson.build index 675b8c69..6c87d173 100644 --- a/swaylock/meson.build +++ b/swaylock/meson.build @@ -1,25 +1,37 @@ sysconfdir = get_option('sysconfdir') -executable( - 'swaylock', [ - 'main.c', - 'password.c', - 'render.c', - 'seat.c' - ], +dependencies = [ + cairo, + client_protos, + gdk_pixbuf, + math, + pango, + pangocairo, + xkbcommon, + wayland_client, + wlroots, +] + +sources = [ + 'main.c', + 'password.c', + 'render.c', + 'seat.c' +] + +if libpam.found() + sources += ['pam.c'] + dependencies += [libpam] +else + warning('The swaylock binary must be setuid when compiled without libpam') + warning('You must do this manually post-install: chmod a+s /path/to/swaylock') + sources += ['shadow.c'] +endif + +executable('swaylock', + sources, include_directories: [sway_inc], - dependencies: [ - cairo, - client_protos, - gdk_pixbuf, - libpam, - math, - pango, - pangocairo, - xkbcommon, - wayland_client, - wlroots, - ], + dependencies: dependencies, link_with: [lib_sway_common, lib_sway_client], install: true ) diff --git a/swaylock/pam.c b/swaylock/pam.c new file mode 100644 index 00000000..cac95a85 --- /dev/null +++ b/swaylock/pam.c @@ -0,0 +1,62 @@ +#define _XOPEN_SOURCE 500 +#include +#include +#include +#include +#include +#include +#include +#include "swaylock/swaylock.h" + +void initialize_pw_backend(void) { + // TODO: only call pam_start once. keep the same handle the whole time +} + +static int function_conversation(int num_msg, const struct pam_message **msg, + struct pam_response **resp, void *data) { + struct swaylock_password *pw = data; + /* PAM expects an array of responses, one for each message */ + struct pam_response *pam_reply = calloc( + num_msg, sizeof(struct pam_response)); + *resp = pam_reply; + for (int i = 0; i < num_msg; ++i) { + switch (msg[i]->msg_style) { + case PAM_PROMPT_ECHO_OFF: + case PAM_PROMPT_ECHO_ON: + pam_reply[i].resp = strdup(pw->buffer); // PAM clears and frees this + break; + case PAM_ERROR_MSG: + case PAM_TEXT_INFO: + break; + } + } + return PAM_SUCCESS; +} + +bool attempt_password(struct swaylock_password *pw) { + struct passwd *passwd = getpwuid(getuid()); + char *username = passwd->pw_name; + const struct pam_conv local_conversation = { + function_conversation, pw + }; + pam_handle_t *local_auth_handle = NULL; + int pam_err; + if ((pam_err = pam_start("swaylock", username, + &local_conversation, &local_auth_handle)) != PAM_SUCCESS) { + wlr_log(WLR_ERROR, "PAM returned error %d", pam_err); + } + if ((pam_err = pam_authenticate(local_auth_handle, 0)) != PAM_SUCCESS) { + wlr_log(WLR_ERROR, "pam_authenticate failed"); + goto fail; + } + // TODO: only call pam_end once we succeed at authing. refresh tokens beforehand + if ((pam_err = pam_end(local_auth_handle, pam_err)) != PAM_SUCCESS) { + wlr_log(WLR_ERROR, "pam_end failed"); + goto fail; + } + clear_password_buffer(pw); + return true; +fail: + clear_password_buffer(pw); + return false; +} diff --git a/swaylock/password.c b/swaylock/password.c index 7c686b34..6a956bcb 100644 --- a/swaylock/password.c +++ b/swaylock/password.c @@ -1,7 +1,6 @@ #define _XOPEN_SOURCE 500 #include #include -#include #include #include #include @@ -11,27 +10,6 @@ #include "swaylock/seat.h" #include "unicode.h" -static int function_conversation(int num_msg, const struct pam_message **msg, - struct pam_response **resp, void *data) { - struct swaylock_password *pw = data; - /* PAM expects an array of responses, one for each message */ - struct pam_response *pam_reply = calloc( - num_msg, sizeof(struct pam_response)); - *resp = pam_reply; - for (int i = 0; i < num_msg; ++i) { - switch (msg[i]->msg_style) { - case PAM_PROMPT_ECHO_OFF: - case PAM_PROMPT_ECHO_ON: - pam_reply[i].resp = strdup(pw->buffer); // PAM clears and frees this - break; - case PAM_ERROR_MSG: - case PAM_TEXT_INFO: - break; - } - } - return PAM_SUCCESS; -} - void clear_password_buffer(struct swaylock_password *pw) { // Use volatile keyword so so compiler can't optimize this out. volatile char *buffer = pw->buffer; @@ -42,35 +20,6 @@ void clear_password_buffer(struct swaylock_password *pw) { pw->len = 0; } -static bool attempt_password(struct swaylock_password *pw) { - struct passwd *passwd = getpwuid(getuid()); - char *username = passwd->pw_name; - const struct pam_conv local_conversation = { - function_conversation, pw - }; - pam_handle_t *local_auth_handle = NULL; - int pam_err; - // TODO: only call pam_start once. keep the same handle the whole time - if ((pam_err = pam_start("swaylock", username, - &local_conversation, &local_auth_handle)) != PAM_SUCCESS) { - wlr_log(WLR_ERROR, "PAM returned error %d", pam_err); - } - if ((pam_err = pam_authenticate(local_auth_handle, 0)) != PAM_SUCCESS) { - wlr_log(WLR_ERROR, "pam_authenticate failed"); - goto fail; - } - // TODO: only call pam_end once we succeed at authing. refresh tokens beforehand - if ((pam_err = pam_end(local_auth_handle, pam_err)) != PAM_SUCCESS) { - wlr_log(WLR_ERROR, "pam_end failed"); - goto fail; - } - clear_password_buffer(pw); - return true; -fail: - clear_password_buffer(pw); - return false; -} - static bool backspace(struct swaylock_password *pw) { if (pw->len != 0) { pw->buffer[--pw->len] = 0; diff --git a/swaylock/shadow.c b/swaylock/shadow.c new file mode 100644 index 00000000..1f10514c --- /dev/null +++ b/swaylock/shadow.c @@ -0,0 +1,128 @@ +#define _XOPEN_SOURCE +#include +#include +#include +#include +#include +#include +#include "swaylock/swaylock.h" + +static int comm[2][2]; + +void run_child(void) { + /* This code runs as root */ + struct passwd *pwent = getpwuid(getuid()); + if (!pwent) { + wlr_log_errno(WLR_ERROR, "failed to getpwuid"); + exit(EXIT_FAILURE); + } + char *encpw = pwent->pw_passwd; + if (strcmp(encpw, "x") == 0) { + struct spwd *swent = getspnam(pwent->pw_name); + if (!swent) { + wlr_log_errno(WLR_ERROR, "failed to getspnam"); + exit(EXIT_FAILURE); + } + encpw = swent->sp_pwdp; + } + wlr_log(WLR_DEBUG, "prepared to authorize user %s", pwent->pw_name); + + size_t size; + char *buf; + while (1) { + ssize_t amt; + amt = read(comm[0][0], &size, sizeof(size)); + if (amt == 0) { + break; + } else if (amt < 0) { + wlr_log_errno(WLR_ERROR, "read pw request"); + } + wlr_log(WLR_DEBUG, "received pw check request"); + buf = malloc(size); + if (!buf) { + wlr_log_errno(WLR_ERROR, "failed to malloc pw buffer"); + exit(EXIT_FAILURE); + } + size_t offs = 0; + do { + amt = read(comm[0][0], &buf[offs], size - offs); + if (amt <= 0) { + wlr_log_errno(WLR_ERROR, "failed to read pw"); + exit(EXIT_FAILURE); + } + offs += (size_t)amt; + } while (offs < size); + bool result = false; + char *c = crypt(buf, encpw); + if (c == NULL) { + wlr_log_errno(WLR_ERROR, "crypt"); + } + result = strcmp(c, encpw) == 0; + if (write(comm[1][1], &result, sizeof(result)) != sizeof(result)) { + wlr_log_errno(WLR_ERROR, "failed to write pw check result"); + exit(EXIT_FAILURE); + } + free(buf); + } + exit(EXIT_SUCCESS); +} + +void initialize_pw_backend(void) { + if (geteuid() != 0) { + wlr_log(WLR_ERROR, "swaylock needs to be setuid to read /etc/shadow"); + exit(EXIT_FAILURE); + } + if (pipe(comm[0]) != 0) { + wlr_log_errno(WLR_ERROR, "failed to create pipe"); + exit(EXIT_FAILURE); + } + if (pipe(comm[1]) != 0) { + wlr_log_errno(WLR_ERROR, "failed to create pipe"); + exit(EXIT_FAILURE); + } + pid_t child = fork(); + if (child == 0) { + close(comm[0][1]); + close(comm[1][0]); + run_child(); + } else if (child < 0) { + wlr_log_errno(WLR_ERROR, "failed to fork"); + exit(EXIT_FAILURE); + } + close(comm[0][0]); + close(comm[1][1]); + if (setgid(getgid()) != 0) { + wlr_log_errno(WLR_ERROR, "Unable to drop root"); + exit(EXIT_FAILURE); + } + if (setuid(getuid()) != 0) { + wlr_log_errno(WLR_ERROR, "Unable to drop root"); + exit(EXIT_FAILURE); + } +} + +bool attempt_password(struct swaylock_password *pw) { + bool result = false; + size_t len = pw->len + 1; + size_t offs = 0; + if (write(comm[0][1], &len, sizeof(len)) < 0) { + wlr_log_errno(WLR_ERROR, "Failed to request pw check"); + goto ret; + } + do { + ssize_t amt = write(comm[0][1], &pw->buffer[offs], len - offs); + if (amt < 0) { + wlr_log_errno(WLR_ERROR, "Failed to write pw buffer"); + goto ret; + } + offs += amt; + } while (offs < len); + if (read(comm[1][0], &result, sizeof(result)) != sizeof(result)) { + wlr_log_errno(WLR_ERROR, "Failed to read pw result"); + goto ret; + } + wlr_log(WLR_DEBUG, "pw result: %d", result); +ret: + clear_password_buffer(pw); + return result; +}