/*
 * Originally based on an implementation of `su' by
 *
 *     Peter Orbaek  <poe@daimi.aau.dk>
 *
 * obtained circa 1997 from ftp://ftp.daimi.aau.dk/pub/linux/poe/
 *
 * Rewritten for Linux-PAM by Andrew G. Morgan <morgan@linux.kernel.org>
 * Modified by Andrey V. Savochkin <saw@msu.ru>
 * Modified for use with libcap by Andrew G. Morgan <morgan@kernel.org>
 */

/* #define PAM_DEBUG */

#include <sys/prctl.h>

/* non-root user of convenience to block signals */
#define TEMP_UID                  1

#ifndef PAM_APP_NAME
#define PAM_APP_NAME              "su"
#endif /* ndef PAM_APP_NAME */

#define DEFAULT_HOME              "/"
#define DEFAULT_SHELL             "/bin/bash"
#define SLEEP_TO_KILL_CHILDREN    3  /* seconds to wait after SIGTERM before
					SIGKILL */
#define SU_FAIL_DELAY     2000000    /* usec on authentication failure */

#define RHOST_UNKNOWN_NAME        ""     /* perhaps "[from.where?]" */
#define DEVICE_FILE_PREFIX        "/dev/"
#define WTMP_LOCK_TIMEOUT         3      /* in seconds */

#ifndef UT_IDSIZE
#define UT_IDSIZE 4            /* XXX - this is sizeof(struct utmp.ut_id) */
#endif

#include <stdlib.h>
#include <signal.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <pwd.h>
#include <grp.h>
#include <string.h>
#include <syslog.h>
#include <errno.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/wait.h>
#include <utmp.h>
#include <ctype.h>
#include <stdarg.h>
#include <netdb.h>
#include <unistd.h>

#include <security/pam_appl.h>
#include <security/pam_misc.h>
#include <sys/capability.h>

#include <security/_pam_macros.h>

/* -------------------------------------------- */
/* ------ declarations ------------------------ */
/* -------------------------------------------- */

extern char **environ;
static pam_handle_t *pamh = NULL;
static int state;

static int wait_for_child_caught=0;
static int need_job_control=0;
static int is_terminal = 0;
static struct termios stored_mode;        /* initial terminal mode settings */
static uid_t terminal_uid = (uid_t) -1;
static uid_t invoked_uid = (uid_t) -1;

/* -------------------------------------------- */
/* ------ some local (static) functions ------- */
/* -------------------------------------------- */

/*
 * We will attempt to transcribe the following env variables
 * independent of whether we keep the whole environment. Others will
 * be set elsewhere: either in modules; or after the identity of the
 * user is known.
 */

static const char *posix_env[] = {
    "LANG",
    "LC_COLLATE",
    "LC_CTYPE",
    "LC_MONETARY",
    "LC_NUMERIC",
    "TZ",
    NULL
};

/*
 * make_environment transcribes a selection of environment variables
 * from the invoking user.
 */
static int make_environment(pam_handle_t *pamh, int keep_env)
{
    const char *tmpe;
    int i;
    int retval;

    if (keep_env) {
	/* preserve the original environment */
	return pam_misc_paste_env(pamh, (const char * const *)environ);
    }

    /* we always transcribe some variables anyway */
    tmpe = getenv("TERM");
    if (tmpe == NULL) {
	tmpe = "dumb";
    }
    retval = pam_misc_setenv(pamh, "TERM", tmpe, 0);
    if (retval == PAM_SUCCESS) {
	retval = pam_misc_setenv(pamh, "PATH", "/bin:/usr/bin", 0);
    }
    if (retval != PAM_SUCCESS) {
	tmpe = NULL;
	D(("error setting environment variables"));
	return retval;
    }

    /* also propagate the POSIX specific ones */
    for (i=0; retval == PAM_SUCCESS && posix_env[i]; ++i) {
	tmpe = getenv(posix_env[i]);
	if (tmpe != NULL) {
	    retval = pam_misc_setenv(pamh, posix_env[i], tmpe, 0);
	}
    }
    tmpe = NULL;

    return retval;
}

/*
 * checkfds ensures that stdout and stderr filedescriptors are
 * defined. If all else fails, it directs them to /dev/null.
 */
static void checkfds(void)
{
    struct stat st;
    int fd;

    if (fstat(1, &st) == -1) {
        fd = open("/dev/null", O_WRONLY);
        if (fd == -1) exit(1);
        if (fd != 1) {
            if (dup2(fd, 1) == -1) exit(1);
            if (close(fd) == -1) exit(1);
        }
    }
    if (fstat(2, &st) == -1) {
        fd = open("/dev/null", O_WRONLY);
        if (fd == -1) exit(1);
        if (fd != 2) {
            if (dup2(fd, 2) == -1) exit(1);
            if (close(fd) == -1) exit(1);
        }
    }
}

/*
 * store_terminal_modes captures the current state of the input
 * terminal. Calling this at the start of the program, we ensure we
 * can restore these default settings when su exits.
 */
static void store_terminal_modes(void)
{
    if (isatty(STDIN_FILENO)) {
	is_terminal = 1;
	if (tcgetattr(STDIN_FILENO, &stored_mode) != 0) {
	    fprintf(stderr, PAM_APP_NAME ": couldn't copy terminal mode");
	    exit(1);
	}
	return;
    }
    fprintf(stderr, PAM_APP_NAME ": must be run from a terminal\n");
    exit(1);
}

/*
 * restore_terminal_modes resets the terminal to the state it was in
 * when the program started.
 *
 * Returns:
 *   0     ok
 *   1     error
 */
static int restore_terminal_modes(void)
{
    if (is_terminal && tcsetattr(STDIN_FILENO, TCSAFLUSH, &stored_mode) != 0) {
	fprintf(stderr, PAM_APP_NAME ": cannot restore terminal mode: %s\n",
		strerror(errno));
	return 1;
    } else {
	return 0;
    }
}

/* ------ unexpected signals ------------------ */

struct sigaction old_int_act, old_quit_act, old_tstp_act, old_pipe_act;

/*
 * disable_terminal_signals attempts to make the process resistant to
 * being stopped - it helps ensure that the PAM stack can complete
 * session and auth failure logging etc.
 */
