root/usr/src/cmd/vt/vtdaemon.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
 *
 * Copyright 2023 OmniOS Community Edition (OmniOSce) Association.
 */

/*
 * vtdaemon is responsible for the session secure switch via hotkeys.
 *
 * vtdaemon itself, like ttymon(8), is also running on a virtual
 * console device (/dev/vt/1), and provides a text console session
 * for password input and authentication. The /dev/vt/1 special text
 * console is reserved and end users cannot switch to it via hotkeys.
 *
 *
 * The hotkey event request can come from either kernel or Xserver,
 * and a door server is setup to handle the request:
 *
 *   1) All text console hotkeys (e.g. "Alt + F#") are intercepted by
 *      the kernel console driver which sends a door upcall to the
 *      vtdaemon via door_upcall (target_vt).
 *
 *   2) All Xserver hotkeys ("Alt + Ctrl + F#") are intercepted by
 *      Xserver which sends a door call to the vtdaemon via
 *      door_call (target_vt).
 *
 *
 * server_for_door receives and handles any door server requests:
 *
 *   Firstly, check source session:
 *
 *      . If it's from kernel for a text console source session,
 *        then directly go to check the target session.
 *
 *      . If it's from Xserver for a graphical source session and the vt
 *        associated with the Xserver is currently active:
 *          check if a user has logged in, if true, issue an internal
 *          VT_EV_LOCK event to the main thread to request lock for
 *          the graphical source session; else, directly go to check
 *          the target session.
 *
 *      . otherwise, discard this request.
 *
 *
 *    Secondly, check the target session
 *
 *      . if the target session is a text one that no one has logged in
 *        or a graphical one, issue an internal VT_EV_ACTIVATE event to
 *        the main thread to request the actual VT switch.
 *
 *      . otherwise, the target session is a text one that someone has
 *        logged in, issue an internal VT_EV_AUTH event to the main
 *        thread to request authentication for the target session.
 *
 *
 * The main thread of vtdaemon is a loop waiting for internal events
 * which come from door call threads:
 *
 *   1)  VT_EV_AUTH      to authenticate for target session:
 *
 *                       firstly switch to the vtdaemon special text console;
 *                       then prompt for password (target_owner on target_vt),
 *                       e.g. "User Bob's password on vt/#: ".
 *
 *                       if the password is correct (authentication succeeds),
 *                       then actually issue the VT switch; otherwise, ignore
 *                       the request.
 *
 *   2)  VT_EV_LOCK      to lock the graphical source session:
 *
 *                       activate screenlock for this graphical session.
 *                       vtdaemon just invokes existing front-end command line
 *                       tools (e.g. xscreensaver-command -lock for JDS) to
 *                       lock the display.
 *
 *   3)  VT_EV_ACTIVATE  to directly switch to the target session
 *
 *
 * There is a system/vtdaemon:default SMF service for vtdaemon.
 *
 *      There's a "hotkeys" property (BOOLEAN) in the
 *      system/vtdaemon:default SMF service, which allows authorized
 *      users to dynamically enable or disable VT switch via hotkeys.
 *      Its default value is TRUE (enabled).
 *
 *      There's a "secure" property (BOOLEAN) in the
 *      system/vtdaemon:default SMF service, which allows authorized
 *      users to dynamically enable or disable hotkeys are secure.
 *      If disabled, the user can freely switch to any session without
 *      authentication. Its default value is TRUE (enabled).
 *
 *
 *  By default, there's only 16 virtual console device nodes (from
 *  /dev/vt/0 to /dev/vt/15). There's a property "nodecount"
 *  (default value is 16) in the system/vtdaemon:default SMF
 *  service, so authorized users can configure it to have more
 *  or less virtual console device nodes.
 *
 *  Xserver needs to switch back to previous active vt via VT_EV_X_EXIT
 *  door event request when it's exiting, so vtdaemon always needs to
 *  be there even if the hotkeys switch is disabled, otherwise the screen
 *  will be just blank when Xserver exits.
 */

#include <sys/param.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <syslog.h>
#include <deflt.h>

#include <bsm/adt.h>
#include <bsm/adt_event.h>

#include <alloca.h>
#include <assert.h>
#include <errno.h>
#include <door.h>
#include <fcntl.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <synch.h>
#include <thread.h>
#include <unistd.h>
#include <wait.h>
#include <limits.h>
#include <zone.h>
#include <priv.h>
#include <pwd.h>
#include <utmpx.h>
#include <procfs.h>
#include <poll.h>
#include <termio.h>
#include <security/pam_appl.h>
#include <time.h>
#include <sys/console.h>
#include <assert.h>
#include <syslog.h>

#include <sys/vt.h>
#include <sys/vtdaemon.h>

/*
 * The door file /var/run/vt/vtdaemon_door
 */
#define VT_TMPDIR "/var/run/vt"

#define VT_DAEMON_ARG   0
#define VT_DAEMON_CONSOLE_FILE  "/dev/vt/1"

