From 3869fb783225f2417b2d420a36130c779441f652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dag-Erling=20Sm=C3=B8rgrav?= Date: Wed, 22 Mar 2017 13:16:04 +0000 Subject: Add options to capture stdout and / or stderr and pass the output on to the user. There is currently no buffering, so the result may be somewhat unpredictable if the conversation function adds a newline, like openpam_ttyconv() does. Clean up and simplify the environment handling code, which triggered an inexplicable bug on some systems. MFC after: 2 weeks --- lib/libpam/modules/pam_exec/pam_exec.8 | 17 +- lib/libpam/modules/pam_exec/pam_exec.c | 341 ++++++++++++++++++++++----------- 2 files changed, 244 insertions(+), 114 deletions(-) (limited to 'lib/libpam') diff --git a/lib/libpam/modules/pam_exec/pam_exec.8 b/lib/libpam/modules/pam_exec/pam_exec.8 index e2be511b0ceb..56a2e2da48b7 100644 --- a/lib/libpam/modules/pam_exec/pam_exec.8 +++ b/lib/libpam/modules/pam_exec/pam_exec.8 @@ -1,4 +1,5 @@ .\" Copyright (c) 2001,2003 Networks Associates Technology, Inc. +.\" Copyright (c) 2017 Dag-Erling Smørgrav .\" All rights reserved. .\" .\" Portions of this software were developed for the FreeBSD Project by @@ -32,7 +33,7 @@ .\" .\" $FreeBSD$ .\" -.Dd February 8, 2012 +.Dd March 22, 2017 .Dt PAM_EXEC 8 .Os .Sh NAME @@ -55,7 +56,19 @@ if the program name conflicts with an option name. .Pp The following options may be passed before the program and its arguments: -.Bl -tag -width ".Cm return_prog_exit_status" +.Bl -tag -width indent +.It Cm capture_stderr +Capture text printed by the program to its standard error stream and +pass it to the conversation function as error messages. +No attempt is made at buffering the text, so results may vary. +.It Cm capture_stdout +Capture text printed by the program to its standard output stream and +pass it to the conversation function as informational messages. +No attempt is made at buffering the text, so results may vary. +.It Cm debug +Ignored for compatibility reasons. +.It Cm no_warn +Ignored for compatibility reasons. .It Cm return_prog_exit_status Use the program exit status as the return code of the pam_sm_* function. It must be a valid return value for this function. diff --git a/lib/libpam/modules/pam_exec/pam_exec.c b/lib/libpam/modules/pam_exec/pam_exec.c index d43d181f31e5..297af81eaa5c 100644 --- a/lib/libpam/modules/pam_exec/pam_exec.c +++ b/lib/libpam/modules/pam_exec/pam_exec.c @@ -1,5 +1,6 @@ /*- * Copyright (c) 2001,2003 Networks Associates Technology, Inc. + * Copyright (c) 2017 Dag-Erling Smørgrav * All rights reserved. * * This software was developed for the FreeBSD Project by ThinkSec AS and @@ -36,9 +37,12 @@ __FBSDID("$FreeBSD$"); #include +#include +#include #include #include +#include #include #include #include @@ -48,24 +52,62 @@ __FBSDID("$FreeBSD$"); #include #include -#define ENV_ITEM(n) { (n), #n } +#define PAM_ITEM_ENV(n) { (n), #n } static struct { int item; const char *name; -} env_items[] = { - ENV_ITEM(PAM_SERVICE), - ENV_ITEM(PAM_USER), - ENV_ITEM(PAM_TTY), - ENV_ITEM(PAM_RHOST), - ENV_ITEM(PAM_RUSER), +} pam_item_env[] = { + PAM_ITEM_ENV(PAM_SERVICE), + PAM_ITEM_ENV(PAM_USER), + PAM_ITEM_ENV(PAM_TTY), + PAM_ITEM_ENV(PAM_RHOST), + PAM_ITEM_ENV(PAM_RUSER), }; +#define NUM_PAM_ITEM_ENV (sizeof(pam_item_env) / sizeof(pam_item_env[0])) + +#define PAM_ERR_ENV_X(str, num) str "=" #num +#define PAM_ERR_ENV(pam_err) PAM_ERR_ENV_X(#pam_err, pam_err) +static const char *pam_err_env[] = { + PAM_ERR_ENV(PAM_SUCCESS), + PAM_ERR_ENV(PAM_OPEN_ERR), + PAM_ERR_ENV(PAM_SYMBOL_ERR), + PAM_ERR_ENV(PAM_SERVICE_ERR), + PAM_ERR_ENV(PAM_SYSTEM_ERR), + PAM_ERR_ENV(PAM_BUF_ERR), + PAM_ERR_ENV(PAM_CONV_ERR), + PAM_ERR_ENV(PAM_PERM_DENIED), + PAM_ERR_ENV(PAM_MAXTRIES), + PAM_ERR_ENV(PAM_AUTH_ERR), + PAM_ERR_ENV(PAM_NEW_AUTHTOK_REQD), + PAM_ERR_ENV(PAM_CRED_INSUFFICIENT), + PAM_ERR_ENV(PAM_AUTHINFO_UNAVAIL), + PAM_ERR_ENV(PAM_USER_UNKNOWN), + PAM_ERR_ENV(PAM_CRED_UNAVAIL), + PAM_ERR_ENV(PAM_CRED_EXPIRED), + PAM_ERR_ENV(PAM_CRED_ERR), + PAM_ERR_ENV(PAM_ACCT_EXPIRED), + PAM_ERR_ENV(PAM_AUTHTOK_EXPIRED), + PAM_ERR_ENV(PAM_SESSION_ERR), + PAM_ERR_ENV(PAM_AUTHTOK_ERR), + PAM_ERR_ENV(PAM_AUTHTOK_RECOVERY_ERR), + PAM_ERR_ENV(PAM_AUTHTOK_LOCK_BUSY), + PAM_ERR_ENV(PAM_AUTHTOK_DISABLE_AGING), + PAM_ERR_ENV(PAM_NO_MODULE_DATA), + PAM_ERR_ENV(PAM_IGNORE), + PAM_ERR_ENV(PAM_ABORT), + PAM_ERR_ENV(PAM_TRY_AGAIN), + PAM_ERR_ENV(PAM_MODULE_UNKNOWN), + PAM_ERR_ENV(PAM_DOMAIN_UNKNOWN), + PAM_ERR_ENV(PAM_NUM_ERR), +}; +#define NUM_PAM_ERR_ENV (sizeof(pam_err_env) / sizeof(pam_err_env[0])) struct pe_opts { int return_prog_exit_status; + int capture_stdout; + int capture_stderr; }; -#define PAM_RV_COUNT 24 - static int parse_options(const char *func, int *argc, const char **argv[], struct pe_opts *options) @@ -79,22 +121,27 @@ parse_options(const char *func, int *argc, const char **argv[], * --: * stop options parsing; what follows is the command to execute */ - options->return_prog_exit_status = 0; + memset(options, 0, sizeof(*options)); for (i = 0; i < *argc; ++i) { - if (strcmp((*argv)[i], "return_prog_exit_status") == 0) { - openpam_log(PAM_LOG_DEBUG, - "%s: Option \"return_prog_exit_status\" enabled", - func); + if (strcmp((*argv)[i], "debug") == 0 || + strcmp((*argv)[i], "no_warn") == 0) { + /* ignore */ + } else if (strcmp((*argv)[i], "capture_stdout") == 0) { + options->capture_stdout = 1; + } else if (strcmp((*argv)[i], "capture_stderr") == 0) { + options->capture_stderr = 1; + } else if (strcmp((*argv)[i], "return_prog_exit_status") == 0) { options->return_prog_exit_status = 1; } else { if (strcmp((*argv)[i], "--") == 0) { (*argc)--; (*argv)++; } - break; } + openpam_log(PAM_LOG_DEBUG, "%s: option \"%s\" enabled", + func, (*argv)[i]); } (*argc) -= i; @@ -104,159 +151,229 @@ parse_options(const char *func, int *argc, const char **argv[], } static int -_pam_exec(pam_handle_t *pamh __unused, +_pam_exec(pam_handle_t *pamh, const char *func, int flags __unused, int argc, const char *argv[], struct pe_opts *options) { - int envlen, i, nitems, pam_err, status; - int nitems_rv; - char **envlist, **tmp, *envstr; - volatile int childerr; + char buf[PAM_MAX_MSG_SIZE]; + struct pollfd pfd[3]; + const void *item; + char **envlist, *envstr, *resp, **tmp; + ssize_t rlen; + int envlen, extralen, i; + int pam_err, serrno, status; + int chout[2], cherr[2], pd; + nfds_t nfds; pid_t pid; - /* - * XXX For additional credit, divert child's stdin/stdout/stderr - * to the conversation function. - */ + pd = -1; + pid = 0; + chout[0] = chout[1] = cherr[0] = cherr[1] = -1; + envlist = NULL; + +#define OUT(ret) do { pam_err = (ret); goto out; } while (0) /* Check there's a program name left after parsing options. */ if (argc < 1) { openpam_log(PAM_LOG_ERROR, "%s: No program specified: aborting", func); - return (PAM_SERVICE_ERR); + OUT(PAM_SERVICE_ERR); } /* - * Set up the child's environment list. It consists of the PAM - * environment, plus a few hand-picked PAM items, the pam_sm_* - * function name calling it and, if return_prog_exit_status is - * set, the valid return codes numerical values. + * Set up the child's environment list. It consists of the PAM + * environment, a few hand-picked PAM items, the name of the + * service function, and if return_prog_exit_status is set, the + * numerical values of all PAM error codes. */ + + /* compute the final size of the environment. */ envlist = pam_getenvlist(pamh); for (envlen = 0; envlist[envlen] != NULL; ++envlen) /* nothing */ ; - nitems = sizeof(env_items) / sizeof(*env_items); - /* Count PAM return values put in the environment. */ - nitems_rv = options->return_prog_exit_status ? PAM_RV_COUNT : 0; - tmp = realloc(envlist, (envlen + nitems + 1 + nitems_rv + 1) * - sizeof(*envlist)); - if (tmp == NULL) { - openpam_free_envlist(envlist); - return (PAM_BUF_ERR); - } + extralen = NUM_PAM_ITEM_ENV + 1; + if (options->return_prog_exit_status) + extralen += NUM_PAM_ERR_ENV; + tmp = reallocarray(envlist, envlen + extralen + 1, sizeof(*envlist)); + openpam_log(PAM_LOG_DEBUG, "envlen = %d extralen = %d tmp = %p", + envlen, extralen, tmp); + if (tmp == NULL) + OUT(PAM_BUF_ERR); envlist = tmp; - for (i = 0; i < nitems; ++i) { - const void *item; + extralen += envlen; - pam_err = pam_get_item(pamh, env_items[i].item, &item); + /* copy selected PAM items to the environment */ + for (i = 0; i < NUM_PAM_ITEM_ENV; ++i) { + pam_err = pam_get_item(pamh, pam_item_env[i].item, &item); if (pam_err != PAM_SUCCESS || item == NULL) continue; - asprintf(&envstr, "%s=%s", env_items[i].name, - (const char *)item); - if (envstr == NULL) { - openpam_free_envlist(envlist); - return (PAM_BUF_ERR); - } + if (asprintf(&envstr, "%s=%s", pam_item_env[i].name, item) < 0) + OUT(PAM_BUF_ERR); envlist[envlen++] = envstr; envlist[envlen] = NULL; + openpam_log(PAM_LOG_DEBUG, "setenv %s", envstr); } - /* Add the pam_sm_* function name to the environment. */ - asprintf(&envstr, "PAM_SM_FUNC=%s", func); - if (envstr == NULL) { - openpam_free_envlist(envlist); - return (PAM_BUF_ERR); - } + /* add the name of the service function to the environment */ + if (asprintf(&envstr, "PAM_SM_FUNC=%s", func) < 0) + OUT(PAM_BUF_ERR); envlist[envlen++] = envstr; + envlist[envlen] = NULL; - /* Add the PAM return values to the environment. */ + /* add the PAM error codes to the environment. */ if (options->return_prog_exit_status) { -#define ADD_PAM_RV_TO_ENV(name) \ - asprintf(&envstr, #name "=%d", name); \ - if (envstr == NULL) { \ - openpam_free_envlist(envlist); \ - return (PAM_BUF_ERR); \ - } \ - envlist[envlen++] = envstr - /* - * CAUTION: When adding/removing an item in the list - * below, be sure to update the value of PAM_RV_COUNT. - */ - ADD_PAM_RV_TO_ENV(PAM_ABORT); - ADD_PAM_RV_TO_ENV(PAM_ACCT_EXPIRED); - ADD_PAM_RV_TO_ENV(PAM_AUTHINFO_UNAVAIL); - ADD_PAM_RV_TO_ENV(PAM_AUTHTOK_DISABLE_AGING); - ADD_PAM_RV_TO_ENV(PAM_AUTHTOK_ERR); - ADD_PAM_RV_TO_ENV(PAM_AUTHTOK_LOCK_BUSY); - ADD_PAM_RV_TO_ENV(PAM_AUTHTOK_RECOVERY_ERR); - ADD_PAM_RV_TO_ENV(PAM_AUTH_ERR); - ADD_PAM_RV_TO_ENV(PAM_BUF_ERR); - ADD_PAM_RV_TO_ENV(PAM_CONV_ERR); - ADD_PAM_RV_TO_ENV(PAM_CRED_ERR); - ADD_PAM_RV_TO_ENV(PAM_CRED_EXPIRED); - ADD_PAM_RV_TO_ENV(PAM_CRED_INSUFFICIENT); - ADD_PAM_RV_TO_ENV(PAM_CRED_UNAVAIL); - ADD_PAM_RV_TO_ENV(PAM_IGNORE); - ADD_PAM_RV_TO_ENV(PAM_MAXTRIES); - ADD_PAM_RV_TO_ENV(PAM_NEW_AUTHTOK_REQD); - ADD_PAM_RV_TO_ENV(PAM_PERM_DENIED); - ADD_PAM_RV_TO_ENV(PAM_SERVICE_ERR); - ADD_PAM_RV_TO_ENV(PAM_SESSION_ERR); - ADD_PAM_RV_TO_ENV(PAM_SUCCESS); - ADD_PAM_RV_TO_ENV(PAM_SYSTEM_ERR); - ADD_PAM_RV_TO_ENV(PAM_TRY_AGAIN); - ADD_PAM_RV_TO_ENV(PAM_USER_UNKNOWN); + for (i = 0; i < (int)NUM_PAM_ERR_ENV; ++i) { + if ((envstr = strdup(pam_err_env[i])) == NULL) + OUT(PAM_BUF_ERR); + envlist[envlen++] = envstr; + envlist[envlen] = NULL; + } } - envlist[envlen] = NULL; + openpam_log(PAM_LOG_DEBUG, "envlen = %d extralen = %d envlist = %p", + envlen, extralen, envlist); - /* - * Fork and run the command. By using vfork() instead of fork(), - * we can distinguish between an execve() failure and a non-zero - * exit status from the command. - */ - childerr = 0; - if ((pid = vfork()) == 0) { - execve(argv[0], (char * const *)argv, (char * const *)envlist); - childerr = errno; + /* set up pipes if capture was requested */ + if (options->capture_stdout) { + if (pipe(chout) != 0) { + openpam_log(PAM_LOG_ERROR, "%s: pipe(): %m", func); + OUT(PAM_SYSTEM_ERR); + } + if (fcntl(chout[0], F_SETFL, O_NONBLOCK) != 0) { + openpam_log(PAM_LOG_ERROR, "%s: fcntl(): %m", func); + OUT(PAM_SYSTEM_ERR); + } + } else { + if ((chout[1] = open("/dev/null", O_RDWR)) < 0) { + openpam_log(PAM_LOG_ERROR, "%s: /dev/null: %m", func); + OUT(PAM_SYSTEM_ERR); + } + } + if (options->capture_stderr) { + if (pipe(cherr) != 0) { + openpam_log(PAM_LOG_ERROR, "%s: pipe(): %m", func); + OUT(PAM_SYSTEM_ERR); + } + if (fcntl(cherr[0], F_SETFL, O_NONBLOCK) != 0) { + openpam_log(PAM_LOG_ERROR, "%s: fcntl(): %m", func); + OUT(PAM_SYSTEM_ERR); + } + } else { + if ((cherr[1] = open("/dev/null", O_RDWR)) < 0) { + openpam_log(PAM_LOG_ERROR, "%s: /dev/null: %m", func); + OUT(PAM_SYSTEM_ERR); + } + } + + if ((pid = pdfork(&pd, 0)) == 0) { + /* child */ + if ((chout[0] >= 0 && close(chout[0]) != 0) || + (cherr[0] >= 0 && close(cherr[0]) != 0)) { + openpam_log(PAM_LOG_ERROR, "%s: close(): %m", func); + } else if (dup2(chout[1], STDOUT_FILENO) != STDOUT_FILENO || + dup2(cherr[1], STDERR_FILENO) != STDERR_FILENO) { + openpam_log(PAM_LOG_ERROR, "%s: dup2(): %m", func); + } else { + execve(argv[0], (char * const *)argv, + (char * const *)envlist); + openpam_log(PAM_LOG_ERROR, "%s: execve(%s): %m", + func, argv[0]); + } _exit(1); } - openpam_free_envlist(envlist); + /* parent */ if (pid == -1) { - openpam_log(PAM_LOG_ERROR, "%s: vfork(): %m", func); - return (PAM_SYSTEM_ERR); + openpam_log(PAM_LOG_ERROR, "%s: pdfork(): %m", func); + OUT(PAM_SYSTEM_ERR); + } + /* use poll() to watch the process and stdout / stderr */ + if (chout[1] >= 0) + close(chout[1]); + if (cherr[1] >= 0) + close(cherr[1]); + memset(pfd, 0, sizeof pfd); + pfd[0].fd = pd; + pfd[0].events = POLLHUP; + nfds = 1; + if (options->capture_stdout) { + pfd[nfds].fd = chout[0]; + pfd[nfds].events = POLLIN|POLLERR|POLLHUP; + nfds++; + } + if (options->capture_stderr) { + pfd[nfds].fd = cherr[0]; + pfd[nfds].events = POLLIN|POLLERR|POLLHUP; + nfds++; } + + /* loop until the process exits */ + do { + if (poll(pfd, nfds, INFTIM) < 0) { + openpam_log(PAM_LOG_ERROR, "%s: poll(): %m", func); + OUT(PAM_SYSTEM_ERR); + } + for (i = 1; i < nfds; ++i) { + if ((pfd[i].revents & POLLIN) == 0) + continue; + if ((rlen = read(pfd[i].fd, buf, sizeof(buf) - 1)) < 0) { + openpam_log(PAM_LOG_ERROR, "%s: read(): %m", + func); + OUT(PAM_SYSTEM_ERR); + } else if (rlen == 0) { + continue; + } + buf[rlen] = '\0'; + (void)pam_prompt(pamh, pfd[i].fd == chout[0] ? + PAM_TEXT_INFO : PAM_ERROR_MSG, &resp, "%s", buf); + } + } while (pfd[0].revents == 0); + + /* the child process has exited */ while (waitpid(pid, &status, 0) == -1) { if (errno == EINTR) continue; openpam_log(PAM_LOG_ERROR, "%s: waitpid(): %m", func); - return (PAM_SYSTEM_ERR); - } - if (childerr != 0) { - openpam_log(PAM_LOG_ERROR, "%s: execve(): %m", func); - return (PAM_SYSTEM_ERR); + OUT(PAM_SYSTEM_ERR); } + + /* check exit code */ if (WIFSIGNALED(status)) { openpam_log(PAM_LOG_ERROR, "%s: %s caught signal %d%s", func, argv[0], WTERMSIG(status), WCOREDUMP(status) ? " (core dumped)" : ""); - return (PAM_SERVICE_ERR); + OUT(PAM_SERVICE_ERR); } if (!WIFEXITED(status)) { openpam_log(PAM_LOG_ERROR, "%s: unknown status 0x%x", func, status); - return (PAM_SERVICE_ERR); + OUT(PAM_SERVICE_ERR); } if (options->return_prog_exit_status) { openpam_log(PAM_LOG_DEBUG, "%s: Use program exit status as return value: %d", func, WEXITSTATUS(status)); - return (WEXITSTATUS(status)); + OUT(WEXITSTATUS(status)); } else { - return (WEXITSTATUS(status) == 0 ? - PAM_SUCCESS : PAM_PERM_DENIED); + OUT(WEXITSTATUS(status) == 0 ? PAM_SUCCESS : PAM_PERM_DENIED); } + /* unreachable */ +out: + serrno = errno; + if (pd >= 0) + close(pd); + if (chout[0] >= 0) + close(chout[0]); + if (chout[1] >= 0) + close(chout[1]); + if (cherr[0] >= 0) + close(cherr[0]); + if (cherr[0] >= 0) + close(cherr[1]); + if (envlist != NULL) + openpam_free_envlist(envlist); + errno = serrno; + return (pam_err); } PAM_EXTERN int -- cgit v1.2.3