static void disable_terminal_signals(void)
{
    /*
     * Protect the process from dangerous terminal signals.
     * The protection is implemented via sigaction() because
     * the signals are sent regardless of the process' uid.
     */
    struct sigaction act;

    act.sa_handler = SIG_IGN;  /* ignore the signal */
    sigemptyset(&act.sa_mask); /* no signal blocking on handler
				  call needed */
    act.sa_flags = SA_RESTART; /* do not reset after first signal
				  arriving, restart interrupted
				  system calls if possible */
    sigaction(SIGINT, &act, &old_int_act);
    sigaction(SIGQUIT, &act, &old_quit_act);
    /*
     * Ignore SIGTSTP signals. Why? attacker could otherwise stop
     * a process and a. kill it, or b. wait for the system to
     * shutdown - either way, nothing appears in syslogs.
     */
    sigaction(SIGTSTP, &act, &old_tstp_act);
    /*
     * Ignore SIGPIPE. The parent `su' process may print something
     * on stderr. Killing of the process would be undesired.
     */
    sigaction(SIGPIPE, &act, &old_pipe_act);
}

static void enable_terminal_signals(void)
{
    sigaction(SIGINT, &old_int_act, NULL);
    sigaction(SIGQUIT, &old_quit_act, NULL);
    sigaction(SIGTSTP, &old_tstp_act, NULL);
    sigaction(SIGPIPE, &old_pipe_act, NULL);
}

/* ------ terminal ownership ------------------ */

/*
 * change_terminal_owner changes the ownership of STDIN if needed.
 * Returns:
 *   0     ok,
 *  -1     fatal error (continuing is impossible),
 *   1     non-fatal error.
 * In the case of an error "err_descr" is set to the error message
 * and "callname" to the name of the failed call.
 */
static int change_terminal_owner(uid_t uid, int is_login,
				 const char **callname, const char **err_descr)
{
    /* determine who owns the terminal line */
    if (is_terminal && is_login) {
	struct stat stat_buf;
	cap_t current, working;
	int status;
	cap_value_t cchown = CAP_CHOWN;

	if (fstat(STDIN_FILENO, &stat_buf) != 0) {
            *callname = "fstat to STDIN";
	    *err_descr = strerror(errno);
	    return -1;
	}

	current = cap_get_proc();
	working = cap_dup(current);
	cap_set_flag(working, CAP_EFFECTIVE, 1, &cchown, CAP_SET);
	status = cap_set_proc(working);
	cap_free(working);

	if (status != 0) {
	    *callname = "capset CHOWN";
	} else if ((status = fchown(STDIN_FILENO, uid, -1)) != 0) {
	    *callname = "fchown of STDIN";
	} else {
	    cap_set_proc(current);
	}
	cap_free(current);

	if (status != 0) {
	    *err_descr = strerror(errno);
	    return 1;
	}

	terminal_uid = stat_buf.st_uid;
    }
    return 0;
}

/*
 * restore_terminal_owner changes the terminal owner back to the value
 * it had when su was started.
 */
static void restore_terminal_owner(void)
{
    if (terminal_uid != (uid_t) -1) {
	cap_t current, working;
	int status;
	cap_value_t cchown = CAP_CHOWN;

	current = cap_get_proc();
	working = cap_dup(current);
	cap_set_flag(working, CAP_EFFECTIVE, 1, &cchown, CAP_SET);
	status = cap_set_proc(working);
	cap_free(working);

	if (status == 0) {
	    status = fchown(STDIN_FILENO, terminal_uid, -1);
	    cap_set_proc(current);
	}
	cap_free(current);

        if (status != 0) {
            openlog(PAM_APP_NAME, LOG_CONS|LOG_PERROR|LOG_PID, LOG_AUTHPRIV);
	    syslog(LOG_ALERT, "Terminal owner hasn\'t been restored: %s",
		   strerror(errno));
	    closelog();
        }
        terminal_uid = (uid_t) -1;
    }
}

/*
 * make_process_unkillable changes the uid of the process. TEMP_UID is
 * used for this temporary state.
 *
 * Returns:
 *   0     ok,
 *  -1     fatal error (continue of the work is impossible),
 *   1     non-fatal error.
 * In the case of an error "err_descr" is set to the error message
 * and "callname" to the name of the failed call.
 */
int make_process_unkillable(const char **callname, const char **err_descr)
{
    invoked_uid = getuid();
    if (invoked_uid == TEMP_UID) {
	/* no change needed */
	return 0;
    }

    if (cap_setuid(TEMP_UID) != 0) {
        *callname = "setuid";
	*err_descr = strerror(errno);
	return -1;
    }
    return 0;
}

/*
 * make_process_killable restores the invoking uid to the current
 * process.
 */
void make_process_killable()
{
    (void) cap_setuid(invoked_uid);
}

/* ------ command line parser ----------------- */

void usage(int exit_val)
{
    fprintf(stderr,"usage: su [-] [-h] [-c \"command\"] [username]\n");
    exit(exit_val);
}

/*
 * parse_command_line extracts the options from the command line
 * arguments.
 */
void parse_command_line(int argc, char *argv[],
			int *is_login, const char **user, const char **command)
{
    int username_present, command_present;

    *is_login = 0;
    *user = NULL;
    *command = NULL;
    username_present = command_present = 0;

    while ( --argc > 0 ) {
	const char *token;

	token = *++argv;
	if (*token == '-') {
	    switch (*++token) {
	    case '\0':             /* su as a login shell for the user */
		if (*is_login)
		    usage(1);
		*is_login = 1;
		break;
	    case 'c':
		if (command_present) {
		    usage(1);
		} else {               /* indicate we are running commands */
		    if (*++token != '\0') {
			command_present = 1;
			*command = token;
		    } else if (--argc > 0) {
			command_present = 1;
			*command = *++argv;
		    } else
			usage(1);
		}
		break;
	    case 'h':
		usage(0);
	    default:
		usage(1);
	    }
	} else {                       /* must be username */
	    if (username_present) {
		usage(1);
	    }
	    username_present = 1;
	    *user = *argv;
	}
    }

    if (!username_present) {
	fprintf(stderr, PAM_APP_NAME ": requires a username\n");
	usage(1);
    }
}