#define VT_IS_SYSTEM_CONSOLE(vtno)      ((vtno) == 1)

/* Defaults for updating expired passwords */
#define DEF_ATTEMPTS    3

int daemonfd;

static boolean_t vt_hotkeys = B_TRUE;           /* '-k' option to disable */
static boolean_t vt_secure = B_TRUE;            /* '-s' option to disable */

static char     vt_door_path[MAXPATHLEN];
static int      vt_door = -1;

/* protecting vt_hotkeys_pending and vt_auth_doing */
static mutex_t  vt_mutex = DEFAULTMUTEX;

static boolean_t vt_hotkeys_pending = B_FALSE;
static boolean_t vt_auth_doing = B_FALSE;

static adt_session_data_t **vt_ah_array = NULL;
static int vtnodecount = 0;

static int vt_audit_start(adt_session_data_t **, pid_t);
static void vt_audit_event(adt_session_data_t *, au_event_t, int);
static void vt_check_source_audit(void);

static int
vt_setup_signal(int signo, int mask)
{
        sigset_t set;

        (void) sigemptyset(&set);
        (void) sigaddset(&set, signo);

        if (mask)
                return (sigprocmask(SIG_BLOCK, &set, NULL));
        else
                return (sigprocmask(SIG_UNBLOCK, &set, NULL));
}

static void
do_activate_screenlock(int display_num)
{
        char dpy[16];

        (void) snprintf(dpy, sizeof (dpy), "%d", display_num);
        (void) execl("/usr/lib/vtxlock", "vtxlock", dpy, NULL);
}

static void
vt_activate_screenlock(int display)
{
        pid_t pid;

        if ((pid = fork()) == -1)
                return;

        if (pid == 0) { /* child */
                do_activate_screenlock(display);
                exit(0);
        }

        /* parent */
        while (waitpid(pid, (int *)0, 0) != pid)
                continue;
}

/*
 * Find the login process and user logged in on the target vt.
 */
static void
vt_read_utx(int target_vt, pid_t *pid, char name[])
{
        struct utmpx  *u;
        char ttyntail[sizeof (u->ut_line)];

        *pid = (pid_t)-1;

        if (VT_IS_SYSTEM_CONSOLE(target_vt)) /* system console */
                (void) snprintf(ttyntail, sizeof (ttyntail),
                    "%s", "console");
        else
                (void) snprintf(ttyntail, sizeof (ttyntail),
                    "%s%d", "vt/", target_vt);

        setutxent();
        while ((u = getutxent()) != NULL)
                /* see if this is the entry we want */
                if ((u->ut_type == USER_PROCESS) &&
                    (!nonuserx(*u)) &&
                    (u->ut_host[0] == '\0') &&
                    (strncmp(u->ut_line, ttyntail, sizeof (u->ut_line)) == 0)) {

                        *pid = u->ut_pid;
                        if (name != NULL) {
                                (void) strncpy(name, u->ut_user,
                                    sizeof (u->ut_user));
                                name[sizeof (u->ut_user)] = '\0';
                        }
                        break;
                }

        endutxent();
}

static boolean_t
vt_is_tipline(void)
{
        static int is_tipline = 0;
        int fd;
        static char termbuf[MAX_TERM_TYPE_LEN];
        static struct cons_getterm cons_term = { sizeof (termbuf), termbuf};

        if (is_tipline != 0)
                return (is_tipline == 1);

        if ((fd = open("/dev/console", O_RDONLY)) < 0)
                return (B_FALSE);

        if (ioctl(fd, CONS_GETTERM, &cons_term) != 0 &&
            errno == ENODEV) {
                is_tipline = 1;
        } else {
                is_tipline = -1;
        }

        (void) close(fd);
        return (is_tipline == 1);
}

static int
validate_target_vt(int target_vt)
{
        int fd;
        struct vt_stat state;

        if (target_vt < 1)
                return (-1);

        if ((fd = open(VT_DAEMON_CONSOLE_FILE, O_WRONLY)) < 0)
                return (-1);

        if (ioctl(fd, VT_GETSTATE, &state) != 0) {
                (void) close(fd);
                return (-1);
        }

        (void) close(fd);

        if (state.v_active == target_vt) {
                return (1);     /* it's current active vt */
        }

        if (target_vt == 1) {
                /*
                 * In tipline case, the system console is always
                 * available, so ignore this request.
                 */
                if (vt_is_tipline())
                        return (-1);

                target_vt = 0;
        }

        /*
         * The hotkey request and corresponding target_vt number can come
         * from either kernel or Xserver (or other user applications).
         * In kernel we've validated the hotkey request, but Xserver (or
         * other user applications) cannot do it, so here we still try
         * to validate it.
         *
         * VT_GETSTATE is only valid for first 16 VTs for historical reasons.
         * Fortunately, in practice, Xserver can only send the hotkey
         * request of target_vt number from 1 to 12 (Ctrl + Alt + F1 to F2).
         */
        if (target_vt < 8 * sizeof (state.v_state)) {
                if ((state.v_state & (1 << target_vt)) != 0) {
                        return (0);
                } else {
                        return (-1);
                }
        }

        return (0);
}

