/* pam_exec.c - invoke an external program as part of a PAM stack * * Copyright (c) 2006 Thorsten Kukuk * Copyright (c) 2006 Daniel Richard G. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, and the entire permission notice in its entirety, * including the disclaimer of warranties. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote * products derived from this software without specific prior * written permission. * * ALTERNATIVELY, this product may be distributed under the terms of * the GNU Public License, in which case the provisions of the GPL are * required INSTEAD OF the above restrictions. (This clause is * necessary due to a potential bad interaction between the GPL and * the restrictions contained in a BSD-style copyright.) * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. */ /* Solaris needs this bit to give us the correct prototypes for * get{pwnam,pwuid,grnam,grgid}_r() */ #ifdef sun # define _POSIX_PTHREAD_SEMANTICS #endif #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* Activate the appropriate bits in the PAM headers */ #define PAM_SM_AUTH #define PAM_SM_ACCOUNT #define PAM_SM_SESSION #define PAM_SM_PASSWORD #include /* needed on Solaris */ #include /* File descriptor for the setenv channel */ #define SETENV_FILENO 3 #if SETENV_FILENO <= 2 || SETENV_FILENO >= FD_SETSIZE # error SETENV_FILENO value is outside of legal range #endif /* These many bytes are allocated on the stack, so don't push it */ #define LINEBUF_SIZE 256 /* Workaround for Solaris */ #ifndef LOG_AUTHPRIV # define LOG_AUTHPRIV LOG_AUTH #endif typedef enum { PE_AUTHENTICATE, /* PE_SETCRED, */ PE_ACCT_MGMT, PE_OPEN_SESSION, PE_CLOSE_SESSION, PE_CHAUTHTOK } PamExecAction; struct exec_options { char *command; /* sh-compatible command string */ const char *runas_spec; /* chown(1)-like user/group spec */ uid_t runas_uid; /* Run command as this user... */ gid_t runas_gid; /* ...and this group */ int flags; /* Raw PAM flags */ int allow_authtok; /* boolean */ int allow_setenv; /* boolean */ int debug; /* boolean */ int expose_account; /* boolean */ int fail_delay; /* integer (milliseconds) */ int maxtime; /* integer (seconds) */ int on_open; /* boolean */ int on_close; /* boolean */ int silent; /* boolean */ int warnings; /* boolean */ int try_first_pass; /* boolean */ int use_first_pass; /* boolean */ /* int use_mapped_pass; / * boolean */ }; /* Work around any built-in log() math function */ #undef log #define log my_log_fn static void log(int err, const char *format, ...) { va_list args; openlog("pam_exec", LOG_CONS | LOG_PID, LOG_AUTHPRIV); va_start(args, format); vsyslog(LOG_AUTHPRIV | err, format, args); va_end(args); closelog(); } /* Returns true iff the argument is a numeric string */ static int is_numeric(const char *s) { int i; if (s == NULL || *s == '\0') return 0; for (i = 0; s[i] != '\0'; i++) if (! isdigit(s[i])) return 0; return 1; } /* Returns true iff fd is an open file descriptor */ static int is_open_fd(int fd) { if (fd < 0) return 0; return fcntl(fd, F_GETFD) != -1; } /* Returns true iff the given string s, in a buffer of size bufsize, * consists entirely of printable characters and is properly * null-terminated. */ static int is_safe_string(const char *s, size_t bufsize) { size_t i; if (s == NULL || bufsize == 0) return 0; for (i = 0; i < bufsize && s[i] != '\0'; i++) if (! isprint(s[i])) return 0; /* If (i == bufsize) then there is no terminating null */ return i < bufsize; } /* Reads a line of text from the given stream, strips off the trailing * newline / carriage return, checks that it is clean, and stores it in the * buffer linebuf with the given size. Returns false if the stream is at * end-of-file, if an error occurred, if the line was unclean or if it was * too long to fit into the buffer; true otherwise. */ static int get_line(struct exec_options *options, char *linebuf, size_t size, FILE *stream) { int too_long = 0; int safe; char *nl; if (options == NULL || linebuf == NULL || size == 0 || stream == NULL) { return 0; } for (;;) { if (fgets(linebuf, (int)size, stream) == NULL) { linebuf[0] = '\0'; return 0; } nl = strchr(linebuf, '\n'); if (nl != NULL) break; too_long = 1; } /* Chop off the trailing newline / carriage return */ *nl = '\0'; if (nl != linebuf && *(nl - 1) == '\r') *(nl - 1) = '\0'; safe = is_safe_string(linebuf, size); if (options->debug) log(LOG_DEBUG, "Read a line of length %d from fd %d", strlen(linebuf), fileno(stream)); if (options->warnings && ! safe) log(LOG_WARNING, "Command generated output with unsafe characters"); if (options->warnings && too_long) log(LOG_WARNING, "Command generated an overlong line"); if (! safe || too_long) linebuf[0] = '\0'; /* for safety */ return safe && ! too_long; } /* Parses a chown(1)-like user/group spec, of the form "user:group" or * "user" or ":group". The associated user ID and/or group ID are stored * into *uid_ptr and *gid_ptr. If the group is not specified, then the * user's primary group is returned. If the user is not specified, then no * user ID is returned. The user and group can be specified either by name, * or by the numeric ID in string form. Returns a PAM error code. */ static int parse_usergroup_spec(const char *spec, uid_t *uid_ptr, gid_t *gid_ptr) { int status = PAM_SUCCESS; char *spec_copy; const char *user; const char *group = NULL; size_t pw_buflen; char *pw_buf = NULL; struct passwd pw, *pw_ptr; size_t gr_buflen; char *gr_buf = NULL; struct group gr, *gr_ptr; int ret; if (spec == NULL || *spec == '\0') return PAM_SERVICE_ERR; spec_copy = strdup(spec); if (spec_copy == NULL) { status = PAM_BUF_ERR; goto cleanup; } user = spec_copy; { char *sep = strchr(spec_copy, ':'); if (sep != NULL) { *sep = '\0'; group = sep + 1; } } if (*user != '\0') { /* Look up user ID/name */ pw_buflen = (size_t)sysconf(_SC_GETPW_R_SIZE_MAX); pw_buf = (char *)malloc(pw_buflen); if (pw_buf == NULL) { status = PAM_BUF_ERR; goto cleanup; } if (is_numeric(user)) { uid_t id = (uid_t)atoi(user); ret = getpwuid_r(id, &pw, pw_buf, pw_buflen, &pw_ptr); } else ret = getpwnam_r(user, &pw, pw_buf, pw_buflen, &pw_ptr); if (ret == 0 && pw_ptr == &pw) { *uid_ptr = pw.pw_uid; *gid_ptr = pw.pw_gid; } else { log(LOG_ERR, "Unable to get passwd info for user \"%s\"", user); status = PAM_SYSTEM_ERR; } } if (group != NULL && *group != '\0') { /* Look up group name */ gr_buflen = (size_t)sysconf(_SC_GETGR_R_SIZE_MAX); gr_buf = (char *)malloc(gr_buflen); if (gr_buf == NULL) { status = PAM_BUF_ERR; goto cleanup; } if (is_numeric(group)) { gid_t id = (gid_t)atoi(group); ret = getgrgid_r(id, &gr, gr_buf, gr_buflen, &gr_ptr); } else ret = getgrnam_r(group, &gr, gr_buf, gr_buflen, &gr_ptr); if (ret == 0 && gr_ptr == &gr) { *gid_ptr = gr.gr_gid; } else { log(LOG_ERR, "Unable to get info for group \"%s\"", group); status = PAM_SYSTEM_ERR; } } cleanup: if (gr_buf != NULL) free(gr_buf); if (pw_buf != NULL) free(pw_buf); if (spec_copy != NULL) free(spec_copy); return status; } /* Parses the "command-line" options passed to the module in the PAM * configuration file, as well as the flags variable, and sets the fields * of the exec_options struct accordingly. Returns a PAM error code. */ static int parse_options(struct exec_options *options, int flags, int argc, const char **argv, PamExecAction action) { size_t cmd_len = 0; int cmd_i0; int i; memset(options, 0, sizeof(struct exec_options)); /* Set the non-zero/non-NULL defaults */ options->runas_uid = (uid_t)-1; options->runas_gid = (gid_t)-1; options->flags = flags; options->on_open = 1; options->on_close = 1; options->warnings = 1; if (flags & PAM_SILENT) options->silent = 1; for (i = 0; i < argc; i++) { const char *arg; const char *value; int inapplicable_option = 0; arg = argv[i]; value = strchr(arg, '='); if (value != NULL) ++value; #define MATCH_KEY(s) (strcmp(s, arg) == 0) #define MATCH_KEY_VAL(s) (strncmp(s "=", arg, sizeof(s "=") - 1) == 0) if (MATCH_KEY("authtok")) { if (action == PE_AUTHENTICATE || action == PE_CHAUTHTOK) { options->allow_authtok = 1; } else inapplicable_option = 1; } else if (MATCH_KEY("debug")) { options->debug = 1; } else if (MATCH_KEY("exec")) { i++; break; } else if (MATCH_KEY("expose_account")) { options->expose_account = 1; } #ifdef PAM_FAIL_DELAY else if (MATCH_KEY_VAL("fail_delay")) { if (action == PE_AUTHENTICATE) options->fail_delay = atoi(value); else inapplicable_option = 1; } #endif else if (MATCH_KEY_VAL("maxtime")) { /* options->maxtime = atoi(value); */ } else if (MATCH_KEY("no_warn")) { options->warnings = 0; } else if (MATCH_KEY("on_close_only")) { if (action == PE_OPEN_SESSION || action == PE_CLOSE_SESSION) { options->on_open = 0; } else inapplicable_option = 1; } else if (MATCH_KEY("on_open_only")) { if (action == PE_OPEN_SESSION || action == PE_CLOSE_SESSION) { options->on_close = 0; } else inapplicable_option = 1; } else if (MATCH_KEY_VAL("runas")) { options->runas_spec = value; } else if (MATCH_KEY("setenv")) { options->allow_setenv = 1; } else if (MATCH_KEY("try_first_pass")) { options->try_first_pass = 1; } else if (MATCH_KEY("use_first_pass")) { options->use_first_pass = 1; } else if (MATCH_KEY("use_mapped_pass")) { /* options->use_mapped_pass = 1; */ } else { log(LOG_ERR, "Unrecognized option: %s", arg); return PAM_SERVICE_ERR; } #undef MATCH_KEY #undef MATCH_KEY_VAL if (inapplicable_option) { log(LOG_ERR, "Option not applicable: %s", arg); return PAM_SERVICE_ERR; } } if (i == argc) { log(LOG_ERR, "No command was specified"); return PAM_SERVICE_ERR; } /* Some sanity checking */ if (options->fail_delay < 0) options->fail_delay = 0; /* if (options->maxtime < 0) options->maxtime = 0; */ if (! options->on_open && ! options->on_close) { log(LOG_ERR, "The on_open_only and on_close_only options are mutually incompatible"); return PAM_SERVICE_ERR; } /* Assemble the remaining args into a command string */ cmd_i0 = i; for (i = cmd_i0; i < argc; i++) cmd_len += strlen(argv[i]) + 1; options->command = (char *)malloc(cmd_len); options->command[0] = '\0'; for (i = cmd_i0; i < argc; i++) { /* O(n^2), but no one really cared... */ if (i > cmd_i0) strcat(options->command, " "); strcat(options->command, argv[i]); } /* Parse the argument to the "runas" option, if given */ if (options->runas_spec != NULL) { int ret; ret = parse_usergroup_spec(options->runas_spec, &options->runas_uid, &options->runas_gid); if (ret != PAM_SUCCESS) return ret; if (options->debug) log(LOG_DEBUG, "Command will be executed with uid=%d gid=%d", (int)options->runas_uid, (int)options->runas_gid); } return PAM_SUCCESS; } /* Reads the command's stdout, and forwards it to the PAM conversation * function so that the user can see it. Returns non-zero on error. */ static int read_command_stdout(pam_handle_t *pamh, struct exec_options *options, FILE *stream) { char linebuf[LINEBUF_SIZE]; struct pam_conv *conv; struct pam_message msg; const struct pam_message *msg_ptrs[1]; struct pam_response *resp; int ret; ret = pam_get_item(pamh, PAM_CONV, (const void **)&conv); if (ret != PAM_SUCCESS) return 1; msg.msg_style = PAM_TEXT_INFO; msg.msg = linebuf; msg_ptrs[0] = &msg; while (get_line(options, linebuf, sizeof(linebuf), stream)) { if (options->silent) continue; resp = NULL; ret = conv->conv(1, msg_ptrs, &resp, conv->appdata_ptr); if (ret != PAM_SUCCESS) return 1; } return 0; } /* Reads the command's stderr, and logs it. Returns zero. */ static int read_command_stderr(pam_handle_t *pamh, struct exec_options *options, FILE *stream) { char linebuf[LINEBUF_SIZE]; pamh = pamh; /* unused */ while (get_line(options, linebuf, sizeof(linebuf), stream)) log(LOG_INFO, "> %s", linebuf); return 0; } /* Reads the command's setenv channel, and if the options allow it, sets or * unsets an environment variable as directed. Returns non-zero on error. */ static int read_command_setenv(pam_handle_t *pamh, struct exec_options *options, FILE *stream) { int status = 0; char linebuf[LINEBUF_SIZE]; char *s; int ret; if (! get_line(options, linebuf, sizeof(linebuf), stream)) return 0; if (! options->allow_setenv) { if (options->warnings) log(LOG_WARNING, "Attempted to set/unset an environment variable without permission"); return 0; /* Access Denied */ } s = strchr(linebuf, '='); if (s != NULL && strncmp("set ", linebuf, (size_t)4) == 0) { /* Set an environment variable */ s = linebuf + 4; if (options->debug) log(LOG_DEBUG, "setenv: %s", s); ret = pam_putenv(pamh, s); if (ret != PAM_SUCCESS) { log(LOG_ERR, "Error setting \"%s\": %s", s, pam_strerror(pamh, ret)); status = 1; } } else if (s == NULL && strncmp("unset ", linebuf, (size_t)6) == 0) { /* Unset an environment variable */ s = linebuf + 6; if (options->debug) log(LOG_DEBUG, "unsetenv: %s", s); ret = pam_putenv(pamh, s); #ifdef PAM_BAD_ITEM if (ret == PAM_BAD_ITEM) { if (options->debug) log(LOG_DEBUG, "(\"%s\" was already unset)", s); } else #endif if (ret != PAM_SUCCESS) { log(LOG_ERR, "Error unsetting environment variable \"%s\": %s", s, pam_strerror(pamh, ret)); status = 1; } } else { log(LOG_ERR, "Malformed environment variable directive"); status = 1; } return status; } /* Reads the command's three channels of output, and dispatches * accordingly. The three file descriptors passed in will be closed. * Returns a PAM error code. */ static int read_command_output(pam_handle_t *pamh, struct exec_options *options, int cmd_stdout_fd, int cmd_stderr_fd, int cmd_setenv_fd) { FILE *cmd_stdout; FILE *cmd_stderr; FILE *cmd_setenv; struct timeval timeout; if ((cmd_stdout = fdopen(cmd_stdout_fd, "r")) == NULL || (cmd_stderr = fdopen(cmd_stderr_fd, "r")) == NULL || (cmd_setenv = fdopen(cmd_setenv_fd, "r")) == NULL) { log(LOG_ERR, "fdopen() failed, errno = %d", errno); return PAM_SYSTEM_ERR; } for (;;) { fd_set fds_read; int ret; FD_ZERO(&fds_read); FD_SET(cmd_stdout_fd, &fds_read); FD_SET(cmd_stderr_fd, &fds_read); FD_SET(cmd_setenv_fd, &fds_read); /* The Linux implementation of select() needs this to be * reinitialized at each iteration */ timeout.tv_sec = 0; timeout.tv_usec = 10000; /* 1/100 second */ ret = select(cmd_setenv_fd + 1, &fds_read, NULL, NULL, &timeout); if (ret == 0) continue; if (ret < 0) { log(LOG_ERR, "select() failed, errno = %d", errno); break; } ret = 0; if (FD_ISSET(cmd_stdout_fd, &fds_read)) ret |= read_command_stdout(pamh, options, cmd_stdout); if (FD_ISSET(cmd_stderr_fd, &fds_read)) ret |= read_command_stderr(pamh, options, cmd_stderr); if (FD_ISSET(cmd_setenv_fd, &fds_read)) ret |= read_command_setenv(pamh, options, cmd_setenv); /* Watch as we completely ignore ret! */ if (feof(cmd_stdout) && feof(cmd_stderr) && feof(cmd_setenv)) { break; } } if (fclose(cmd_stdout) || fclose(cmd_stderr) || fclose(cmd_setenv)) { log(LOG_ERR, "fclose() failed, errno = %d", errno); return PAM_SYSTEM_ERR; } return PAM_SUCCESS; } /* Main pam_exec routine. Returns a PAM error code. */ static int do_exec(pam_handle_t *pamh, struct exec_options *options, PamExecAction action) { int status = PAM_SUCCESS; sigset_t sigmask; sigset_t old_sigmask; int sigchld_blocked = 0; const char *login_user; const char *login_service; const char *login_tty; const char *login_ruser; const char *login_rhost; const char *login_authtok; const char *login_oldauthtok; struct passwd login_user_pw, *login_user_pw_ptr; size_t pw_buflen; char *pw_buf = NULL; int opened_stdin = 0; int opened_stdout = 0; int opened_stderr = 0; int opened_setenv = 0; int stdout_pipe[2]; int stderr_pipe[2]; int setenv_pipe[2]; pid_t pid; pid_t ret_pid; int cmd_status; int ret; /* This function has a postamble that must not be bypassed. We * temporarily disable the "return" keyword to ensure this. (The * forked child section will use _exit(), so we're good there.) */ #define return dont_return_use_goto_cleanup /* Temporarily block SIGCHLD, so that the application program can't * reap our child when the latter finishes (needed for gdm et al.) */ if (sigemptyset(&sigmask) || sigaddset(&sigmask, SIGCHLD) || sigprocmask(SIG_BLOCK, &sigmask, &old_sigmask)) { log(LOG_ERR, "Could not block SIGCHLD, errno = %d", errno); goto cleanup; } else sigchld_blocked = 1; /* All right, who is this all about? */ ret = pam_get_user(pamh, (const char **)&login_user, NULL); if (ret != PAM_SUCCESS) { #if defined(PAM_CONV_AGAIN) && defined(PAM_INCOMPLETE) if (ret == PAM_CONV_AGAIN) { status = PAM_INCOMPLETE; goto cleanup; } #endif log(LOG_ERR, "Cannot get login name: %s", pam_strerror(pamh, ret)); status = ret; goto cleanup; } if (options->debug) log(LOG_DEBUG, "pam_get_user() => \"%s\"", login_user); /* * * * Get some informative PAM items */ #define GET_PAM_ITEM(arg_item, arg_var, arg_errmsg) \ ret = pam_get_item(pamh, arg_item, (const void **)&arg_var); \ if (ret != PAM_SUCCESS) \ { \ log(LOG_ERR, arg_errmsg); \ status = PAM_SERVICE_ERR; \ goto cleanup; \ } \ if (arg_var == NULL) \ arg_var = ""; \ if (options->debug && \ arg_item != PAM_AUTHTOK && \ arg_item != PAM_OLDAUTHTOK) \ log(LOG_DEBUG, "pam_get_item(" #arg_item ") => \"%s\"", arg_var) GET_PAM_ITEM(PAM_SERVICE, login_service, "Cannot obtain service name"); GET_PAM_ITEM(PAM_TTY, login_tty, "Cannot determine login tty"); GET_PAM_ITEM(PAM_RUSER, login_ruser, "Cannot determine remote user"); GET_PAM_ITEM(PAM_RHOST, login_rhost, "Cannot determine remote host"); if (options->allow_authtok) { GET_PAM_ITEM(PAM_AUTHTOK, login_authtok, "Cannot obtain authentication token"); GET_PAM_ITEM(PAM_OLDAUTHTOK, login_oldauthtok, "Cannot obtain old authentication token"); } #undef GET_PAM_ITEM /* * * * Get passwd info for the user logging in */ pw_buflen = (size_t)sysconf(_SC_GETPW_R_SIZE_MAX); pw_buf = (char *)malloc(pw_buflen); if (pw_buf == NULL) { status = PAM_BUF_ERR; goto cleanup; } ret = getpwnam_r(login_user, &login_user_pw, pw_buf, pw_buflen, &login_user_pw_ptr); if (ret != 0 || login_user_pw_ptr != &login_user_pw) { log(LOG_ERR, "Cannot obtain passwd info for user \"%s\"", login_user); status = PAM_SYSTEM_ERR; goto cleanup; } /* * * * Set up file descriptors */ /* We don't want the pipe() calls below to use any of our four * reserved file descriptors. These may or may not be currently * open, depending on what the application program has done, so we * temporarily open whichever ones aren't (thereby keeping them * unavailable to pipe()). */ #define OPEN_IF_CLOSED(arg_fd, arg_flag) \ if (! is_open_fd(arg_fd)) \ { \ int null_fd = open("/dev/null", O_RDONLY); \ if (null_fd != arg_fd) \ dup2(null_fd, arg_fd); \ arg_flag = 1; \ if (options->debug) \ log(LOG_DEBUG, "fd %d was closed, now open (%d too)", arg_fd, null_fd); \ } \ (void)0 OPEN_IF_CLOSED(STDIN_FILENO, opened_stdin); OPEN_IF_CLOSED(STDOUT_FILENO, opened_stdout); OPEN_IF_CLOSED(STDERR_FILENO, opened_stderr); OPEN_IF_CLOSED(SETENV_FILENO, opened_setenv); #undef OPEN_IF_CLOSED if (pipe(stdout_pipe) || pipe(stderr_pipe) || pipe(setenv_pipe)) { log(LOG_ERR, "pipe() failed, errno = %d", errno); status = PAM_SYSTEM_ERR; goto cleanup; } if ((opened_stdin && close(STDIN_FILENO)) || (opened_stdout && close(STDOUT_FILENO)) || (opened_stderr && close(STDERR_FILENO)) || (opened_setenv && close(SETENV_FILENO))) { log(LOG_ERR, "close() failed! errno = %d", errno); status = PAM_SYSTEM_ERR; goto cleanup; } pid = fork(); if (pid < 0) { log(LOG_ERR, "fork() failed, errno = %d", errno); status = PAM_SYSTEM_ERR; goto cleanup; } else if (pid == 0) { /**** Child ****/ char **envlist; char **env; const char *action_name = ""; int fd; /* Take on our new identity */ if ((options->runas_gid != (gid_t)-1 && setregid(options->runas_gid, options->runas_gid)) || (options->runas_uid != (uid_t)-1 && setreuid(options->runas_uid, options->runas_uid))) { log(LOG_ERR, "Error changing uid/gid, errno = %d", errno); _exit(PAM_SYSTEM_ERR); } /* * * * Set up those environment variables we promised */ /* Current PAM environment */ envlist = pam_getenvlist(pamh); for (env = envlist; *env != NULL; env = &env[1]) { if (putenv(*env)) { log(LOG_ERR, "putenv() failed, errno = %d", errno); _exit(PAM_BUF_ERR); } } /* Free the array of pointers to [environment-variable * assignment] strings, but not the strings themselves, * as they have become one with the environment */ free(envlist); /* Standard PAM items */ if (setenv("PAM_USER", login_user, 1) || setenv("PAM_SERVICE", login_service, 1) || setenv("PAM_TTY", login_tty, 1) || setenv("PAM_RHOST", login_rhost, 1) || setenv("PAM_RUSER", login_ruser, 1) || (options->allow_authtok && (setenv("PAM_AUTHTOK", login_authtok, 1) || setenv("PAM_OLDAUTHTOK", login_oldauthtok, 1)))) { log(LOG_ERR, "setenv() failed, errno = %d", errno); _exit(PAM_SYSTEM_ERR); } /* Standard PAM flags */ #define SETENV_FLAG(arg_flag) \ if (options->flags & arg_flag) \ { \ if (setenv(#arg_flag, "true", 1) != 0) \ { \ log(LOG_ERR, "setenv() failed, errno = %d", errno); \ _exit(PAM_SYSTEM_ERR); \ } \ } \ (void)0 SETENV_FLAG(PAM_SILENT); SETENV_FLAG(PAM_DISALLOW_NULL_AUTHTOK); SETENV_FLAG(PAM_CHANGE_EXPIRED_AUTHTOK); SETENV_FLAG(PAM_PRELIM_CHECK); SETENV_FLAG(PAM_UPDATE_AUTHTOK); #undef SET_FLAG_VAR /* Password database information */ setenv("PW_NAME", login_user_pw.pw_name, 1); setenv("PW_GECOS", login_user_pw.pw_gecos, 1); setenv("PW_DIR", login_user_pw.pw_dir, 1); setenv("PW_SHELL", login_user_pw.pw_shell, 1); { char idstr[16]; sprintf(idstr, "%d", (int)login_user_pw.pw_uid); setenv("PW_UID", idstr, 1); sprintf(idstr, "%d", (int)login_user_pw.pw_gid); setenv("PW_GID", idstr, 1); } /* PAM-related symbolic exit codes */ #define SETENV_EXIT_CODE(arg_code) \ { \ char buf[8]; \ sprintf(buf, "%d", arg_code); \ if (setenv("EXIT_" #arg_code, buf, 1) != 0) \ { \ log(LOG_ERR, "setenv() failed, errno = %d", errno); \ _exit(PAM_SYSTEM_ERR); \ } \ } \ (void)0 SETENV_EXIT_CODE(PAM_ACCT_EXPIRED); SETENV_EXIT_CODE(PAM_AUTHINFO_UNAVAIL); SETENV_EXIT_CODE(PAM_AUTH_ERR); SETENV_EXIT_CODE(PAM_CRED_INSUFFICIENT); SETENV_EXIT_CODE(PAM_IGNORE); SETENV_EXIT_CODE(PAM_MAXTRIES); SETENV_EXIT_CODE(PAM_NEW_AUTHTOK_REQD); SETENV_EXIT_CODE(PAM_PERM_DENIED); SETENV_EXIT_CODE(PAM_SERVICE_ERR); SETENV_EXIT_CODE(PAM_SESSION_ERR); SETENV_EXIT_CODE(PAM_SUCCESS); SETENV_EXIT_CODE(PAM_USER_UNKNOWN); #undef SETENV_EXIT_CODE /* * * * Do the necessary POSIX-fu to get IPC working */ /* Close the read ends of these pipes */ if (close(stdout_pipe[0]) || close(stderr_pipe[0]) || close(setenv_pipe[0])) { log(LOG_ERR, "close() failed! errno = %d", errno); _exit(PAM_SYSTEM_ERR); } /* Close the existing standard filehandles, as we will be * substituting our own. These may or may not be open right * now, so we don't bother checking the return values. */ close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); close(SETENV_FILENO); if (dup2(stdout_pipe[1], STDOUT_FILENO) == -1 || dup2(stderr_pipe[1], STDERR_FILENO) == -1 || dup2(setenv_pipe[1], SETENV_FILENO) == -1) { log(LOG_ERR, "dup2() failed, errno = %d", errno); _exit(PAM_SYSTEM_ERR); } /* command = 0; fd--) { if (fd != stdout_pipe[1] && fd != stderr_pipe[1] && fd != setenv_pipe[1] && fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO && fd != SETENV_FILENO) { close(fd); } } /* * * * ...aaaand, action! */ #define ACTION_CASE(arg_action, arg_name) \ case arg_action: action_name = arg_name; break switch (action) { ACTION_CASE(PE_AUTHENTICATE, "auth"); /* ACTION_CASE(PE_SETCRED, "setcred"); */ ACTION_CASE(PE_ACCT_MGMT, "acctmgmt"); ACTION_CASE(PE_OPEN_SESSION, "sessopen"); ACTION_CASE(PE_CLOSE_SESSION, "sessclose"); ACTION_CASE(PE_CHAUTHTOK, "chauthtok"); } #undef ACTION_CASE setenv("ACTION", action_name, 1); if (options->debug) log(LOG_DEBUG, "exec: /bin/sh -c '%s' /bin/sh %s", options->command, action_name); execl("/bin/sh", "/bin/sh", "-c", options->command, "/bin/sh", action_name, (char *)NULL); /* the cast is required */ /* If we can't exec /bin/sh then the system is FUBAR */ log(LOG_ERR, "execl() failed, errno = %d", errno); _exit(PAM_SYSTEM_ERR); } /**** Parent ****/ /* Close the write ends of these pipes */ if (close(stdout_pipe[1]) || close(stderr_pipe[1]) || close(setenv_pipe[1])) { log(LOG_ERR, "close() failed! errno = %d", errno); status = PAM_SYSTEM_ERR; goto cleanup; } /* * * * * * * * * */ ret = read_command_output(pamh, options, stdout_pipe[0], stderr_pipe[0], setenv_pipe[0]); /* * * * * * * * * */ do { ret_pid = waitpid(pid, &cmd_status, 0); } while (ret_pid == (pid_t)-1 && errno == EINTR); if (ret_pid == -1 || ret_pid != pid) { log(LOG_ERR, "waitpid() failed, errno = %d", errno); status = PAM_SYSTEM_ERR; goto cleanup; } if (WIFEXITED(cmd_status)) { status = WEXITSTATUS(cmd_status); if (options->debug) log(LOG_DEBUG, "Command exited with status %d", status); } else if (WIFSIGNALED(cmd_status)) { int ts = WTERMSIG(cmd_status); const char *cd = ""; #ifdef WCOREDUMP if (WCOREDUMP(cmd_status)) cd = " (core dumped)"; #endif if (options->warnings) log(LOG_WARNING, "Command terminated on signal %d%s", ts, cd); status = PAM_SERVICE_ERR; } else { log(LOG_ERR, "Command exited with unknown status 0x%x", cmd_status); status = PAM_SERVICE_ERR; } /* * * * Clean up after ourselves */ cleanup: if (sigchld_blocked) { /* Restore previous signal mask */ sigprocmask(SIG_SETMASK, &old_sigmask, NULL); } if (pw_buf != NULL) free(pw_buf); /* Re-enable the "return" keyword */ #undef return return status; } /* end do_exec() */ /* Save ourselves some typing */ #define COMMON_ERROR_CODES() \ case PAM_SUCCESS: \ case PAM_SERVICE_ERR: \ case PAM_SYSTEM_ERR: \ case PAM_BUF_ERR: \ case PAM_CONV_ERR: \ case PAM_IGNORE: \ (void)0 /* * * * Authentication management */ PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) { struct exec_options options; int ret; ret = parse_options(&options, flags, argc, argv, PE_AUTHENTICATE); if (ret != PAM_SUCCESS) return ret; #ifdef PAM_FAIL_DELAY if (options.fail_delay > 0) pam_fail_delay(pamh, (unsigned int)options.fail_delay * 1000); #endif ret = do_exec(pamh, &options, PE_AUTHENTICATE); switch (ret) { COMMON_ERROR_CODES(); case PAM_AUTH_ERR: case PAM_CRED_INSUFFICIENT: case PAM_AUTHINFO_UNAVAIL: case PAM_USER_UNKNOWN: case PAM_MAXTRIES: return ret; } return PAM_AUTH_ERR; } PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { pamh = pamh; /* unused */ flags = flags; /* unused */ argc = argc; /* unused */ argv = argv; /* unused */ /* This module task is not well-served by an external program */ return PAM_IGNORE; } /* * * * Account management */ PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) { struct exec_options options; int ret; ret = parse_options(&options, flags, argc, argv, PE_ACCT_MGMT); if (ret != PAM_SUCCESS) return ret; ret = do_exec(pamh, &options, PE_ACCT_MGMT); switch (ret) { COMMON_ERROR_CODES(); case PAM_ACCT_EXPIRED: case PAM_AUTH_ERR: /* case PAM_AUTHTOKEN_REQD: */ case PAM_USER_UNKNOWN: case PAM_NEW_AUTHTOK_REQD: /* from Sun PAM */ case PAM_PERM_DENIED: /* from Sun PAM */ return ret; } return PAM_AUTH_ERR; } /* * * * Session management */ PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { struct exec_options options; int ret; ret = parse_options(&options, flags, argc, argv, PE_OPEN_SESSION); if (ret != PAM_SUCCESS) return ret; if (! options.on_open) return PAM_IGNORE; ret = do_exec(pamh, &options, PE_OPEN_SESSION); switch (ret) { COMMON_ERROR_CODES(); case PAM_SESSION_ERR: return ret; } return PAM_SESSION_ERR; } PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { struct exec_options options; int ret; ret = parse_options(&options, flags, argc, argv, PE_CLOSE_SESSION); if (ret != PAM_SUCCESS) return ret; if (! options.on_close) return PAM_IGNORE; ret = do_exec(pamh, &options, PE_CLOSE_SESSION); switch (ret) { COMMON_ERROR_CODES(); case PAM_SESSION_ERR: return ret; } return PAM_SESSION_ERR; } /* * * * Password management */ PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv) { struct exec_options options; int ret; ret = parse_options(&options, flags, argc, argv, PE_CHAUTHTOK); if (ret != PAM_SUCCESS) return ret; ret = do_exec(pamh, &options, PE_CHAUTHTOK); switch (ret) { COMMON_ERROR_CODES(); case PAM_AUTHTOK_ERR: case PAM_AUTHTOK_RECOVERY_ERR: case PAM_AUTHTOK_LOCK_BUSY: case PAM_AUTHTOK_DISABLE_AGING: case PAM_PERM_DENIED: case PAM_TRY_AGAIN: case PAM_USER_UNKNOWN: return ret; } return PAM_AUTHTOK_ERR; } #undef COMMON_ERROR_CODES #ifdef PAM_STATIC struct pam_module _pam_exec_modstruct = { "pam_exec", pam_sm_authenticate, pam_sm_setcred, pam_sm_acct_mgmt, pam_sm_open_session, pam_sm_close_session, pam_sm_chauthtok, }; #endif /* PAM_STATIC */ /* end pam_exec.c */