/*
 * This following contains code that waits for a child process to die.
 * It also chooses to intercept a couple of signals that it will
 * kindly pass on a SIGTERM to the child ;^). Waiting again for the
 * child to exit. If the child resists dying, it will SIGKILL it!
 */

static void wait_for_child_catch_sig(int ignore)
{
    wait_for_child_caught = 1;
}

static void prepare_for_job_control(int need_it)
{
    sigset_t ourset;

    (void) sigfillset(&ourset);
    if (sigprocmask(SIG_BLOCK, &ourset, NULL) != 0) {
	fprintf(stderr,"[trouble blocking signals]\n");
	wait_for_child_caught = 1;
	return;
    }
    need_job_control = need_it;
}

int wait_for_child(pid_t child)
{
    int retval, status, exit_code;
    sigset_t ourset;

    exit_code = -1; /* no exit code yet, exit codes could be from 0 to 255 */
    if (child == -1) {
	return exit_code;
    }

    /*
     * set up signal handling
     */

    if (!wait_for_child_caught) {
	struct sigaction action, defaction;

	action.sa_handler = wait_for_child_catch_sig;
	sigemptyset(&action.sa_mask);
	action.sa_flags = 0;

	defaction.sa_handler = SIG_DFL;
	sigemptyset(&defaction.sa_mask);
	defaction.sa_flags = 0;

	sigemptyset(&ourset);

	if (   sigaddset(&ourset, SIGTERM)
	    || sigaction(SIGTERM, &action, NULL)
	    || sigaddset(&ourset, SIGHUP)
	    || sigaction(SIGHUP, &action, NULL)
	    || sigaddset(&ourset, SIGALRM)          /* required by sleep(3) */
            || (need_job_control && sigaddset(&ourset, SIGTSTP))
            || (need_job_control && sigaction(SIGTSTP, &defaction, NULL))
            || (need_job_control && sigaddset(&ourset, SIGTTIN))
            || (need_job_control && sigaction(SIGTTIN, &defaction, NULL))
            || (need_job_control && sigaddset(&ourset, SIGTTOU))
            || (need_job_control && sigaction(SIGTTOU, &defaction, NULL))
	    || (need_job_control && sigaddset(&ourset, SIGCONT))
            || (need_job_control && sigaction(SIGCONT, &defaction, NULL))
	    || sigprocmask(SIG_UNBLOCK, &ourset, NULL)
	    ) {
	    fprintf(stderr,"[trouble setting signal intercept]\n");
	    wait_for_child_caught = 1;
	}

	/* application should be ready for receiving a SIGTERM/HUP now */
    }

    /*
     * This code waits for the process to actually die. If it stops,
     * then the parent attempts to mimic the behavior of the
     * child.. There is a slight bug in the code when the 'su'd user
     * attempts to restart the child independently of the parent --
     * the child dies.
     */
    while (!wait_for_child_caught) {
        /* parent waits for child */
	if ((retval = waitpid(child, &status, 0)) <= 0) {
            if (errno == EINTR) {
                continue;             /* recovering from a 'fg' */
	    }
            fprintf(stderr, "[error waiting child: %s]\n", strerror(errno));
            /*
             * Break the loop keeping exit_code undefined.
             * Do we have a chance for a successful wait() call
             * after kill()? (SAW)
             */
            wait_for_child_caught = 1;
            break;
        } else {
	    /* the child is terminated via exit() or a fatal signal */
	    if (WIFEXITED(status)) {
		exit_code = WEXITSTATUS(status);
	    } else {
		exit_code = 1;
	    }
	    break;
	}
    }

    if (wait_for_child_caught) {
	fprintf(stderr,"\nKilling shell...");
	kill(child, SIGTERM);
    }

    /*
     * do we need to wait for the child to catch up?
     */
    if (wait_for_child_caught) {
	sleep(SLEEP_TO_KILL_CHILDREN);
	kill(child, SIGKILL);
	fprintf(stderr, "killed\n");
    }

    /*
     * collect the zombie the shell was killed by ourself
     */
    if (exit_code == -1) {
	do {
	    retval = waitpid(child, &status, 0);
	} while (retval == -1 && errno == EINTR);
	if (retval == -1) {
	    fprintf(stderr, PAM_APP_NAME ": the final wait failed: %s\n",
		    strerror(errno));
	}
	if (WIFEXITED(status)) {
	    exit_code = WEXITSTATUS(status);
	} else {
	    exit_code = 1;
	}
    }

    return exit_code;
}


/*
 * Next some code that parses the spawned shell command line.
 */

