/* 
 * $Id: su.c,v 1.12 1997/06/24 14:18:06 morgan Exp morgan $
 *
 * su.c
 *
 * ( based on an implementation of `su' by
 *
 *     Peter Orbaek  <poe@daimi.aau.dk>
 *
 * obtained from ftp://ftp.daimi.aau.dk/pub/linux/poe/ )
 *
 * Rewritten for Linux-PAM by Andrew Morgan <morgan@parc.power.net>
 *
 * $Log: su.c,v $
 * Revision 1.12  1997/06/24 14:18:06  morgan
 * update for .55 release
 *
 * Revision 1.11  1997/02/24 06:02:03  morgan
 * major overhaul, read diff
 *
 * Revision 1.10  1997/01/29 03:35:39  morgan
 * update for release
 *
 * Revision 1.9  1996/12/15 16:27:27  morgan
 * root can ignore account management
 *
 * Revision 1.8  1996/12/01 00:54:05  morgan
 * extra checks for no user shell
 */

/* to be strictly POSIX uncomment the following
#define SU_STRICTLY_POSIX
*/

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

#include <stdlib.h>
#include <signal.h>
#include <termios.h>
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>

#ifndef SU_STRICTLY_POSIX
#define __USE_BSD
#endif
#include <unistd.h>

#include <pwd.h>
#define __USE_BSD
#include <grp.h>

#include <sys/file.h>
#include <string.h>
#include <stdarg.h>

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

#ifdef HAVE_PWDB
#include <pwdb/pwdb_public.h>
#endif

#include "../inc/make_env.-c"
#include "../inc/setcred.-c"
#include "../inc/shell_args.-c"
#include "../inc/wait4shell.-c"
#include "../inc/wtmp.-c"

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

static int is_terminal=0;
static struct termios stored_mode;        /* initial terminal mode settings */
static struct sigaction old_int_act, old_quit_act, old_tstp_act;

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

static pam_handle_t *pamh=NULL;

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);

#ifdef HAVE_PWDB
    while (pwdb_end() == PWDB_SUCCESS);                       /* clean up */
#endif /* HAVE_PWDB */

    /* USER's shell may have completely broken terminal settings
       restore the sane(?) initial conditions */
    if (is_terminal) {
	(void) tcsetattr(STDIN_FILENO, TCSAFLUSH, &stored_mode);
    }

    exit(exit_code);
}

#ifdef HAVE_PWDB
static void end_now(int retval)
{
    if (retval != PWDB_SUCCESS)
	exit_now(1, "su: failed\n");
}
#endif

static void su_exec_shell(const char *shell, uid_t uid, int login
			  , const char *command, const char *user)
{
    char * const * shell_args;
    char * const * shell_env;
    const char *pw_dir;
    int retval;

    /*
     * Now, find the home directory for the user
     */

    pw_dir = pam_getenv(pamh, "HOME");
    if ( !pw_dir || pw_dir[0] == '\0' ) {
	/* Not set so far, so we get it now. */
	struct passwd *pwd;

	pwd = getpwnam(user);
	if (pwd != NULL && pwd->pw_name != NULL) {
	    pw_dir = x_strdup(pwd->pw_name);
	}

	/* Last resort, take default directory.. */
	if ( !pw_dir || pw_dir[0] == '\0') {
	    fprintf(stderr, "setting home directory for %s to %s\n"
		    , user, DEFAULT_HOME);
	    pw_dir = DEFAULT_HOME;
	}
    }

    /*
     * We may wish to change the current directory.
     */

    if (login && chdir(pw_dir)) {
	exit_now(1, "%s not available; exiting\n", pw_dir);
    }

    /*
     * If it is a login session, we should set the environment
     * accordingly.
     */

    if (login
	&& pam_misc_setenv(pamh, "HOME", pw_dir, 0) != PAM_SUCCESS) {
	D(("failed to set $HOME"));
	fprintf(stderr, "Warning: unable to set HOME environment variable\n");
    }

    /*
     * Break up the shell command into a command and arguments
     */

    shell_args = build_shell_args(shell, login, command);
    if (shell_args == NULL) {
	exit_now(1, "su: could not identify appropriate shell\n");
    }

    /*
     * and now copy the environment for non-PAM use
     */

    shell_env = pam_getenvlist(pamh);
    if (shell_env == NULL) {
	exit_now(1, "su: corrupt environment\n");
    }

    /*
     * close PAM (quietly = this is a forked process so ticket files
     * should *not* be deleted logs should not be written - the parent
     * will take care of this)
     */

    D(("pam_end"));
    retval = pam_end(pamh, PAM_SUCCESS
#ifdef PAM_DATA_QUIET
		     | PAM_DATA_QUIET
#endif
	);
    pamh = NULL;
    user = NULL;                            /* user's name not valid now */
    if (retval != PAM_SUCCESS) {
	exit_now(1, "su: failed to release authenticator\n");
    }

#ifdef HAVE_PWDB
    while ( pwdb_end() == PWDB_SUCCESS );            /* forget all */
#endif

    /* assume user's identity */
    if (setuid(uid) != 0) {
	exit_now(1, "su: cannot assume uid\n");
    }

    /*
     * Restore signal status: information if signal was ingored
     * is inherited accross exec() call.  (SAW)
     */
    sigaction(SIGINT, &old_int_act, NULL);
    sigaction(SIGQUIT, &old_quit_act, NULL);
    sigaction(SIGTSTP, &old_tstp_act, NULL);

    execve(shell_args[0], shell_args+1, shell_env);
    exit_now(1, "su: exec failed\n");
}

