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
This commit is contained in:
Dag-Erling Smørgrav 2017-03-22 13:16:04 +00:00
parent ea8e769e42
commit 3869fb7832
Notes: svn2git 2020-12-20 02:59:44 +00:00
svn path=/head/; revision=315710
2 changed files with 247 additions and 117 deletions

View File

@ -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.

View File

@ -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 <sys/types.h>
#include <sys/poll.h>
#include <sys/procdesc.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -48,24 +52,62 @@ __FBSDID("$FreeBSD$");
#include <security/pam_modules.h>
#include <security/openpam.h>
#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;
/* Add the PAM return values 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);
}
envlist[envlen] = NULL;
/*
* 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;
/* add the PAM error codes to the environment. */
if (options->return_prog_exit_status) {
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;
}
}
openpam_log(PAM_LOG_DEBUG, "envlen = %d extralen = %d envlist = %p",
envlen, extralen, envlist);
/* 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