static char * const *build_shell_args(const char *pw_shell, int login,
				      const char *command)
{
    int use_default = 1;  /* flag to signal we should use the default shell */
    const char **args=NULL;             /* array of PATH+ARGS+NULL pointers */

    D(("called."));
    if (login) {
        command = NULL;                 /* command always ignored for login */
    }

    if (pw_shell && *pw_shell != '\0') {
        char *line;
        const char *tmp, *tmpb=NULL;
        int arg_no=0,i;

        /* first find the number of arguments */
        D(("non-null shell"));
        for (tmp=pw_shell; *tmp; ++arg_no) {

            /* skip leading spaces */
            while (isspace(*tmp))
                ++tmp;

            if (tmpb == NULL)               /* mark beginning token */
                tmpb = tmp;
            if (*tmp == '\0')               /* end of line with no token */
                break;

            /* skip token */
            while (*tmp && !isspace(*tmp))
                ++tmp;
        }

        /*
         * We disallow shells:
         *    - without a full specified path;
         *    - when we are not logging in and the #args != 1
         *                                         (unlikely a simple shell)
         */

        D(("shell so far = %s, arg_no = %d", tmpb, arg_no));
        if (tmpb != NULL && tmpb[0] == '/'    /* something (full path) */
            && ( login || arg_no == 1 )       /* login, or single arg shells */
            ) {

            use_default = 0;                  /* we will use this shell */
            D(("committed to using user's shell"));
            if (command) {
                arg_no += 2;                  /* will append "-c" "command" */
            }

            /* allocate an array of pointers long enough */

            D(("building array of size %d", 2+arg_no));
            args = (const char **) calloc(2+arg_no, sizeof(const char *));
            if (args == NULL)
                return NULL;
            /* get a string long enough for all the arguments */

            D(("an array of size %d chars", 2+strlen(tmpb)
                                   + ( command ? 4:0 )));
            line = (char *) malloc(2+strlen(tmpb)
                                   + ( command ? 4:0 ));
            if (line == NULL) {
                free(args);
                return NULL;
            }

            /* fill array - tmpb points to start of first non-space char */

            line[0] = '-';
            strcpy(line+1, tmpb);

            /* append " -c" to line? */
            if (command) {
                strcat(line, " -c");
            }

            D(("complete command: %s [+] %s", line, command));

            tmp = strtok(line, " \t");
            D(("command path=%s", line+1));
            args[0] = line+1;

            if (login) {               /* standard procedure for login shell */
                D(("argv[0]=%s", line));
                args[i=1] = line;
            } else {                 /* not a login shell -- for use with su */
                D(("argv[0]=%s", line+1));
                args[i=1] = line+1;
            }

            while ((tmp = strtok(NULL, " \t"))) {
                D(("adding argument %d: %s",i,tmp));
                args[++i] = tmp;
            }
            if (command) {
                D(("appending command [%s]", command));
                args[++i] = command;
            }
            D(("terminating args with NULL"));
            args[++i] = NULL;
            D(("list completed."));
        }
    }

    /* should we use the default shell instead of specific one? */

    if (use_default && !login) {
        int last_arg;

        D(("selecting default shell"));
        last_arg = command ? 5:3;

        args = (const char **) calloc(last_arg--, sizeof(const char *));
        if (args == NULL) {
            return NULL;
        }
        args[1] = DEFAULT_SHELL;      /* mapped to argv[0] (NOT login shell) */
        args[0] = args[1];            /* path to program */
        if (command) {
            args[2] = "-c";           /* should perform command and exit */
            args[3] = command;        /* the desired command */
        }
        args[last_arg] = NULL;        /* terminate list of args */
    }

    D(("returning arg list"));
    return (char * const *) args;
}


/* ------ abnormal termination ---------------- */

static void exit_now(int exit_code, const char *format, ...)
{
    va_list args;

    va_start(args, format);
    vfprintf(stderr, format, args);
    va_end(args);

    if (pamh != NULL)
	pam_end(pamh, exit_code ? PAM_ABORT:PAM_SUCCESS);

    /* USER's shell may have completely broken terminal settings
       restore the sane(?) initial conditions */
    restore_terminal_modes();

    exit(exit_code);
}

static void exit_child_now(int exit_code, const char *format, ...)
{
    va_list args;

    va_start(args,format);
    vfprintf(stderr, format, args);
    va_end(args);

    if (pamh != NULL)
	pam_end(pamh, (exit_code ? PAM_ABORT:PAM_SUCCESS) | PAM_DATA_SILENT);

    exit(exit_code);
}

/* ------ PAM setup --------------------------- */

static struct pam_conv conv = {
    misc_conv,                   /* defined in <pam_misc/libmisc.h> */
    NULL
};

static void do_pam_init(const char *user, int is_login)
{
    int retval;

    retval = pam_start(PAM_APP_NAME, user, &conv, &pamh);
    if (retval != PAM_SUCCESS) {
	/*
	 * From my point of view failing of pam_start() means that
	 * pamh isn't a valid handler. Without a handler
	 * we couldn't call pam_strerror :-(   1998/03/29 (SAW)
	 */
	fprintf(stderr, PAM_APP_NAME ": pam_start failed with code %d\n",
		retval);
	exit(1);
    }

    /*
     * Fill in some blanks
     */

    retval = make_environment(pamh, !is_login);
    D(("made_environment returned: %s", pam_strerror(pamh,retval)));

    if (retval == PAM_SUCCESS && is_terminal) {
	const char *terminal = ttyname(STDIN_FILENO);
	if (terminal) {
	    retval = pam_set_item(pamh, PAM_TTY, (const void *)terminal);
	} else {
	    retval = PAM_PERM_DENIED;                /* how did we get here? */
	}
	terminal = NULL;
    }

    if (retval == PAM_SUCCESS && is_terminal) {
	const char *ruser = getlogin();      /* Who is running this program? */
	if (ruser) {
	    retval = pam_set_item(pamh, PAM_RUSER, (const void *)ruser);
	} else {
	    retval = PAM_PERM_DENIED;             /* must be known to system */
	}
	ruser = NULL;
    }

    if (retval == PAM_SUCCESS) {
	retval = pam_set_item(pamh, PAM_RHOST, (const void *)"localhost");
    }

    if (retval != PAM_SUCCESS) {
	exit_now(1, PAM_APP_NAME ": problem establishing environment\n");
    }

    /* have to pause on failure. At least this long (doubles..) */
    retval = pam_fail_delay(pamh, SU_FAIL_DELAY);
    if (retval != PAM_SUCCESS) {
	exit_now(1, PAM_APP_NAME ": problem initializing failure delay\n");
    }
}

/*
 * authenticate_user arranges for the PAM authentication stack to run.
 */
static int authenticate_user(pam_handle_t *pamh, cap_t all,
			     int *retval, const char **place,
			     const char **err_descr)
{
    *place = "pre-auth cap_set_proc";
    if (cap_set_proc(all)) {
	D(("failed to raise all capabilities"));
	*err_descr = "cap_set_proc() failed";
	*retval = PAM_SUCCESS;
	return 1;
    }

    D(("attempt to authenticate user"));
    *place = "pam_authenticate";
    *retval = pam_authenticate(pamh, 0);
    return (*retval != PAM_SUCCESS);
}

/*
 * user_accounting confirms an authenticated user is permitted service.
 */
static int user_accounting(pam_handle_t *pamh, cap_t all,
			   int *retval, const char **place,
			   const char **err_descr) {
    *place = "user_accounting";
    if (cap_set_proc(all)) {
	D(("failed to raise all capabilities"));
	*err_descr = "cap_set_proc() failed";
	return 1;
    }
    *place = "pam_acct_mgmt";
    *retval = pam_acct_mgmt(pamh, 0);
    return (*retval != PAM_SUCCESS);
}