/* ------ some static data objects ------- */

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

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

void main(int argc, char *argv[])
{
    int retval, login, status;
    const char *command=NULL;
    const char *user=NULL, *shell=NULL;
    pid_t child;
    uid_t uid, invoked_uid, terminal_uid;

    /*
     * store terminal modes for later
     */
    if (isatty(STDIN_FILENO)) {
	is_terminal = 1;
	if (tcgetattr(STDIN_FILENO, &stored_mode) != 0) {
	    fprintf(stderr, "su: couldn't copy terminal mode");
	    exit(1);
	}
    } else if (getuid()) {
	fprintf(stderr, "su: must be run from a terminal\n");
	exit(1);
    } else {
	D(("process was run by superuser; assume you know what you're doing"));
	is_terminal = 0;
    }

    /*
     * Turn off terminal signals - this is to be sure that su gets a
     * chance to call pam_end() in spite of the frustrated user
     * pressing Ctrl-C. (Only the superuser is exempt in the case that
     * they are trying to run su without a controling tty).
     */

    /*
     * XXX: Because of the way Linux, Solaris and others(?) handle
     * process control, it is possible for another process (owned by
     * the invoking user) to kill this (setuid) program during its
     * execution. This breaks the integrety of the PAM model (which
     * assumes that pam_end() is correctly called in all cases).  As
     * of 1997/3/8, this still needs to be addressed.  At this time,
     * is not clear if this is an issue for the kernel, or requires
     * some enhancements to the PAM spec. (AGM)
     *
     * I see only one way to protect us from unexpected killing --
     * changing our uid. So we should choose between a) performing
     * authentication etc with changed uid, and b) can be killed
     * before pam_end() call. So we should change and/or make more
     * detail PAM specification, especially uid assumptions.
     *                                  1997/7/14  (SAW)
     */

    {
	/* 
	 * Now we protect ourself against terminal signals.
	 * They are sent regardless of the user id.
	 * We perform protecting via sigaction().
	 */
	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);
    }

    /* ------------ parse the argument list ----------- */

    /*
     * we overload status: here it is used to indicate we have already
     * obtained a username. Similarly we overload retval: here it is
     * used to indicate some command was specified.
     */

    status = retval = login = 0;

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

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

#ifdef HAVE_PWDB
    retval = pwdb_start();
    end_now(retval);
#endif

    if (!status) {                         /* default user is superuser */
	const struct passwd *pw;

	pw = getpwuid(ROOT_UID);
	if (pw == NULL) {                              /* No ROOT_UID!? */
	    exit_now(1,"su: no access to superuser identity!? (%d)\n"
		     , ROOT_UID);
	}
	user = x_strdup(pw->pw_name);
    }

    /* ------ initialize the Linux-PAM interface ------ */

    retval = pam_start("su", user, &conv, &pamh);
    user = NULL;                      /* get this info later (it may change) */

    /*
     * Fill in some blanks
     */

    retval = make_environment(pamh, !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, "su: problem establishing environment\n");
    }

    /*
     * Note. We have forgotten everything about the user. We will get
     * this info back after the user has been authenticated..
     */