static void
vt_do_activate(int target_vt)
{
        (void) ioctl(daemonfd, VT_ACTIVATE, target_vt);
        (void) mutex_lock(&vt_mutex);
        vt_hotkeys_pending = B_FALSE;
        (void) mutex_unlock(&vt_mutex);
}

/* events written to fd 0 and read from fd 1 */
#define VT_EV_AUTH      1
#define VT_EV_LOCK      2
#define VT_EV_ACTIVATE  3

/* events written to fd 1 and read from fd 0 */
#define VT_EV_TERMINATE_AUTH    4

typedef struct vt_evt {
        int     ve_cmd;
        int     ve_info;        /* vtno or display num */
} vt_evt_t;

static int eventstream[2];

boolean_t
eventstream_init(void)
{
        if (pipe(eventstream) == -1)
                return (B_FALSE);
        return (B_TRUE);
}

void
eventstream_write(int channel, vt_evt_t *pevt)
{
        (void) write(eventstream[channel], pevt, sizeof (vt_evt_t));
}

static boolean_t
eventstream_read(int channel, vt_evt_t *pevt)
{
        ssize_t rval;

        rval = read(eventstream[channel], pevt, sizeof (vt_evt_t));
        return (rval > 0);
}

static void
vt_ev_request(int cmd, int info)
{
        int channel;
        vt_evt_t ve;

        ve.ve_cmd = cmd;
        ve.ve_info = info;

        channel = (cmd == VT_EV_TERMINATE_AUTH) ? 1 : 0;
        eventstream_write(channel, &ve);
}

static void
vt_clear_events(void)
{
        int rval = 0;
        struct stat buf;
        vt_evt_t evt;

        while (rval == 0) {
                rval = fstat(eventstream[0], &buf);
                if (rval != -1 && buf.st_size > 0)
                        (void) eventstream_read(0, &evt);
                else
                        break;
        }
}

static int vt_conv(int, const struct pam_message **,
    struct pam_response **, void *);

/*ARGSUSED*/
static void
catch(int x)
{
        (void) signal(SIGINT, catch);
}

/*
 * The SIGINT (ctl_c) will restart the authentication, and re-prompt
 * the end user to input the password.
 */
static int
vt_poll()
{
        struct pollfd pollfds[2];
        vt_evt_t ve;
        int ret;

        pollfds[0].fd = eventstream[0];
        pollfds[1].fd = daemonfd;
        pollfds[0].events = pollfds[1].events =
            POLLIN | POLLRDNORM | POLLRDBAND | POLLPRI;

        for (;;) {
                pollfds[0].revents = pollfds[1].revents = 0;

                ret = poll(pollfds,
                    sizeof (pollfds) / sizeof (struct pollfd), -1);
                if (ret == -1 && errno != EINTR) {
                        continue;
                }

                if (ret == -1 && errno == EINTR)
                        return (-1);

                if (pollfds[0].revents) {
                        (void) eventstream_read(0, &ve);
                        return (0);
                }

                if (pollfds[1].revents)
                        return (1);

                return (0);

        }
}

static char
vt_getchar(int fd)
{
        char c;
        int cnt;

        cnt = read(fd, &c, 1);
        if (cnt > 0) {
                return (c);
        }

        return (EOF);
}

static char *
vt_getinput(int noecho)
{
        int c;
        int i = 0;
        struct termio tty;
        tcflag_t tty_flags;
        char input[PAM_MAX_RESP_SIZE];

        if (noecho) {
                (void) ioctl(daemonfd, TCGETA, &tty);
                tty_flags = tty.c_lflag;
                tty.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);
                (void) ioctl(daemonfd, TCSETAF, &tty);
        }

        while ((vt_poll()) == 1) {
                if ((c = vt_getchar(daemonfd)) != '\n' && c != '\r' &&
                    c != EOF && (i < PAM_MAX_RESP_SIZE - 1))
                        input[i++] = (char)c;
                else
                        break;
        }

        input[i] = '\0';

        if (noecho) {
                tty.c_lflag = tty_flags;
                (void) ioctl(daemonfd, TCSETAW, &tty);
                (void) fputc('\n', stdout);
        }

        return (strdup(input));
}

/*
 * vt_conv: vtdaemon PAM conversation function.
 * SIGINT/EINTR is handled in vt_getinput()/vt_poll().
 */

static int
vt_conv(int num_msg, const struct pam_message **msg,
    struct pam_response **response, void *appdata_ptr)
{
        const struct pam_message *m;
        struct pam_response *r;
        int i, k;

        if (num_msg >= PAM_MAX_NUM_MSG) {
                syslog(LOG_ERR, "too many messages %d >= %d",
                    num_msg, PAM_MAX_NUM_MSG);
                *response = NULL;
                return (PAM_CONV_ERR);
        }