/*
 * Find entry for this terminal (if there is one).
 * Utmp file should have been opened and rewinded for the call.
 *
 * XXX: the search should be more or less compatible with libc one.
 * The caller expects that pututline with the same arguments
 * will replace the found entry.
 */
static const struct utmp *find_utmp_entry(const char *ut_line,
					  const char *ut_id)
{
    struct utmp *u_tmp_p;

    while ((u_tmp_p = getutent()) != NULL)
	if ((u_tmp_p->ut_type == INIT_PROCESS ||
             u_tmp_p->ut_type == LOGIN_PROCESS ||
             u_tmp_p->ut_type == USER_PROCESS ||
             u_tmp_p->ut_type == DEAD_PROCESS) &&
            !strncmp(u_tmp_p->ut_id, ut_id, UT_IDSIZE) &&
            !strncmp(u_tmp_p->ut_line, ut_line, UT_LINESIZE))
                break;

    return u_tmp_p;
}

/*
 * Identify the terminal name and the abbreviation we will use.
 */
static void set_terminal_name(const char *terminal, char *ut_line, char *ut_id)
{
    memset(ut_line, 0, UT_LINESIZE);
    memset(ut_id, 0, UT_IDSIZE);

    /* set the terminal entry */
    if ( *terminal == '/' ) {     /* now deal with filenames */
	int o1, o2;

	o1 = strncmp(DEVICE_FILE_PREFIX, terminal, 5) ? 0 : 5;
	if (!strncmp("/dev/tty", terminal, 8)) {
	    o2 = 8;
	} else {
	    o2 = strlen(terminal) - sizeof(UT_IDSIZE);
	    if (o2 < 0)
		o2 = 0;
	}

	strncpy(ut_line, terminal + o1, UT_LINESIZE);
	strncpy(ut_id, terminal + o2, UT_IDSIZE);
    } else if (strchr(terminal, ':')) {  /* deal with X-based session */
	const char *suffix;

	suffix = strrchr(terminal,':');
	strncpy(ut_line, terminal, UT_LINESIZE);
	strncpy(ut_id, suffix, UT_IDSIZE);
    } else {	                         /* finally deal with weird terminals */
	strncpy(ut_line, terminal, UT_LINESIZE);
	ut_id[0] = '?';
	strncpy(ut_id + 1, terminal, UT_IDSIZE - 1);
    }
}

/*
 * Append an entry to wtmp. See utmp_open_session for the return convention.
 * Be careful: the function uses alarm().
 */

#define WWTMP_STATE_BEGINNING     0
#define WWTMP_STATE_FILE_OPENED   1
#define WWTMP_STATE_SIGACTION_SET 2
#define WWTMP_STATE_LOCK_TAKEN    3

static int write_wtmp(struct utmp *u_tmp_p, const char **callname,
		      const char **err_descr)
{
    int w_tmp_fd;
    struct flock w_lock;
    struct sigaction act1, act2;
    int state;
    int retval;

    state = WWTMP_STATE_BEGINNING;
    retval = 1;

    do {
        D(("writing to wtmp"));
        w_tmp_fd = open(_PATH_WTMP, O_APPEND|O_WRONLY);
        if (w_tmp_fd == -1) {
            *callname = "wtmp open";
            *err_descr = strerror(errno);
            break;
        }
        state = WWTMP_STATE_FILE_OPENED;

        /* prepare for blocking operation... */
        act1.sa_handler = SIG_DFL;
        sigemptyset(&act1.sa_mask);
        act1.sa_flags = 0;
        if (sigaction(SIGALRM, &act1, &act2) == -1) {
            *callname = "sigaction";
            *err_descr = strerror(errno);
            break;
        }
        alarm(WTMP_LOCK_TIMEOUT);
        state = WWTMP_STATE_SIGACTION_SET;

        /* now we try to lock this file-rcord exclusively; non-blocking */
        memset(&w_lock, 0, sizeof(w_lock));
        w_lock.l_type = F_WRLCK;
        w_lock.l_whence = SEEK_END;
        if (fcntl(w_tmp_fd, F_SETLK, &w_lock) < 0) {
            D(("locking %s failed.", _PATH_WTMP));
            *callname = "fcntl(F_SETLK)";
            *err_descr = strerror(errno);
            break;
        }
        alarm(0);
        sigaction(SIGALRM, &act2, NULL);
        state = WWTMP_STATE_LOCK_TAKEN;

        if (write(w_tmp_fd, u_tmp_p, sizeof(struct utmp)) != -1) {
            retval = 0;
	}
    } while(0); /* it's not a loop! */

    if (state >= WWTMP_STATE_LOCK_TAKEN) {
        w_lock.l_type = F_UNLCK;               /* unlock wtmp file */
        fcntl(w_tmp_fd, F_SETLK, &w_lock);
    }else if (state >= WWTMP_STATE_SIGACTION_SET) {
        alarm(0);
        sigaction(SIGALRM, &act2, NULL);
    }

    if (state >= WWTMP_STATE_FILE_OPENED) {
        close(w_tmp_fd);                       /* close wtmp file */
        D(("wtmp written"));
    }

    return retval;
}

/*
 * XXX - if this gets turned into a module, make this a
 * pam_data item. You should put the pid in the name so we can
 * "probably" nest calls more safely...
 */
struct utmp *login_stored_utmp=NULL;

/*
 * Returns:
 *   0     ok,
 *   1     non-fatal error
 *  -1     fatal error
 *  callname and err_descr will be set
 * Be careful: the function indirectly uses alarm().
 */
