Skip site navigation (1)Skip section navigation (2)
Date:      Wed, 22 Mar 2017 13:16:05 +0000 (UTC)
From:      =?UTF-8?Q?Dag-Erling_Sm=c3=b8rgrav?= <des@FreeBSD.org>
To:        src-committers@freebsd.org, svn-src-all@freebsd.org, svn-src-head@freebsd.org
Subject:   svn commit: r315710 - head/lib/libpam/modules/pam_exec
Message-ID:  <201703221316.v2MDG5EU017903@repo.freebsd.org>

next in thread | raw e-mail | index | archive | help
Author: des
Date: Wed Mar 22 13:16:04 2017
New Revision: 315710
URL: https://svnweb.freebsd.org/changeset/base/315710

Log:
  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

Modified:
  head/lib/libpam/modules/pam_exec/pam_exec.8
  head/lib/libpam/modules/pam_exec/pam_exec.c

Modified: head/lib/libpam/modules/pam_exec/pam_exec.8
==============================================================================
--- head/lib/libpam/modules/pam_exec/pam_exec.8	Wed Mar 22 11:06:33 2017	(r315709)
+++ head/lib/libpam/modules/pam_exec/pam_exec.8	Wed Mar 22 13:16:04 2017	(r315710)
@@ -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 op
 .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.

Modified: head/lib/libpam/modules/pam_exec/pam_exec.c
==============================================================================
--- head/lib/libpam/modules/pam_exec/pam_exec.c	Wed Mar 22 11:06:33 2017	(r315709)
+++ head/lib/libpam/modules/pam_exec/pam_exec.c	Wed Mar 22 13:16:04 2017	(r315710)
@@ -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 *arg
 	 *   --:
 	 *     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 *arg
 }
 
 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



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?201703221316.v2MDG5EU017903>