        *response = calloc(num_msg, sizeof (struct pam_response));
        if (*response == NULL)
                return (PAM_BUF_ERR);

        m = *msg;
        r = *response;
        for (i = 0; i < num_msg; i++) {
                int echo_off = 0;

                /* Bad message */
                if (m->msg == NULL) {
                        syslog(LOG_ERR, "message[%d]: %d/NULL\n",
                            i, m->msg_style);
                        goto err;
                }

                /*
                 * Fix up final newline:
                 * remove from prompts, add back for messages.
                 */
                if (m->msg[strlen(m->msg)] == '\n')
                        m->msg[strlen(m->msg)] = '\0';

                r->resp = NULL;
                r->resp_retcode = 0;

                switch (m->msg_style) {

                case PAM_PROMPT_ECHO_OFF:
                        echo_off = 1;
                        /* FALLTHROUGH */

                case PAM_PROMPT_ECHO_ON:
                        (void) fputs(m->msg, stdout);

                        r->resp = vt_getinput(echo_off);
                        break;

                case PAM_ERROR_MSG:
                        /* the user may want to see this */
                        (void) fputs(m->msg, stdout);
                        (void) fputs("\n", stdout);
                        break;

                case PAM_TEXT_INFO:
                        (void) fputs(m->msg, stdout);
                        (void) fputs("\n", stdout);
                        break;

                default:
                        syslog(LOG_ERR, "message[%d]: unknown type"
                            "%d/val=\"%s\"", i, m->msg_style, m->msg);

                        /* error, service module won't clean up */
                        goto err;
                }

                /* Next message/response */
                m++;
                r++;

        }
        return (PAM_SUCCESS);

err:
        /*
         * Service modules don't clean up responses if an error is returned.
         * Free responses here.
         */
        r = *response;
        for (k = 0; k < i; k++, r++) {
                if (r->resp) {
                        /* Clear before freeing -- maybe a password */
                        bzero(r->resp, strlen(r->resp));
                        free(r->resp);
                        r->resp = NULL;
                }
        }

        free(*response);
        *response = NULL;
        return (PAM_CONV_ERR);
}

#define DEF_FILE        "/etc/default/login"

/* Get PASSREQ from default file */
static boolean_t
vt_default(void)
{
        int flags;
        char *ptr;
        boolean_t retval = B_FALSE;

        if ((defopen(DEF_FILE)) == 0) {
                /* ignore case */
                flags = defcntl(DC_GETFLAGS, 0);
                TURNOFF(flags, DC_CASE);
                (void) defcntl(DC_SETFLAGS, flags);

                if ((ptr = defread("PASSREQ=")) != NULL &&
                    strcasecmp("YES", ptr) == 0)
                        retval = B_TRUE;

                (void) defopen(NULL);
        }

        return (retval);
}

/*
 * VT_CLEAR_SCREEN_STR is the console terminal escape sequence used to
 * clear the current screen.  The vt special console (/dev/vt/1) is
 * just reserved for vtdaemon, and the TERM/termcap of it is always
 * the local sun-color, which is always supported by our kernel terminal
 * emulator.
 */
#define VT_CLEAR_SCREEN_STR     "\033[2J\033[1;1H"