static int utmp_do_open_session(const char *user, const char *terminal,
				const char *rhost, pid_t pid,
				const char **place, const char **err_descr)
{
    struct utmp u_tmp;
    const struct utmp *u_tmp_p;
    char ut_line[UT_LINESIZE], ut_id[UT_IDSIZE];
    int retval;

    set_terminal_name(terminal, ut_line, ut_id);

    utmpname(_PATH_UTMP);
    setutent();                                           /* rewind file */
    u_tmp_p = find_utmp_entry(ut_line, ut_id);

    /* reset new entry */
    memset(&u_tmp, 0, sizeof(u_tmp));                     /* reset new entry */
    if (u_tmp_p == NULL) {
	D(("[NEW utmp]"));
    } else {
	D(("[OLD utmp]"));

	/*
	 * here, we make a record of the former entry. If the
	 * utmp_close_session code is attached to the same process,
	 * the wtmp will be replaced, otherwise we leave init to pick
	 * up the pieces.
	 */
	if (login_stored_utmp == NULL) {
	    login_stored_utmp = malloc(sizeof(struct utmp));
            if (login_stored_utmp == NULL) {
                *place = "malloc";
                *err_descr = "fail";
                endutent();
                return -1;
            }
	}
        memcpy(login_stored_utmp, u_tmp_p, sizeof(struct utmp));
    }

    /* we adjust the entry to reflect the current session */
    {
	strncpy(u_tmp.ut_line, ut_line, UT_LINESIZE);
	memset(ut_line, 0, UT_LINESIZE);
	strncpy(u_tmp.ut_id, ut_id, UT_IDSIZE);
	memset(ut_id, 0, UT_IDSIZE);
	strncpy(u_tmp.ut_user, user
		, sizeof(u_tmp.ut_user));
	strncpy(u_tmp.ut_host, rhost ? rhost : RHOST_UNKNOWN_NAME
		, sizeof(u_tmp.ut_host));

	/* try to fill the host address entry */
	if (rhost != NULL) {
	    struct hostent *hptr;

	    /* XXX: it isn't good to do DNS lookup here...  1998/05/29  SAW */
            hptr = gethostbyname(rhost);
	    if (hptr != NULL && hptr->h_addr_list) {
		memcpy(&u_tmp.ut_addr, hptr->h_addr_list[0]
		       , sizeof(u_tmp.ut_addr));
	    }
	}

	/* we fill in the remaining info */
	u_tmp.ut_type = USER_PROCESS;          /* a user process starting */
	u_tmp.ut_pid = pid;                    /* session identifier */
	u_tmp.ut_time = time(NULL);
    }

    setutent();                                /* rewind file (replace old) */
    pututline(&u_tmp);                         /* write it to utmp */
    endutent();                                /* close the file */

    retval = write_wtmp(&u_tmp, place, err_descr); /* write to wtmp file */
    memset(&u_tmp, 0, sizeof(u_tmp));          /* reset entry */

    return retval;
}

static int utmp_do_close_session(const char *terminal,
				 const char **place, const char **err_descr)
{
    int retval;
    struct utmp u_tmp;
    const struct utmp *u_tmp_p;
    char ut_line[UT_LINESIZE], ut_id[UT_IDSIZE];

    retval = 0;

    set_terminal_name(terminal, ut_line, ut_id);

    utmpname(_PATH_UTMP);
    setutent();                                              /* rewind file */

    /*
     * if there was a stored entry, return it to the utmp file, else
     * if there is a session to close, we close that
     */
    if (login_stored_utmp) {
	pututline(login_stored_utmp);

	memcpy(&u_tmp, login_stored_utmp, sizeof(u_tmp));
	u_tmp.ut_time = time(NULL);            /* a new time to restart */

        retval = write_wtmp(&u_tmp, place, err_descr);

	memset(login_stored_utmp, 0, sizeof(u_tmp)); /* reset entry */
	free(login_stored_utmp);
    } else {
        u_tmp_p = find_utmp_entry(ut_line, ut_id);
        if (u_tmp_p != NULL) {
            memset(&u_tmp, 0, sizeof(u_tmp));
            strncpy(u_tmp.ut_line, ut_line, UT_LINESIZE);
            strncpy(u_tmp.ut_id, ut_id, UT_IDSIZE);
            memset(&u_tmp.ut_user, 0, sizeof(u_tmp.ut_user));
            memset(&u_tmp.ut_host, 0, sizeof(u_tmp.ut_host));
            u_tmp.ut_addr = 0;
            u_tmp.ut_type = DEAD_PROCESS;      /* `old' login process */
            u_tmp.ut_pid = 0;
            u_tmp.ut_time = time(NULL);
            setutent();                        /* rewind file (replace old) */
            pututline(&u_tmp);                 /* mark as dead */

            retval = write_wtmp(&u_tmp, place, err_descr);
        }
    }

    /* clean up */
    memset(ut_line, 0, UT_LINESIZE);
    memset(ut_id, 0, UT_IDSIZE);

    endutent();                                /* close utmp file */
    memset(&u_tmp, 0, sizeof(u_tmp));          /* reset entry */

    return 0;
}

/*
 * Returns:
 *   0     ok,
 *   1     non-fatal error
 *  -1     fatal error
 * place and err_descr will be set
 * Be careful: the function indirectly uses alarm().
 */
static int utmp_open_session(pam_handle_t *pamh, pid_t pid,
			     int *retval,
			     const char **place, const char **err_descr)
{
    const char *user, *terminal, *rhost;

    *retval = pam_get_item(pamh, PAM_USER, (const void **)&user);
    if (*retval != PAM_SUCCESS) {
        return -1;
    }
    *retval = pam_get_item(pamh, PAM_TTY, (const void **)&terminal);
    if (retval != PAM_SUCCESS) {
        return -1;
    }
    *retval = pam_get_item(pamh, PAM_RHOST, (const void **)&rhost);
    if (retval != PAM_SUCCESS) {
        rhost = NULL;
    }

    return utmp_do_open_session(user, terminal, rhost, pid, place, err_descr);
}

static int utmp_close_session(pam_handle_t *pamh
                              , const char **place, const char **err_descr)
{
    int retval;
    const char *terminal;

    retval = pam_get_item(pamh, PAM_TTY, (const void **)&terminal);
    if (retval != PAM_SUCCESS) {
        *place = "pam_get_item(PAM_TTY)";
        *err_descr = pam_strerror(pamh, retval);
        return -1;
    }

    return utmp_do_close_session(terminal, place, err_descr);
}

/*
 * set_credentials raises all of the process and PAM credentials.
 */