#ifdef HAVE_PAM_FAIL_DELAY
    /* 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, "su: problem initializing failure delay\n");
    }
#endif /* HAVE_PAM_FAIL_DELAY */

    while (retval == PAM_SUCCESS) {    /* abuse loop to avoid using goto... */

	retval = pam_authenticate(pamh, 0);	   /* authenticate the user */
	if (retval != PAM_SUCCESS)
	    break;

	/*
	 * The user is valid, but should they have access at this
	 * time?
	 */

	retval = pam_acct_mgmt(pamh, 0);
	if (retval != PAM_SUCCESS) {
	    if (getuid() == 0) {
		fprintf(stderr, "Account management:- %s\n(Ignored)\n"
			, pam_strerror(pamh,retval));
	    } else {
		break;
	    }
	}

	/* open the su-session */

	retval = pam_open_session(pamh,0);      /* Must take care to close */
	if (retval != PAM_SUCCESS)
	    break;

	/*
	 * We obtain all of the new credentials of the user.
	 */

	retval = set_user_credentials(pamh, login, &user, &uid, &shell);
	if (retval != PAM_SUCCESS) {
	    (void) pam_close_session(pamh,retval);
	    break;
	}

	/* determine who owns the terminal line */
	if (is_terminal) {
	    struct stat stat_buf;

	    if (fstat(STDIN_FILENO,&stat_buf)) {
		(void) pam_setcred(pamh, PAM_DELETE_CRED);
		(void) pam_close_session(pamh,retval);
		fprintf(stderr, "su: failed to identify terminal\n");
		retval = PAM_ABORT;
		break;
	    }
	    terminal_uid = stat_buf.st_uid;
	    fchown(STDIN_FILENO, uid, -1);
	} else {
	    terminal_uid = (uid_t) -1;
	}
	
#ifndef SU_STRICTLY_POSIX
	/*
	 * It's a time to make us unkillable by user.  Now only root
	 * can kill us. We suppose that root knows what he is doing.  (SAW)
	 */
	invoked_uid = getuid();
	setuid(0);
#endif /* SU_STRICTLY_POSIX */

	/* this is where we execute the user's shell */
	{
	    int fds[2];        /* this is a hack to avoid a race condition */

	    if (login && pipe(fds) == -1) {
		D(("failed to allocate a pipe."));
		retval = PAM_ABORT;
		break;
	    }

	    child = fork();
	    if (child == 0) {       /* child exec's shell */

		/* avoid the race condition here.. wait for parent. */
		if (login) {
		    char buf[1];

		    /* block until we get something */

		    close(fds[1]);
		    read(fds[0], buf, 1);
		    close(fds[0]);

		    /* The race condition is only insofar as the user does
		       something like getlogin() and the utmp file has not
		       been written yet (by the parent). */
		}

		su_exec_shell(shell, uid, login, command, user);
		exit_now(1, "su: shell failed to execute!?\n");
	    }

	    if (login) {
		char buf_c = '\0';

		utmp_open_session(pamh, child);  /* new logname of terminal */

		close(fds[0]);
		write(fds[1], &buf_c, 1);
		close(fds[1]);
	    }
	}

	/* permit child to stop parent */
	sigaction(SIGTSTP, &old_tstp_act, NULL);

	/* wait for child to terminate */

	status = wait_for_child(child, terminal_uid);
	if (status != 0) {
	    D(("shell returned %d", status));
	}

	/* return terminal to local control */
	if (is_terminal) {
	    fchown(STDIN_FILENO, terminal_uid, -1);
	}

	if (login) {
	    utmp_close_session(pamh, child);   /* retrieve original logname */
	}

	/*
	 * PAM expects real uid to be restored. Effective uid
	 * remains unchanged: superuser.  (SAW)
	 */
#ifndef SU_STRICTLY_POSIX
	setreuid(invoked_uid, -1);
#endif /* SU_STRICTLY_POSIX */

	D(("setcred"));
	/* Delete the user's credentials. */
	retval = pam_setcred(pamh, PAM_DELETE_CRED);
	if (retval != PAM_SUCCESS) {
	    fprintf(stderr, "WARNING: could not delete credentials\n\t%s\n"
		    , pam_strerror(pamh,retval));
	}

	D(("session"));
	D(("%p", pamh));

	/* close down */
	retval = pam_close_session(pamh,0);
	if (retval != PAM_SUCCESS)
	    break;

	/* clean up */
	D(("all done"));
	(void) pam_end(pamh,PAM_SUCCESS);
	pamh = NULL;

#ifdef HAVE_PWDB
	while ( pwdb_end() == PWDB_SUCCESS );
#endif

	/* reset the terminal */

	if (is_terminal
	    && tcsetattr(STDIN_FILENO, TCSAFLUSH, &stored_mode) != 0) {
	    fprintf(stderr, "su: cannot reset terminal mode\n");
	    exit(1);
	}
	exit(status);                /* transparent exit */
    }

    /*
     * reaching here means that an error has occured; mention any
     * error and then exit
     */

    if (pamh != NULL)
	(void) pam_end(pamh,retval);

    if (retval != PAM_SUCCESS)
	fprintf(stderr, "su: %s\n", pam_strerror(pamh,retval));

    /* Clean up terminal to cover case that user has broken it */
    if (is_terminal
	&& tcsetattr(STDIN_FILENO, TCSAFLUSH, &stored_mode) != 0) {
	fprintf(stderr,"su: cannot reset terminal mode\n");
    }

#ifdef HAVE_PWDB
    while (pwdb_end() == PWDB_SUCCESS);
#endif

    exit(1);
}