static void
vt_do_auth(int target_vt)
{
        char    user_name[sizeof (((struct utmpx *)0)->ut_line) + 1] = {'\0'};
        pam_handle_t    *vt_pamh;
        int             err;
        int             pam_flag = 0;
        int             chpasswd_tries;
        struct pam_conv pam_conv = {vt_conv, NULL};
        pid_t           pid;
        adt_session_data_t      *ah;

        vt_read_utx(target_vt, &pid, user_name);

        if (pid == (pid_t)-1 || user_name[0] == '\0')
                return;

        if ((err = pam_start("vtdaemon", user_name, &pam_conv,
            &vt_pamh)) != PAM_SUCCESS)
                return;

        /*
         * firstly switch to the vtdaemon special console
         * and clear the current screen
         */
        (void) ioctl(daemonfd, VT_ACTIVATE, VT_DAEMON_ARG);
        (void) write(daemonfd, VT_CLEAR_SCREEN_STR,
            strlen(VT_CLEAR_SCREEN_STR));
        (void) ioctl(daemonfd, VT_SET_TARGET, target_vt);

        (void) mutex_lock(&vt_mutex);
        vt_auth_doing = B_TRUE;
        vt_hotkeys_pending = B_FALSE;
        (void) mutex_unlock(&vt_mutex);

        /*
         * Fetch audit handle.
         */
        ah = vt_ah_array[target_vt - 1];

        if (vt_default())
                pam_flag = PAM_DISALLOW_NULL_AUTHTOK;

        do {
                if (VT_IS_SYSTEM_CONSOLE(target_vt))
                        (void) fprintf(stdout,
                            "\nUnlock user %s on the system console\n",
                            user_name);
                else
                        (void) fprintf(stdout,
                            "\nUnlock user %s on vt/%d\n", user_name,
                            target_vt);

                err = pam_authenticate(vt_pamh, pam_flag);

                (void) mutex_lock(&vt_mutex);
                if (vt_hotkeys_pending) {
                        (void) mutex_unlock(&vt_mutex);
                        break;
                }
                (void) mutex_unlock(&vt_mutex);

                if (err == PAM_SUCCESS) {
                        err = pam_acct_mgmt(vt_pamh, pam_flag);

                        (void) mutex_lock(&vt_mutex);
                        if (vt_hotkeys_pending) {
                                (void) mutex_unlock(&vt_mutex);
                                break;
                        }
                        (void) mutex_unlock(&vt_mutex);

                        if (err == PAM_NEW_AUTHTOK_REQD) {
                                chpasswd_tries = 0;

                                do {
                                        err = pam_chauthtok(vt_pamh,
                                            PAM_CHANGE_EXPIRED_AUTHTOK);
                                        chpasswd_tries++;

                                        (void) mutex_lock(&vt_mutex);
                                        if (vt_hotkeys_pending) {
                                                (void) mutex_unlock(&vt_mutex);
                                                break;
                                        }
                                        (void) mutex_unlock(&vt_mutex);

                                } while ((err == PAM_AUTHTOK_ERR ||
                                    err == PAM_TRY_AGAIN) &&
                                    chpasswd_tries < DEF_ATTEMPTS);

                                (void) mutex_lock(&vt_mutex);
                                if (vt_hotkeys_pending) {
                                        (void) mutex_unlock(&vt_mutex);
                                        break;
                                }
                                (void) mutex_unlock(&vt_mutex);

                                vt_audit_event(ah, ADT_passwd, err);
                        }
                }

                /*
                 * Only audit failed unlock here, successful unlock
                 * will be audited after switching to target vt.
                 */
                if (err != PAM_SUCCESS) {
                        (void) fprintf(stdout, "%s",
                            pam_strerror(vt_pamh, err));

                        vt_audit_event(ah, ADT_screenunlock, err);
                }

                (void) mutex_lock(&vt_mutex);
                if (vt_hotkeys_pending) {
                        (void) mutex_unlock(&vt_mutex);
                        break;
                }
                (void) mutex_unlock(&vt_mutex);

        } while (err != PAM_SUCCESS);

        (void) mutex_lock(&vt_mutex);
        if (!vt_hotkeys_pending) {
                /*
                 * Should be PAM_SUCCESS to reach here.
                 */
                (void) ioctl(daemonfd, VT_ACTIVATE, target_vt);

                vt_audit_event(ah, ADT_screenunlock, err);

                /*
                 * Free audit handle.
                 */
                (void) adt_end_session(ah);
                vt_ah_array[target_vt - 1] = NULL;
        }
        (void) mutex_unlock(&vt_mutex);

        (void) pam_end(vt_pamh, err);

        (void) mutex_lock(&vt_mutex);
        vt_auth_doing = B_FALSE;
        vt_clear_events();
        (void) mutex_unlock(&vt_mutex);
}

/* main thread (lock and auth) */
static void __NORETURN
vt_serve_events(void)
{
        struct pollfd pollfds[1];
        int ret;
        vt_evt_t ve;

        pollfds[0].fd = eventstream[1];
        pollfds[0].events = POLLIN | POLLRDNORM | POLLRDBAND | POLLPRI;

        for (;;) {
                pollfds[0].revents = 0;
                ret = poll(pollfds,
                    sizeof (pollfds) / sizeof (struct pollfd), -1);
                if (ret == -1 && errno == EINTR) {
                        continue;
                }

                if (pollfds[0].revents && eventstream_read(1, &ve)) {
                        /* new request */
                        switch (ve.ve_cmd) {
                        case VT_EV_AUTH:
                                vt_do_auth(ve.ve_info);
                                break;

                        case VT_EV_LOCK:
                                vt_activate_screenlock(ve.ve_info);
                                break;

                        case VT_EV_ACTIVATE:
                                /* directly activate target vt */
                                vt_do_activate(ve.ve_info);
                                break;
                        }
                }
        }
}

static void
vt_check_target_session(uint32_t target_vt)
{
        pid_t   pid = (pid_t)-1;

        if (!vt_secure) {
                vt_ev_request(VT_EV_ACTIVATE, target_vt);
                return;
        }

        /* check the target session */
        vt_read_utx(target_vt, &pid, NULL);
        if (pid  == (pid_t)-1) {
                vt_ev_request(VT_EV_ACTIVATE, target_vt);
                return;
        }

        vt_ev_request(VT_EV_AUTH, target_vt);
}