static int set_credentials(pam_handle_t *pamh, cap_t all, int login,
			   const char **pw_shell,
			   int *retval, const char **place,
			   const char **err_descr)
{
    const char *user;
    char *shell;
    cap_value_t csetgid = CAP_SETGID;
    cap_t current;
    int status;
    struct passwd *pw;
    uid_t uid;

    D(("get user from pam"));
    *place = "set_credentials";
    *retval = pam_get_item(pamh, PAM_USER, (const void **)&user);
    if (*retval != PAM_SUCCESS || user == NULL || *user == '\0') {
	D(("error identifying user from PAM."));
	*retval = PAM_USER_UNKNOWN;
	return 1;
    }

    /*
     * Add the LOGNAME and HOME environment variables.
     */

    pw = getpwnam(user);
    if (pw == NULL || (user = x_strdup(pw->pw_name)) == NULL) {
	D(("failed to identify user"));
	*retval = PAM_USER_UNKNOWN;
	return 1;
    }

    uid = pw->pw_uid;
    shell = x_strdup(pw->pw_shell);
    if (shell == NULL) {
	D(("user %s has no shell", user));
	*retval = PAM_CRED_ERR;
	return 1;
    }

    if (login) {
	/* set LOGNAME, HOME */
	if (pam_misc_setenv(pamh, "LOGNAME", user, 0) != PAM_SUCCESS) {
	    D(("failed to set LOGNAME"));
	    *retval = PAM_CRED_ERR;
	    return 1;
	}
	if (pam_misc_setenv(pamh, "HOME", pw->pw_dir, 0) != PAM_SUCCESS) {
	    D(("failed to set HOME"));
	    *retval = PAM_CRED_ERR;
	    return 1;
	}
    }

    current = cap_get_proc();
    cap_set_flag(current, CAP_EFFECTIVE, 1, &csetgid, CAP_SET);
    status = cap_set_proc(current);
    cap_free(current);
    if (status != 0) {
	*err_descr = "unable to raise CAP_SETGID";
	return 1;
    }

    /* initialize groups */
    if (initgroups(pw->pw_name, pw->pw_gid) != 0 || setgid(pw->pw_gid) != 0) {
	D(("failed to setgid etc"));
	*retval = PAM_PERM_DENIED;
	return 1;
    }
    *pw_shell = shell;

    pw = NULL;                                                  /* be tidy */

    D(("desired uid=%d", uid));

    /* assume user's identity - but preserve the permitted set */
    if (cap_setuid(uid) != 0) {
	D(("failed to setuid: %v", strerror(errno)));
	*retval = PAM_PERM_DENIED;
	return 1;
    }

    /*
     * Next, we call the PAM framework to add/enhance the credentials
     * of this user [it may change the user's home directory in the
     * pam_env, and add supplemental group memberships...].
     */
    D(("setting credentials"));
    if (cap_set_proc(all)) {
	D(("failed to raise all capabilities"));
	*retval = PAM_PERM_DENIED;
	return 1;
    }

    D(("calling pam_setcred to establish credentials"));
    *retval = pam_setcred(pamh, PAM_ESTABLISH_CRED);

    return (*retval != PAM_SUCCESS);
}

/*
 * open_session invokes the open session PAM stack.
 */
static int open_session(pam_handle_t *pamh, cap_t all,
			int *retval, const char **place, const char **err_descr)
{
    /* Open the su-session */
    *place = "pam_open_session";
    if (cap_set_proc(all)) {
	D(("failed to raise t_caps capabilities"));
	*err_descr = "capability setting failed";
	return 1;
    }
    *retval = pam_open_session(pamh, 0);     /* Must take care to close */
    if (*retval != PAM_SUCCESS) {
	return 1;
    }
    return 0;
}

/* ------ shell invoker ----------------------- */

static int launch_callback_fn(void *h)
{
    pam_handle_t *pamh = h;
    int retval;

    D(("pam_end"));
    retval = pam_end(pamh, PAM_SUCCESS | PAM_DATA_SILENT);
    pamh = NULL;
    if (retval != PAM_SUCCESS) {
	return -1;
    }

    /*
     * Restore a signal status: information if the signal is ignored
     * is inherited across exec() call.  (SAW)
     */
    enable_terminal_signals();

    D(("about to launch"));
    return 0;
}

/* Returns PAM_<STATUS>. */
static int perform_launch_and_cleanup(cap_t all, int is_login,
				      const char *shell, const char *command)
{
    int retval, status;
    const char *user, *home;
    uid_t uid;
    char * const * shell_args;
    char * const * shell_env;
    cap_launch_t launcher;
    pid_t child;
    cap_iab_t iab;

    /*
     * Break up the shell command into a command and arguments
     */
    shell_args = build_shell_args(shell, is_login, command);
    if (shell_args == NULL) {
	D(("failed to compute shell arguments"));
	return PAM_SYSTEM_ERR;
    }

    home = pam_getenv(pamh, "HOME");
    if ( !home || home[0] == '\0' ) {
	fprintf(stderr, "setting home directory for %s to %s\n",
		user, DEFAULT_HOME);
	home = DEFAULT_HOME;
	if (pam_misc_setenv(pamh, "HOME", home, 0) != PAM_SUCCESS) {
	    D(("unable to set $HOME"));
	    fprintf(stderr,
		    "Warning: unable to set HOME environment variable\n");
	}
    }
    if (is_login) {
	if (chdir(home) && chdir(DEFAULT_HOME)) {
	    D(("failed to change directory"));
	    return PAM_SYSTEM_ERR;
	}
    }

    shell_env = pam_getenvlist(pamh);
    if (shell_env == NULL) {
	D(("failed to obtain environment for child"));
	return PAM_SYSTEM_ERR;
    }

    iab = cap_iab_get_proc();
    if (iab == NULL) {
	D(("failed to read IAB value of process"));
	return PAM_SYSTEM_ERR;
    }
    if (cap_set_proc(all) != 0) {
	D(("failed to restore process capabilities"));
	return PAM_SYSTEM_ERR;
    }

    launcher = cap_new_launcher(shell_args[0],
				(const char * const *) &shell_args[1],
				(const char * const *) shell_env);
    if (launcher == NULL) {
	D(("failed to initialize launcher"));
	return PAM_SYSTEM_ERR;
    }
    cap_launcher_set_iab(launcher, iab);
    cap_launcher_callback(launcher, launch_callback_fn);

    child = cap_launch(launcher, pamh);
    cap_free(launcher);

    /* job control is off for login sessions */
    prepare_for_job_control(!is_login && command != NULL);

    if (cap_setuid(TEMP_UID) != 0) {
	fprintf(stderr, "[failed to change monitor UID=%d]\n", TEMP_UID);
    }

    /* wait for child to terminate */
    status = wait_for_child(child);
    if (status != 0) {
	D(("shell returned %d", status));
    }
    return status;
}

