From: Lennart Poettering Date: Wed, 16 Jun 2010 19:54:17 +0000 (+0200) Subject: service: optionally call into PAM when dropping priviliges X-Git-Tag: v1~182 X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?p=elogind.git;a=commitdiff_plain;h=5b6319dceedd81f3f1ce7eb70ea5defaef43bcec service: optionally call into PAM when dropping priviliges --- diff --git a/Makefile.am b/Makefile.am index c78968fc0..b887d2621 100644 --- a/Makefile.am +++ b/Makefile.am @@ -290,7 +290,8 @@ systemd_LDADD = \ $(DBUS_LIBS) \ $(UDEV_LIBS) \ $(CGROUP_LIBS) \ - $(LIBWRAP_LIBS) + $(LIBWRAP_LIBS) \ + $(PAM_LIBS) test_engine_SOURCES = \ $(COMMON_SOURCES) \ diff --git a/configure.ac b/configure.ac index ae2520044..c5c5cc0d4 100644 --- a/configure.ac +++ b/configure.ac @@ -111,27 +111,61 @@ AC_SUBST(CGROUP_LIBS) AC_ARG_ENABLE([tcpwrap], AS_HELP_STRING([--disable-tcpwrap],[Disable optional TCP wrappers support]), [case "${enableval}" in - yes) tcpwrap=yes ;; - no) tcpwrap=no ;; + yes) have_tcpwrap=yes ;; + no) have_tcpwrap=no ;; *) AC_MSG_ERROR(bad value ${enableval} for --disable-tcpwrap) ;; esac], - [tcpwrap=auto]) + [have_tcpwrap=auto]) -if test "x${tcpwrap}" != xno ; then +if test "x${have_tcpwrap}" != xno ; then ACX_LIBWRAP if test "x${LIBWRAP_LIBS}" = x ; then - if test "x$tcpwrap" = xyes ; then - AC_MSG_ERROR([*** TCP wrappers support not found]) + if test "x$have_tcpwrap" = xyes ; then + AC_MSG_ERROR([*** TCP wrappers support not found.]) fi else - tcpwrap=yes + have_tcpwrap=yes fi else - LIBWRAP_LIBS= + LIBWRAP_LIBS= fi - AC_SUBST(LIBWRAP_LIBS) +AC_ARG_ENABLE([pam], + AS_HELP_STRING([--disable-pam],[Disable optional PAM support]), + [case "${enableval}" in + yes) have_pam=yes ;; + no) have_pam=no ;; + *) AC_MSG_ERROR(bad value ${enableval} for --disable-pam) ;; + esac], + [have_pam=auto]) + +if test "x${have_pam}" != xno ; then + AC_CHECK_HEADERS( + [security/pam_modules.h security/pam_modutil.h security/pam_ext.h], + [have_pam=yes], + [if test "x$have_pam" = xyes ; then + AC_MSG_ERROR([*** PAM headers not found.]) + fi]) + + AC_CHECK_LIB( + [pam], + [pam_syslog], + [have_pam=yes], + [if test "x$have_pam" = xyes ; then + AC_MSG_ERROR([*** libpam not found.]) + fi]) + + if test "x$have_pam" = xyes ; then + PAM_LIBS="-lpam -lpam_misc" + AC_DEFINE(HAVE_PAM, 1, [PAM available]) + fi +else + PAM_LIBS= +fi +AC_SUBST(PAM_LIBS) +AM_CONDITIONAL([HAVE_PAM], [test "x$have_pam" != xno]) + have_gtk=no AC_ARG_ENABLE(gtk, AS_HELP_STRING([--disable-gtk], [disable GTK tools])) if test "x$enable_gtk" != "xno"; then @@ -336,7 +370,8 @@ echo " Syslog service: ${SPECIAL_SYSLOG_SERVICE} D-Bus service: ${SPECIAL_DBUS_SERVICE} Gtk: ${have_gtk} - tcpwrap: ${tcpwrap} + tcpwrap: ${have_tcpwrap} + PAM: ${have_pam} prefix: ${prefix} root dir: ${with_rootdir} udev rules dir: ${with_udevrulesdir} diff --git a/fixme b/fixme index 17a8ef8cf..7a443f4cd 100644 --- a/fixme +++ b/fixme @@ -51,14 +51,16 @@ * write utmp record a la upstart for processes -* run PAM session stuff +* follow property change dbus spec -* use setproctitle() when forking, before exec() (waiting for (PR_SET_PROCTITLE_AREA to enter the kernel) +* pam module -* follow property change dbus spec +* selinux Regularly: * look for close() vs. close_nointr() vs. close_nointr_nofail() * check for strerror(r) instead of strerror(-r) + +* Use PR_SET_PROCTITLE_AREA if it becomes available in the kernel diff --git a/src/dbus-execute.h b/src/dbus-execute.h index 243854f89..1e83caca5 100644 --- a/src/dbus-execute.h +++ b/src/dbus-execute.h @@ -44,7 +44,8 @@ " \n" \ " \n" \ " \n" \ - " \n" + " \n" \ + " \n" #define BUS_EXEC_CONTEXT_PROPERTIES(interface, context) \ { interface, "Environment", bus_property_append_strv, "as", (context).environment }, \ @@ -73,7 +74,8 @@ { interface, "User", bus_property_append_string, "s", (context).user }, \ { interface, "Group", bus_property_append_string, "s", (context).group }, \ { interface, "SupplementaryGroups", bus_property_append_strv, "as", (context).supplementary_groups }, \ - { interface, "TCPWrapName", bus_property_append_string, "s", (context).tcpwrap_name } + { interface, "TCPWrapName", bus_property_append_string, "s", (context).tcpwrap_name }, \ + { interface, "PAMName", bus_property_append_string, "s", (context).pam_name } int bus_execute_append_output(Manager *m, DBusMessageIter *i, const char *property, void *data); int bus_execute_append_input(Manager *m, DBusMessageIter *i, const char *property, void *data); diff --git a/src/dbus-manager.c b/src/dbus-manager.c index c80b22b9e..d4c9e8f0e 100644 --- a/src/dbus-manager.c +++ b/src/dbus-manager.c @@ -626,7 +626,7 @@ static DBusHandlerResult bus_manager_message_handler(DBusConnection *connection return bus_send_error_reply(m, message, NULL, r); } - e = strv_env_merge(m->environment, l, NULL); + e = strv_env_merge(2, m->environment, l); strv_free(l); if (!e) @@ -650,7 +650,7 @@ static DBusHandlerResult bus_manager_message_handler(DBusConnection *connection return bus_send_error_reply(m, message, NULL, r); } - e = strv_env_delete(m->environment, l, NULL); + e = strv_env_delete(m->environment, 1, l); strv_free(l); if (!e) diff --git a/src/execute.c b/src/execute.c index 1a7871b4e..ed10ea2bf 100644 --- a/src/execute.c +++ b/src/execute.c @@ -37,6 +37,10 @@ #include #include +#ifdef HAVE_PAM +#include +#endif + #include "execute.h" #include "strv.h" #include "macro.h" @@ -720,6 +724,164 @@ static int enforce_user(const ExecContext *context, uid_t uid) { return 0; } +#ifdef HAVE_PAM + +static int null_conv( + int num_msg, + const struct pam_message **msg, + struct pam_response **resp, + void *appdata_ptr) { + + /* We don't support conversations */ + + return PAM_CONV_ERR; +} + +static int setup_pam( + const char *name, + const char *user, + const char *tty, + char ***pam_env, + int fds[], unsigned n_fds) { + + static const struct pam_conv conv = { + .conv = null_conv, + .appdata_ptr = NULL + }; + + pam_handle_t *handle = NULL; + sigset_t ss, old_ss; + int pam_code = PAM_SUCCESS; + char **e = NULL; + bool close_session = false; + pid_t pam_pid = 0, parent_pid; + + assert(name); + assert(user); + assert(pam_env); + + /* We set up PAM in the parent process, then fork. The child + * will then stay around untill killed via PR_GET_PDEATHSIG or + * systemd via the cgroup logic. It will then remove the PAM + * session again. The parent process will exec() the actual + * daemon. We do things this way to ensure that the main PID + * of the daemon is the one we initially fork()ed. */ + + if ((pam_code = pam_start(name, user, &conv, &handle)) != PAM_SUCCESS) { + handle = NULL; + goto fail; + } + + if (tty) + if ((pam_code = pam_set_item(handle, PAM_TTY, tty)) != PAM_SUCCESS) + goto fail; + + if ((pam_code = pam_acct_mgmt(handle, PAM_SILENT)) != PAM_SUCCESS) + goto fail; + + if ((pam_code = pam_open_session(handle, PAM_SILENT)) != PAM_SUCCESS) + goto fail; + + close_session = true; + + if ((pam_code = pam_setcred(handle, PAM_ESTABLISH_CRED | PAM_SILENT)) != PAM_SUCCESS) + goto fail; + + if ((!(e = pam_getenvlist(handle)))) { + pam_code = PAM_BUF_ERR; + goto fail; + } + + /* Block SIGTERM, so that we know that it won't get lost in + * the child */ + if (sigemptyset(&ss) < 0 || + sigaddset(&ss, SIGTERM) < 0 || + sigprocmask(SIG_BLOCK, &ss, &old_ss) < 0) + goto fail; + + parent_pid = getpid(); + + if ((pam_pid = fork()) < 0) + goto fail; + + if (pam_pid == 0) { + int sig; + int r = EXIT_PAM; + + /* The child's job is to reset the PAM session on + * termination */ + + /* This string must fit in 10 chars (i.e. the length + * of "/sbin/init") */ + rename_process("sd:pam"); + + /* Make sure we don't keep open the passed fds in this + child. We assume that otherwise only those fds are + open here that have been opened by PAM. */ + close_many(fds, n_fds); + + /* Wait until our parent died. This will most likely + * not work since the kernel does not allow + * unpriviliged paretns kill their priviliged children + * this way. We rely on the control groups kill logic + * to do the rest for us. */ + if (prctl(PR_SET_PDEATHSIG, SIGTERM) < 0) + goto child_finish; + + /* Check if our parent process might already have + * died? */ + if (getppid() == parent_pid) { + if (sigwait(&ss, &sig) < 0) + goto child_finish; + + assert(sig == SIGTERM); + } + + /* Only if our parent died we'll end the session */ + if (getppid() != parent_pid) + if ((pam_code = pam_close_session(handle, PAM_DATA_SILENT)) != PAM_SUCCESS) + goto child_finish; + + r = 0; + + child_finish: + pam_end(handle, pam_code | PAM_DATA_SILENT); + _exit(r); + } + + /* If the child was forked off successfully it will do all the + * cleanups, so forget about the handle here. */ + handle = NULL; + + /* Unblock SIGSUR1 again in the parent */ + if (sigprocmask(SIG_SETMASK, &old_ss, NULL) < 0) + goto fail; + + /* We close the log explicitly here, since the PAM modules + * might have opened it, but we don't want this fd around. */ + closelog(); + + return 0; + +fail: + if (handle) { + if (close_session) + pam_code = pam_close_session(handle, PAM_DATA_SILENT); + + pam_end(handle, pam_code | PAM_DATA_SILENT); + } + + strv_free(e); + + closelog(); + + if (pam_pid > 1) + kill(pam_pid, SIGTERM); + + return EXIT_PAM; +} +#endif + int exec_spawn(ExecCommand *command, char **argv, const ExecContext *context, @@ -777,13 +939,17 @@ int exec_spawn(ExecCommand *command, const char *username = NULL, *home = NULL; uid_t uid = (uid_t) -1; gid_t gid = (gid_t) -1; - char **our_env = NULL, **final_env = NULL; + char **our_env = NULL, **pam_env = NULL, **final_env = NULL; unsigned n_env = 0; int saved_stdout = -1, saved_stdin = -1; bool keep_stdout = false, keep_stdin = false; /* child */ + /* This string must fit in 10 chars (i.e. the length + * of "/sbin/init") */ + rename_process("sd:exec"); + /* We reset exactly these signals, since they are the * only ones we set to SIG_IGN in the main daemon. All * others we leave untouched because we set them to @@ -928,6 +1094,25 @@ int exec_spawn(ExecCommand *command, } } +#ifdef HAVE_PAM + if (context->pam_name && username) { + /* Make sure no fds leak into the PAM + * supervisor process. We will call this later + * on again to make sure that any fds leaked + * by the PAM modules get closed before our + * exec(). */ + if (close_all_fds(fds, n_fds) < 0) { + r = EXIT_FDS; + goto fail; + } + + if (setup_pam(context->pam_name, username, context->tty_path, &pam_env, fds, n_fds) < 0) { + r = EXIT_PAM; + goto fail; + } + } +#endif + if (apply_permissions) if (enforce_groups(context, username, uid) < 0) { r = EXIT_GROUP; @@ -1049,7 +1234,13 @@ int exec_spawn(ExecCommand *command, assert(n_env <= 6); - if (!(final_env = strv_env_merge(environment, our_env, context->environment, NULL))) { + if (!(final_env = strv_env_merge( + 4, + environment, + our_env, + context->environment, + pam_env, + NULL))) { r = EXIT_MEMORY; goto fail; } @@ -1060,6 +1251,7 @@ int exec_spawn(ExecCommand *command, fail: strv_free(our_env); strv_free(final_env); + strv_free(pam_env); if (saved_stdin >= 0) close_nointr_nofail(saved_stdin); @@ -1133,6 +1325,9 @@ void exec_context_done(ExecContext *c) { strv_free(c->supplementary_groups); c->supplementary_groups = NULL; + free(c->pam_name); + c->pam_name = NULL; + if (c->capabilities) { cap_free(c->capabilities); c->capabilities = NULL; @@ -1332,6 +1527,9 @@ void exec_context_dump(ExecContext *c, FILE* f, const char *prefix) { fputs("\n", f); } + if (c->pam_name) + fprintf(f, "%sPAMName: %s", prefix, c->pam_name); + if (strv_length(c->read_write_dirs) > 0) { fprintf(f, "%sReadWriteDirs:", prefix); strv_fprintf(f, c->read_write_dirs); @@ -1613,6 +1811,9 @@ const char* exit_status_to_string(ExitStatus status) { case EXIT_TCPWRAP: return "TCPWRAP"; + case EXIT_PAM: + return "PAM"; + default: return NULL; } diff --git a/src/execute.h b/src/execute.h index 1adf41ea6..e61804949 100644 --- a/src/execute.h +++ b/src/execute.h @@ -116,6 +116,8 @@ struct ExecContext { char *group; char **supplementary_groups; + char *pam_name; + char **read_write_dirs, **read_only_dirs, **inaccessible_dirs; unsigned long mount_flags; @@ -182,7 +184,8 @@ typedef enum ExitStatus { EXIT_SETSID, /* 220 */ EXIT_CONFIRM, EXIT_STDERR, - EXIT_TCPWRAP + EXIT_TCPWRAP, + EXIT_PAM } ExitStatus; diff --git a/src/install.c b/src/install.c index e3db63769..c3dbe8b19 100644 --- a/src/install.c +++ b/src/install.c @@ -69,7 +69,7 @@ static int help(void) { " enable [NAME...] Enable one or more units\n" " disable [NAME...] Disable one or more units\n" " test [NAME...] Test whether any of the specified units are enabled\n", - __progname); + program_invocation_short_name); return 0; } diff --git a/src/load-fragment.c b/src/load-fragment.c index f409776e8..1f082d57b 100644 --- a/src/load-fragment.c +++ b/src/load-fragment.c @@ -1392,7 +1392,8 @@ static int load_from_path(Unit *u, const char *path) { { "InaccessibleDirectories",config_parse_path_strv, &(context).inaccessible_dirs, section }, \ { "PrivateTmp", config_parse_bool, &(context).private_tmp, section }, \ { "MountFlags", config_parse_mount_flags, &(context), section }, \ - { "TCPWrapName", config_parse_string, &(context).tcpwrap_name, section } + { "TCPWrapName", config_parse_string, &(context).tcpwrap_name, section }, \ + { "PAMName", config_parse_string, &(context).pam_name, section } const ConfigItem items[] = { { "Names", config_parse_names, u, "Unit" }, diff --git a/src/log.c b/src/log.c index 265a7f10e..b7173eb55 100644 --- a/src/log.c +++ b/src/log.c @@ -267,7 +267,7 @@ static int write_to_syslog( zero(iovec); IOVEC_SET_STRING(iovec[0], header_priority); IOVEC_SET_STRING(iovec[1], header_time); - IOVEC_SET_STRING(iovec[2], __progname); + IOVEC_SET_STRING(iovec[2], program_invocation_short_name); IOVEC_SET_STRING(iovec[3], header_pid); IOVEC_SET_STRING(iovec[4], buffer); @@ -302,7 +302,7 @@ static int write_to_kmsg( zero(iovec); IOVEC_SET_STRING(iovec[0], header_priority); - IOVEC_SET_STRING(iovec[1], __progname); + IOVEC_SET_STRING(iovec[1], program_invocation_short_name); IOVEC_SET_STRING(iovec[2], header_pid); IOVEC_SET_STRING(iovec[3], buffer); IOVEC_SET_STRING(iovec[4], "\n"); diff --git a/src/main.c b/src/main.c index 9cdbf2e89..e5bdf849b 100644 --- a/src/main.c +++ b/src/main.c @@ -504,7 +504,7 @@ static int help(void) { " --dump-configuration-items Dump understood unit configuration items\n" " --confirm-spawn Ask for confirmation when spawning processes\n" " --introspect[=INTERFACE] Extract D-Bus interface data\n", - __progname); + program_invocation_short_name); return 0; } diff --git a/src/manager.c b/src/manager.c index 97d05b52c..385b371db 100644 --- a/src/manager.c +++ b/src/manager.c @@ -109,7 +109,7 @@ static int manager_setup_notify(Manager *m) { return -ENOMEM; ne[1] = NULL; - t = strv_env_merge(m->environment, ne, NULL); + t = strv_env_merge(2, m->environment, ne); free(ne[0]); if (!t) diff --git a/src/strv.c b/src/strv.c index 2ebd0ee53..85599fe92 100644 --- a/src/strv.c +++ b/src/strv.c @@ -374,7 +374,9 @@ char **strv_remove(char **l, const char *s) { static int env_append(char **r, char ***k, char **a) { assert(r); assert(k); - assert(a); + + if (!a) + return 0; /* Add the entries of a to *k unless they already exist in *r * in which case they are overriden instead. This assumes @@ -400,38 +402,33 @@ static int env_append(char **r, char ***k, char **a) { return 0; } -char **strv_env_merge(char **x, ...) { +char **strv_env_merge(unsigned n_lists, ...) { size_t n = 0; char **l, **k, **r; va_list ap; + unsigned i; /* Merges an arbitrary number of environment sets */ - if (x) { - n += strv_length(x); - - va_start(ap, x); - while ((l = va_arg(ap, char**))) - n += strv_length(l); - va_end(ap); + va_start(ap, n_lists); + for (i = 0; i < n_lists; i++) { + l = va_arg(ap, char**); + n += strv_length(l); } - + va_end(ap); if (!(r = new(char*, n+1))) return NULL; k = r; - if (x) { - if (env_append(r, &k, x) < 0) + va_start(ap, n_lists); + for (i = 0; i < n_lists; i++) { + l = va_arg(ap, char**); + if (env_append(r, &k, l) < 0) goto fail; - - va_start(ap, x); - while ((l = va_arg(ap, char**))) - if (env_append(r, &k, l) < 0) - goto fail; - va_end(ap); } + va_end(ap); *k = NULL; @@ -472,7 +469,7 @@ static bool env_match(const char *t, const char *pattern) { return false; } -char **strv_env_delete(char **x, ...) { +char **strv_env_delete(char **x, unsigned n_lists, ...) { size_t n = 0, i = 0; char **l, **k, **r, **j; va_list ap; @@ -486,12 +483,14 @@ char **strv_env_delete(char **x, ...) { return NULL; STRV_FOREACH(k, x) { - va_start(ap, x); + va_start(ap, n_lists); - while ((l = va_arg(ap, char**))) + for (i = 0; i < n_lists; i++) { + l = va_arg(ap, char**); STRV_FOREACH(j, l) if (env_match(*k, *j)) goto delete; + } va_end(ap); diff --git a/src/strv.h b/src/strv.h index 2d24b4e39..af1398399 100644 --- a/src/strv.h +++ b/src/strv.h @@ -55,8 +55,8 @@ char **strv_split_quoted(const char *s) _malloc_; char *strv_join(char **l, const char *separator) _malloc_; -char **strv_env_merge(char **x, ...) _sentinel_; -char **strv_env_delete(char **x, ...) _sentinel_; +char **strv_env_merge(unsigned n_lists, ...); +char **strv_env_delete(char **x, unsigned n_lists, ...); #define STRV_FOREACH(s, l) \ for ((s) = (l); (s) && *(s); (s)++) diff --git a/src/systemctl.c b/src/systemctl.c index 663cb833b..35ca082ab 100644 --- a/src/systemctl.c +++ b/src/systemctl.c @@ -1322,7 +1322,7 @@ static int help(void) { " show-environment Dump environment\n" " set-environment [NAME=VALUE...] Set one or more environment variables\n" " unset-environment [NAME...] Unset one or more environment variables\n", - __progname); + program_invocation_short_name); return 0; } diff --git a/src/util.c b/src/util.c index 71409034a..ed0991a68 100644 --- a/src/util.c +++ b/src/util.c @@ -43,6 +43,7 @@ #include #include #include +#include #include "macro.h" #include "util.h" @@ -222,6 +223,13 @@ void close_nointr_nofail(int fd) { errno = saved_errno; } +void close_many(const int fds[], unsigned n_fd) { + unsigned i; + + for (i = 0; i < n_fd; i++) + close_nointr_nofail(fds[i]); +} + int parse_boolean(const char *v) { assert(v); @@ -2161,6 +2169,19 @@ fallback: return random() * RAND_MAX + random(); } +void rename_process(const char name[8]) { + assert(name); + + prctl(PR_SET_NAME, name); + + /* This is a like a poor man's setproctitle(). The string + * passed should fit in 7 chars (i.e. the length of + * "systemd") */ + + if (program_invocation_name) + strncpy(program_invocation_name, name, strlen(program_invocation_name)); +} + static const char *const ioprio_class_table[] = { [IOPRIO_CLASS_NONE] = "none", [IOPRIO_CLASS_RT] = "realtime", diff --git a/src/util.h b/src/util.h index 0b397ac0d..1e5ee28eb 100644 --- a/src/util.h +++ b/src/util.h @@ -104,6 +104,7 @@ bool first_word(const char *s, const char *word); int close_nointr(int fd); void close_nointr_nofail(int fd); +void close_many(const int fds[], unsigned n_fd); int parse_boolean(const char *v); int parse_usec(const char *t, usec_t *usec); @@ -252,7 +253,7 @@ bool is_device_path(const char *path); int dir_is_empty(const char *path); -extern char * __progname; +void rename_process(const char name[8]); const char *ioprio_class_to_string(int i); int ioprio_class_from_string(const char *s);