static boolean_t
vt_get_active_disp_info(struct vt_dispinfo *vd)
{
        int fd;
        struct vt_stat state;
        char vtname[16];

        if ((fd = open(VT_DAEMON_CONSOLE_FILE, O_RDONLY)) < 0)
                return (B_FALSE);

        if (ioctl(fd, VT_GETSTATE, &state) != 0) {
                (void) close(fd);
                return (B_FALSE);
        }
        (void) close(fd);

        (void) snprintf(vtname, sizeof (vtname), "/dev/vt/%d", state.v_active);
        if ((fd = open(vtname, O_RDONLY)) < 0)
                return (B_FALSE);

        if (ioctl(fd, VT_GETDISPINFO, vd) != 0) {
                (void) close(fd);
                return (B_FALSE);
        }

        (void) close(fd);
        return (B_TRUE);
}

/*
 * Xserver registers its pid into kernel to associate it with
 * its vt upon startup for each graphical display. So here we can
 * check if the pid is of the Xserver for the current active
 * display when we receive a special VT_EV_X_EXIT request from
 * a process. If the request does not come from the current
 * active Xserver, it is discarded.
 */
static boolean_t
vt_check_disp_active(pid_t x_pid)
{
        struct vt_dispinfo vd;

        if (vt_get_active_disp_info(&vd) &&
            vd.v_pid == x_pid)
                return (B_TRUE);

        return (B_FALSE);
}

/*
 * check if the pid is of the Xserver for the current active display,
 * return true when it is, and then also return other associated
 * information with the Xserver.
 */
static boolean_t
vt_get_disp_info(pid_t x_pid, int *logged_in, int *display_num)
{
        struct vt_dispinfo vd;

        if (!vt_get_active_disp_info(&vd) ||
            vd.v_pid != x_pid)
                return (B_FALSE);

        *logged_in = vd.v_login;
        *display_num = vd.v_dispnum;
        return (B_TRUE);
}

static void
vt_terminate_auth(void)
{
        struct timespec sleeptime;

        sleeptime.tv_sec = 0;
        sleeptime.tv_nsec = 1000000; /* 1ms */

        (void) mutex_lock(&vt_mutex);
        while (vt_auth_doing) {
                vt_ev_request(VT_EV_TERMINATE_AUTH, 0);

                if (vt_auth_doing) {
                        (void) mutex_unlock(&vt_mutex);
                        (void) nanosleep(&sleeptime, NULL);
                        sleeptime.tv_nsec *= 2;
                        (void) mutex_lock(&vt_mutex);
                }
        }
        (void) mutex_unlock(&vt_mutex);
}

static void
vt_do_hotkeys(pid_t pid, uint32_t target_vt)
{
        int             logged_in;
        int             display_num;

        if (validate_target_vt(target_vt) != 0)
                return;

        /*
         * Maybe last switch action is being taken and the lock is ongoing,
         * here we must reject the newly request.
         */
        (void) mutex_lock(&vt_mutex);
        if (vt_hotkeys_pending) {
                (void) mutex_unlock(&vt_mutex);
                return;
        }

        /* cleared in vt_do_active and vt_do_auth */
        vt_hotkeys_pending = B_TRUE;
        (void) mutex_unlock(&vt_mutex);

        vt_terminate_auth();

        /* check source session for this hotkeys request */
        if (pid == 0) {
                /* ok, it comes from kernel. */
                if (vt_secure)
                        vt_check_source_audit();

                /* then only need to check target session */
                vt_check_target_session(target_vt);
                return;
        }

        /*
         * check if it comes from current active X graphical session,
         * if not, ignore this request.
         */
        if (!vt_get_disp_info(pid, &logged_in, &display_num)) {
                (void) mutex_lock(&vt_mutex);
                vt_hotkeys_pending = B_FALSE;
                (void) mutex_unlock(&vt_mutex);
                return;
        }

        if (logged_in && vt_secure)
                vt_ev_request(VT_EV_LOCK, display_num);

        vt_check_target_session(target_vt);
}

/*
 * The main routine for the door server that deals with secure hotkeys
 */
/* ARGSUSED */
static void
server_for_door(void *cookie, char *args, size_t alen, door_desc_t *dp,
    uint_t n_desc)
{
        ucred_t *uc = NULL;
        vt_cmd_arg_t *vtargp;

        /* LINTED E_BAD_PTR_CAST_ALIGN */
        vtargp = (vt_cmd_arg_t *)args;

        if (vtargp == NULL ||
            alen != sizeof (vt_cmd_arg_t) ||
            door_ucred(&uc) != 0) {
                (void) door_return(NULL, 0, NULL, 0);
                return;
        }

        switch (vtargp->vt_ev) {
        case VT_EV_X_EXIT:
                /*
                 * Xserver will issue this event requesting to switch back
                 * to previous active vt when it's exiting and the associated
                 * vt is currently active.
                 */
                if (vt_check_disp_active(ucred_getpid(uc)))
                        vt_do_hotkeys(0, vtargp->vt_num);
                break;

        case VT_EV_HOTKEYS:
                if (!vt_hotkeys)        /* hotkeys are disabled? */
                        break;

                vt_do_hotkeys(ucred_getpid(uc), vtargp->vt_num);
                break;

        default:
                break;
        }

        ucred_free(uc);
        (void) door_return(NULL, 0, NULL, 0);
}