static void close_session(pam_handle_t *pamh, cap_t all)
{
    int retval;

    D(("session %p closing", pamh));
    if (cap_set_proc(all)) {
	fprintf(stderr, "WARNING: could not raise all caps\n");
    }
    retval = pam_close_session(pamh, 0);
    if (retval != PAM_SUCCESS) {
	fprintf(stderr, "WARNING: could not close session\n\t%s\n",
		pam_strerror(pamh,retval));
    }
}

/* -------------------------------------------- */
/* ------ the application itself -------------- */
/* -------------------------------------------- */

int main(int argc, char *argv[])
{
    int retcode, is_login, status;
    int retval, final_retval; /* PAM_xxx return values */
    const char *command, *shell;
    pid_t child;
    uid_t uid;
    const char *place = NULL, *err_descr = NULL;
    cap_t all, t_caps;

    all = cap_get_proc();
    cap_fill(all, CAP_EFFECTIVE, CAP_PERMITTED);
    cap_clear_flag(all, CAP_INHERITABLE);

    checkfds();

    /*
     * Check whether stdin is a terminal and store terminal modes for later.
     */
    store_terminal_modes();

    /* ---------- parse the argument list and --------- */
    /* ------ initialize the Linux-PAM interface ------ */
    {
	const char *user;              /* transient until PAM_USER defined */
	parse_command_line(argc, argv, &is_login, &user, &command);
	place = "do_pam_init";
	do_pam_init(user, is_login);   /* call pam_start and set PAM items */
    }

    /*
     * Turn off terminal signals - this is to be sure that su gets a
     * chance to call pam_end() and restore the terminal modes in
     * spite of the frustrated user pressing Ctrl-C.
     */
    disable_terminal_signals();

    /*
     * Random exits from here are strictly prohibited :-) (SAW) AGM
     * achieves this with goto's and a single exit at the end of main.
     */
    status = 1;                       /* fake exit status of a child */
    err_descr = NULL;                 /* errors haven't happened */

    if (make_process_unkillable(&place, &err_descr) != 0) {
	goto su_exit;
    }

    if (authenticate_user(pamh, all, &retval, &place, &err_descr) != 0) {
	goto auth_exit;
    }

    /*
     * The user is valid, but should they have access at this
     * time?
     */
    if (user_accounting(pamh, all, &retval, &place, &err_descr) != 0) {
	goto auth_exit;
    }

    D(("su attempt is confirmed as authorized"));

    /*
     * ... setup terminal, ...
     */
    retcode = change_terminal_owner(uid, is_login, &place, &err_descr);
    if (retcode > 0) {
	fprintf(stderr, PAM_APP_NAME ": %s: %s\n", place, err_descr);
	err_descr = NULL; /* forget about the problem */
    } else if (retcode < 0) {
	D(("terminal owner to uid=%d change failed", uid));
	goto auth_exit;
    }

    if (set_credentials(pamh, all, is_login,
			&shell, &retval, &place, &err_descr) != 0) {
	D(("failed to set credentials"));
	goto auth_exit;
    }

    /*
     * Here the IAB value is fixed and may differ from all's
     * Inheritable value. So synthesize what we need to proceed in the
     * child, for now, in this current process.
     */
    place = "preserving inheritable parts";
    t_caps = cap_get_proc();
    if (t_caps == NULL) {
	D(("failed to read capabilities"));
	err_descr = "capability read failed";
	goto delete_cred;
    }
    if (cap_fill(t_caps, CAP_EFFECTIVE, CAP_PERMITTED)) {
	D(("failed to fill effective bits"));
	err_descr = "capability fill failed";
	goto delete_cred;
    }

    /*
     * ... make [uw]tmp entries.
     */
    if (is_login) {
	/*
	 * Note: we use the parent pid as a session identifier for
	 * the logging.
	 */
	retcode = utmp_open_session(pamh, getpid(),
				    &retval, &place, &err_descr);
	if (retcode > 0) {
	    fprintf(stderr, PAM_APP_NAME ": %s: %s\n", place, err_descr);
	    err_descr = NULL; /* forget about this non-critical problem */
	} else if (retcode < 0) {
	    goto delete_cred;
	}
    }

    if (open_session(pamh, t_caps, &retval, &place, &err_descr) != 0) {
	goto utmp_closer;
    }

    status = perform_launch_and_cleanup(all, is_login, shell, command);
    close_session(pamh, all);

utmp_closer:
    if (is_login) {
	/* do [uw]tmp cleanup */
	retcode = utmp_close_session(pamh, &place, &err_descr);
	if (retcode) {
	    fprintf(stderr, PAM_APP_NAME ": %s: %s\n", place, err_descr);
	}
    }

delete_cred:
    D(("delete credentials"));
    if (cap_set_proc(all)) {
	D(("failed to raise all capabilities"));
    }
    retcode = pam_setcred(pamh, PAM_DELETE_CRED);
    if (retcode != PAM_SUCCESS) {
	fprintf(stderr, "WARNING: could not delete credentials\n\t%s\n",
		pam_strerror(pamh, retcode));
    }

old_owner:
    D(("return terminal to local control"));
    restore_terminal_owner();

auth_exit:
    D(("for clean up we restore the launching user"));
    make_process_killable();

    D(("all done - closing down pam"));
    if (retval != PAM_SUCCESS) {      /* PAM has failed */
	fprintf(stderr, PAM_APP_NAME ": %s\n", pam_strerror(pamh, retval));
	final_retval = PAM_ABORT;
    } else if (err_descr != NULL) {   /* a system error has happened */
	fprintf(stderr, PAM_APP_NAME ": %s: %s\n", place, err_descr);
	final_retval = PAM_ABORT;
    } else {
	final_retval = PAM_SUCCESS;
    }
    (void) pam_end(pamh, final_retval);
    pamh = NULL;

    if (restore_terminal_modes() != 0 && !status) {
	status = 1;
    }

su_exit:
    exit(status);                 /* transparent exit */
}