static boolean_t
setup_door(void)
{
        if ((vt_door = door_create(server_for_door, NULL,
            DOOR_UNREF | DOOR_REFUSE_DESC | DOOR_NO_CANCEL)) < 0) {
                syslog(LOG_ERR, "door_create failed: %s", strerror(errno));
                return (B_FALSE);
        }

        (void) fdetach(vt_door_path);

        if (fattach(vt_door, vt_door_path) != 0) {
                syslog(LOG_ERR, "fattach to %s failed: %s",
                    vt_door_path, strerror(errno));
                (void) door_revoke(vt_door);
                (void) fdetach(vt_door_path);
                vt_door = -1;
                return (B_FALSE);
        }

        return (B_TRUE);
}

/*
 * check to see if vtdaemon is already running.
 *
 * The idea here is that we want to open the path to which we will
 * attach our door, lock it, and then make sure that no-one has beat us
 * to fattach(3c)ing onto it.
 *
 * fattach(3c) is really a mount, so there are actually two possible
 * vnodes we could be dealing with.  Our strategy is as follows:
 *
 * - If the file we opened is a regular file (common case):
 *      There is no fattach(3c)ed door, so we have a chance of becoming
 *      the running vtdaemon. We attempt to lock the file: if it is
 *      already locked, that means someone else raced us here, so we
 *      lose and give up.
 *
 * - If the file we opened is a namefs file:
 *      This means there is already an established door fattach(3c)'ed
 *      to the rendezvous path.  We've lost the race, so we give up.
 *      Note that in this case we also try to grab the file lock, and
 *      will succeed in acquiring it since the vnode locked by the
 *      "winning" vtdaemon was a regular one, and the one we locked was
 *      the fattach(3c)'ed door node.  At any rate, no harm is done.
 */
static boolean_t
make_daemon_exclusive(void)
{
        int doorfd = -1;
        boolean_t ret = B_FALSE;
        struct stat st;
        struct flock flock;

top:
        if ((doorfd = open(vt_door_path, O_CREAT|O_RDWR,
            S_IREAD|S_IWRITE|S_IRGRP|S_IROTH)) < 0) {
                syslog(LOG_ERR, "failed to open %s", vt_door_path);
                goto out;
        }
        if (fstat(doorfd, &st) < 0) {
                syslog(LOG_ERR, "failed to stat %s", vt_door_path);
                goto out;
        }
        /*
         * Lock the file to synchronize
         */
        flock.l_type = F_WRLCK;
        flock.l_whence = SEEK_SET;
        flock.l_start = (off_t)0;
        flock.l_len = (off_t)0;
        if (fcntl(doorfd, F_SETLK, &flock) < 0) {
                /*
                 * Someone else raced us here and grabbed the lock file
                 * first.  A warning here and exit.
                 */
                syslog(LOG_ERR, "vtdaemon is already running!");
                goto out;
        }

        if (strcmp(st.st_fstype, "namefs") == 0) {
                struct door_info info;

                /*
                 * There is already something fattach()'ed to this file.
                 * Lets see what the door is up to.
                 */
                if (door_info(doorfd, &info) == 0 && info.di_target != -1) {
                        syslog(LOG_ERR, "vtdaemon is already running!");
                        goto out;
                }

                (void) fdetach(vt_door_path);
                (void) close(doorfd);
                goto top;
        }

        ret = setup_door();

out:
        (void) close(doorfd);
        return (ret);
}

static boolean_t
mkvtdir(void)
{
        struct stat st;
        /*
         * We must create and lock everyone but root out of VT_TMPDIR
         * since anyone can open any UNIX domain socket, regardless of
         * its file system permissions.
         */
        if (mkdir(VT_TMPDIR, S_IRWXU|S_IROTH|S_IXOTH|S_IRGRP|S_IXGRP) < 0 &&
            errno != EEXIST) {
                syslog(LOG_ERR, "could not mkdir '%s'", VT_TMPDIR);
                return (B_FALSE);
        }
        /* paranoia */
        if ((stat(VT_TMPDIR, &st) < 0) || !S_ISDIR(st.st_mode)) {
                syslog(LOG_ERR, "'%s' is not a directory", VT_TMPDIR);
                return (B_FALSE);
        }
        (void) chmod(VT_TMPDIR, S_IRWXU|S_IROTH|S_IXOTH|S_IRGRP|S_IXGRP);
        return (B_TRUE);
}

int
main(int argc, char *argv[])
{
        int i;
        int opt;
        priv_set_t *privset;
        int active;

        openlog("vtdaemon", LOG_PID | LOG_CONS, 0);

        /*
         * Check that we have all privileges.  It would be nice to pare
         * this down, but this is at least a first cut.
         */
        if ((privset = priv_allocset()) == NULL) {
                syslog(LOG_ERR, "priv_allocset failed");
                return (1);
        }

        if (getppriv(PRIV_EFFECTIVE, privset) != 0) {
                syslog(LOG_ERR, "getppriv failed", "getppriv");
                priv_freeset(privset);
                return (1);
        }

        if (priv_isfullset(privset) == B_FALSE) {
                syslog(LOG_ERR, "You lack sufficient privilege "
                    "to run this command (all privs required)");
                priv_freeset(privset);
                return (1);
        }
        priv_freeset(privset);

        while ((opt = getopt(argc, argv, "ksrc:")) != EOF) {
                switch (opt) {
                case 'k':
                        vt_hotkeys = B_FALSE;
                        break;
                case 's':
                        vt_secure = B_FALSE;
                        break;
                case 'c':
                        vtnodecount = atoi(optarg);
                        break;
                default:
                        break;
                }
        }

        (void) vt_setup_signal(SIGINT, 1);

        if (!mkvtdir())
                return (1);

        if (!eventstream_init())
                return (1);

        (void) snprintf(vt_door_path, sizeof (vt_door_path),
            VT_TMPDIR "/vtdaemon_door");

        if (!make_daemon_exclusive())
                return (1);

        /* only the main thread accepts SIGINT */
        (void) vt_setup_signal(SIGINT, 0);
        (void) sigset(SIGPIPE, SIG_IGN);
        (void) signal(SIGQUIT, SIG_IGN);
        (void) signal(SIGINT, catch);

        for (i = 0; i < 3; i++)
                (void) close(i);
        (void) setsid();

        if ((daemonfd = open(VT_DAEMON_CONSOLE_FILE, O_RDWR)) < 0) {
                return (1);
        }

        if (daemonfd != 0)
                (void) dup2(daemonfd, STDIN_FILENO);
        if (daemonfd != 1)
                (void) dup2(daemonfd, STDOUT_FILENO);

        if (vtnodecount >= 2)
                (void) ioctl(daemonfd, VT_CONFIG, vtnodecount);

        if ((vt_ah_array = calloc(vtnodecount - 1,
            sizeof (adt_session_data_t *))) == NULL)
                return (1);

        (void) ioctl(daemonfd, VT_GETACTIVE, &active);

        if (active == 1) {
                /*
                 * This is for someone who restarts vtdaemon while vtdaemon
                 * is doing authentication on /dev/vt/1.
                 * A better way is to continue the authentication, but there
                 * are chances that the status of the target VT has changed.
                 * So we just clear the screen here.
                 */
                (void) write(daemonfd, VT_CLEAR_SCREEN_STR,
                    strlen(VT_CLEAR_SCREEN_STR));
        }

        vt_serve_events();
        /*NOTREACHED*/
}

static int
vt_audit_start(adt_session_data_t **ah, pid_t pid)
{
        ucred_t *uc;

        if (adt_start_session(ah, NULL, 0))
                return (-1);

        if ((uc = ucred_get(pid)) == NULL) {
                (void) adt_end_session(*ah);
                return (-1);
        }

        if (adt_set_from_ucred(*ah, uc, ADT_NEW)) {
                ucred_free(uc);
                (void) adt_end_session(*ah);
                return (-1);
        }

        ucred_free(uc);
        return (0);
}

/*
 * Write audit event
 */
static void
vt_audit_event(adt_session_data_t *ah, au_event_t event_id, int status)
{
        adt_event_data_t        *event;


        if ((event = adt_alloc_event(ah, event_id)) == NULL) {
                return;
        }

        (void) adt_put_event(event,
            status == PAM_SUCCESS ? ADT_SUCCESS : ADT_FAILURE,
            status == PAM_SUCCESS ? ADT_SUCCESS : ADT_FAIL_PAM + status);

        adt_free_event(event);
}

static void
vt_check_source_audit(void)
{
        int     fd;
        int     source_vt;
        int     real_vt;
        struct vt_stat state;
        pid_t   pid;
        adt_session_data_t *ah;

        if ((fd = open(VT_DAEMON_CONSOLE_FILE, O_WRONLY)) < 0)
                return;

        if (ioctl(fd, VT_GETSTATE, &state) != 0 ||
            ioctl(fd, VT_GETACTIVE, &real_vt) != 0) {
                (void) close(fd);
                return;
        }

        source_vt = state.v_active;     /* 1..n */
        (void) close(fd);

        /* check if it's already locked */
        if (real_vt == 1)       /* vtdaemon is taking over the screen */
                return;

        vt_read_utx(source_vt, &pid, NULL);
        if (pid == (pid_t)-1)
                return;

        if (vt_audit_start(&ah, pid) != 0) {
                syslog(LOG_ERR, "audit start failed ");
                return;
        }

        /*
         * In case the previous session terminated abnormally.
         */
        if (vt_ah_array[source_vt - 1] != NULL)
                (void) adt_end_session(vt_ah_array[source_vt - 1]);

        vt_ah_array[source_vt - 1] = ah;

        vt_audit_event(ah, ADT_screenlock, PAM_SUCCESS);
}