/* Copyright 1999, 2005 Red Hat, Inc. * This software may be used under the terms of the GNU General Public * License, available in the file COPYING accompanying this file. * * /var/run/console/console.lock is the file used to control access to * devices. It is created when the first console user logs in, * and that user has the control of the console until they have * logged out of all concurrent login sessions. That is, * user A logs in on console 1 (gets access to console devices) * user B logs in on console 2 (does not get access) * user A logs in on console 3 (already has access) * user A logs out of console 1 (still has access on console 3) * user A logs out of console 3 (access revoked; user B does NOT get access) * Note that all console users (both A and B in this situation) * should be able to run console access programs (that is, * pam_sm_authenticate() should return PAM_SUCCESS) even if * console access to files/devices is not available to any one of * the users (B in this case). * * /var/run/console/ is used for reference counting * and to make console authentication easy -- if it exists, then * is logged on console. * * A system startup script should remove /var/run/console/console.lock * and everything in /var/run/console/ */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "pam_console.h" #include "handlers.h" #include #include #include #include /* In order to avoid errors in pam_get_item(), we need a very * unfortunate cast. This is a terrible design error in PAM * that Linux-PAM slavishly follows. :-( */ #define CAST_ME_HARDER (const void**) static char consolelock[] = LOCKDIR "/" LOCKFILE; static char consolerefs[] = LOCKDIR "/"; static char consoleapps[] = "/etc/security/console.apps/"; static char consolehandlers[PATH_MAX] = "/etc/security/console.handlers"; static int configfileparsed = 0; static int debug = 0; static int allow_nonroot_tty = 0; /* some syslogging */ void _pam_log(pam_handle_t *pamh, int err, int debug_p, const char *format, ...) { va_list args; if (debug_p && !debug) return; va_start(args, format); pam_vsyslog(pamh, err, format, args); closelog(); } static void * _do_malloc(size_t req) { void *ret; ret = malloc(req); if (!ret) abort(); return ret; } static void _args_parse(pam_handle_t *pamh, int argc, const char **argv) { for (; argc-- > 0; ++argv) { if (!strcmp(*argv,"debug")) debug = 1; else if (!strcmp(*argv,"allow_nonroot_tty")) allow_nonroot_tty = 1; else if (!strncmp(*argv,"handlersfile=",13)) if (strlen(*argv+13) < PATH_MAX) strcpy(consolehandlers,*argv+13); else _pam_log(pamh, LOG_ERR, FALSE, "_args_parse: handlersfile filename too long"); else { _pam_log(pamh, LOG_ERR, FALSE, "_args_parse: unknown option; %s",*argv); } } } static int is_root(pam_handle_t *pamh, const char *username) { /* this should correspond to suser() in the kernel, since the * whole point of this is to avoid doing unnecessary file ops */ struct passwd *pwd; pwd = pam_modutil_getpwnam(pamh, username); if (pwd == NULL) { _pam_log(pamh, LOG_ERR, FALSE, "getpwnam failed for %s", username); return 0; } return !pwd->pw_uid; } static int check_one_console_name(const char *name, const char *cregex) { regex_t p; int r_err; char *class_exp; class_exp = _do_malloc(strlen(cregex) + 3); sprintf(class_exp, "^%s$", cregex); r_err = regcomp(&p, class_exp, REG_EXTENDED|REG_NOSUB); if (r_err) do_regerror(r_err, &p); r_err = regexec(&p, name, 0, NULL, 0); regfree(&p); free (class_exp); return !r_err; } static int try_xsocket(const char *path, size_t len) { int fd; union { struct sockaddr sa; struct sockaddr_un su; } addr; fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) return 0; memset(&addr, 0, sizeof(addr)); addr.su.sun_family = AF_UNIX; if (len > sizeof(addr.su.sun_path)) return 0; memcpy(addr.su.sun_path, path, len); if (connect(fd, &addr.sa, sizeof(addr.su) - (sizeof(addr.su.sun_path) - len)) == 0) { close(fd); return 1; } close(fd); return 0; } static int check_console_name(pam_handle_t *pamh, const char *consolename, int nonroot_ok, int on_set) { int found = 0; int statted = 0; struct stat st; char full_path[PATH_MAX]; const char *consoleregex; _pam_log(pamh, LOG_DEBUG, TRUE, "check console %s", consolename); if ((consoleregex = console_get_regexes()) == NULL) { /* probably a broken configuration */ _pam_log(pamh, LOG_INFO, FALSE, "no console regexes in console.handlers config"); return 0; } for (; *consoleregex != '\0'; consoleregex += strlen(consoleregex)+1) { if (check_one_console_name(consolename, consoleregex)) { found = 1; break; } } if (!found) { /* not found */ _pam_log(pamh, LOG_INFO, TRUE, "no matching console regex found"); return 0; } /* add some policy here -- not really the PAM way of doing things, but it gives us an extra measure of security in case of misconfiguration */ memset(&st, 0, sizeof(st)); statted = 0; _pam_log(pamh, LOG_DEBUG, TRUE, "checking possible console \"%s\"", consolename); if (lstat(consolename, &st) != -1) { statted = 1; } if (!statted) { strcpy(full_path, "/dev/"); strncat(full_path, consolename, sizeof(full_path) - 1 - strlen(full_path)); full_path[sizeof(full_path) - 1] = '\0'; _pam_log(pamh, LOG_DEBUG, TRUE, "checking possible console \"%s\"", full_path); if (lstat(full_path, &st) != -1) { statted = 1; } } if (!statted && (consolename[0] == ':')) { int l; char *dot = NULL; char *path = full_path + 1; full_path[0] = '\0'; strcpy(path, "/tmp/.X11-unix/X"); l = sizeof(full_path) - 2 - strlen(path); dot = strchr(consolename + 1, '.'); if (dot != NULL) { l = (l < dot - consolename - 1) ? l : dot - consolename - 1; } strncat(path, consolename + 1, l); full_path[sizeof(full_path) - 1] = '\0'; _pam_log(pamh, LOG_DEBUG, TRUE, "checking possible X socket \"%s\"", path); /* this will work because st.st_uid is 0 */ if (try_xsocket(full_path, strlen(path)+1)) { statted = 1; } else if (try_xsocket(path, strlen(path))) { statted = 1; } else if (!on_set) { /* there is no X11 socket in case of X11 crash */ _pam_log(pamh, LOG_DEBUG, TRUE, "can't find X11 socket to examine for %s probably due to X crash", consolename); statted = 1; } } if (statted) { int ok = 0; if (st.st_uid == 0) { _pam_log(pamh, LOG_DEBUG, TRUE, "console %s is owned by UID 0", consolename); ok = 1; } if (S_ISCHR(st.st_mode)) { _pam_log(pamh, LOG_DEBUG, TRUE, "console %s is a character device", consolename); ok = 1; } if (!ok && !nonroot_ok) { _pam_log(pamh, LOG_INFO, TRUE, "%s is not a valid console device because it is owned by UID %d and the allow_nonroot flag was not set", consolename, st.st_uid); found = 0; } } else { _pam_log(pamh, LOG_INFO, TRUE, "can't find device or X11 socket to examine for %s", consolename); found = 0; } if (found) return 1; /* not found */ _pam_log(pamh, LOG_INFO, TRUE, "did not find console %s", consolename); return 0; } static int lock_console(pam_handle_t *pamh, const char *id) { int fd, ret_val; fd = open(consolelock, O_RDWR|O_CREAT|O_EXCL, 0600); if (fd < 0) { _pam_log(pamh, LOG_INFO, TRUE, "console file lock already in place %s", consolelock); return -1; } ret_val = pam_modutil_write (fd, id, strlen(id)); if (ret_val == -1) { close(fd); } else { ret_val = close(fd); } if (ret_val == -1) { unlink(consolelock); return -1; } return 0; } /* warning, the following function uses goto for error recovery. * If you can't stand goto, don't read this function. :-P */ static int use_count(pam_handle_t *pamh, char *filename, int increment, int delete) { int fd, err, val; static int cache_fd = 0; struct stat st; struct flock lockinfo; char *buf = NULL; if (cache_fd) { fd = cache_fd; cache_fd = 0; /* the cached fd is always already locked */ } else { top: fd = open(filename, O_RDWR|O_CREAT, 0600); if (fd < 0) { _pam_log(pamh, LOG_ERR, FALSE, "Could not open lock file %s, disallowing console access", filename); return -1; } lockinfo.l_type = F_WRLCK; lockinfo.l_whence = SEEK_SET; lockinfo.l_start = 0; lockinfo.l_len = 0; alarm(20); /* FIXME: what if caller has sigalrm masked? */ err = fcntl(fd, F_SETLKW, &lockinfo); alarm(0); if (err == EAGAIN) { /* if someone has locked the file and not written to it in * at least 20 seconds, we assume they either forgot to unlock * it or are catatonic -- chances are slim that they are in * the middle of a read-write cycle and I don't want to make * us lock users out. Perhaps I should just return PAM_SUCCESS * instead and log the event? Kill the process holding the * lock? Options abound... For now, we ignore it. */ fcntl(fd, F_GETLK, &lockinfo); /* now lockinfo.l_pid == 0 implies that the lock was released * by the other process between returning from the 20 second * wait and calling fcntl again, not likely to ever happen, and * not a problem other than cosmetics even if it does. */ _pam_log(pamh, LOG_ERR, FALSE, "ignoring stale lock on file %s by process %d", filename, lockinfo.l_pid); } /* it is possible at this point that the file has been removed * by a previous login; if this happens, we need to start over. * Unfortunately, the only way to do this without potential stack * trashing is a goto. */ if (access (filename, F_OK) < 0) { close (fd); goto top; } } if (fstat (fd, &st)) { _pam_log(pamh, LOG_ERR, FALSE, "\"impossible\" fstat error on open fd for %s", filename); err = -1; goto return_error; } buf = _do_malloc(st.st_size+2); /* size will never grow by more than one */ if (st.st_size) { buf[0] = '\0'; /* if read returns eof, need atoi to give us 0 */ if (pam_modutil_read (fd, buf, st.st_size) == -1) { _pam_log(pamh, LOG_ERR, FALSE, "\"impossible\" read error on %s", filename); err = -1; goto return_error; } if (lseek(fd, 0, SEEK_SET) == -1) { _pam_log(pamh, LOG_ERR, FALSE, "\"impossible\" lseek error on %s", filename); err = -1; goto return_error; } buf[st.st_size] = '\0'; val = atoi(buf); } else { val = 0; } if (increment) { /* increment == 0 implies query */ val += increment; if (val <= 0 && delete) { if (unlink (filename)) { _pam_log(pamh, LOG_ERR, FALSE, "\"impossible\" unlink error on %s", filename); err = -1; goto return_error; } err = 0; goto return_error; } sprintf(buf, "%d", val); if (pam_modutil_write(fd, buf, strlen(buf)) == -1) { _pam_log(pamh, LOG_ERR, FALSE, "\"impossible\" write error on %s", filename); err = -1; goto return_error; } } err = val; if (!increment) { cache_fd = fd; } else { return_error: close (fd); } if (buf) free (buf); return err; } PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags UNUSED, int argc, const char **argv) { /* getuid() must return an id that maps to a username as a filename in * /var/run/console/ * and the service name must be listed in * /etc/security/console-apps */ struct passwd *pwd; char *lockfile = NULL; char *appsfile = NULL; const char *service; int ret = PAM_AUTH_ERR; D(("called.")); _args_parse(pamh, argc, argv); if (getuid() == 0) { /* Obtain user name by pam_get_user() . * We must make sure that the user sits on the local console */ const char *user = NULL; const char *host = NULL; const char *user_prompt; D(("invoked under root.")); ret = pam_get_item(pamh, PAM_RHOST, (const void **) &host); if (ret == PAM_SUCCESS && host && *host) { _pam_log(pamh, LOG_ERR, TRUE, "PAM_RHOST is set - not invoked from console."); return PAM_AUTH_ERR; } D(("Obtain user name.")); if (pam_get_item(pamh, PAM_USER_PROMPT, (const void **) &user_prompt) != PAM_SUCCESS) { user_prompt = "login: "; } ret = pam_get_user(pamh, &user, user_prompt); if (ret != PAM_SUCCESS) { _pam_log(pamh, LOG_ERR, FALSE, "could not obtain user name"); return ret; } pwd = pam_modutil_getpwnam(pamh, user); if (pwd == NULL) { _pam_log(pamh, LOG_ERR, FALSE, "user '%s' unknown for this system", user); return PAM_AUTH_ERR; } if (pwd->pw_uid == 0) { _pam_log(pamh, LOG_ERR, TRUE, "user '%s' is not allowed to " "authenticate by pam_console", pwd->pw_name); return PAM_AUTH_ERR; } } else { pwd = pam_modutil_getpwuid(pamh, getuid()); if (pwd == NULL) { _pam_log(pamh, LOG_ERR, FALSE, "user with id %d not found", getuid()); return PAM_AUTH_ERR; } } lockfile = _do_malloc(strlen(consolerefs) + strlen(pwd->pw_name) + 2); sprintf(lockfile, "%s%s", consolerefs, pwd->pw_name); /* trusted data */ pam_get_item(pamh, PAM_SERVICE, CAST_ME_HARDER &service); appsfile = _do_malloc(strlen(consoleapps) + strlen(service) + 2); sprintf(appsfile, "%s%s", consoleapps, service); /* trusted data */ if (access(lockfile, F_OK) < 0) { _pam_log(pamh, LOG_ERR, TRUE, "user %s not a console user", pwd->pw_name); ret = PAM_AUTH_ERR; goto error_return; } if (access(appsfile, F_OK) < 0) { _pam_log(pamh, LOG_ERR, TRUE, "console access disallowed for service %s", service); ret = PAM_AUTH_ERR; goto error_return; } /* all checks OK, must be OK */ ret = PAM_SUCCESS; error_return: if (lockfile) free (lockfile); if (appsfile) free (appsfile); return ret; } PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh UNUSED, int flags UNUSED, int argc UNUSED, const char **argv UNUSED) { return PAM_SUCCESS; } PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags UNUSED, int argc, const char **argv) { /* Create /var/run/console/console.lock if it does not exist * Create /var/run/console/ if it does not exist * Increment its use count * Change file ownerships and permissions as given in * /etc/security/console.perms IFF returned use count was 0 * and we created /var/run/console/console.lock */ int got_console = 0; int count = 0; int ret = PAM_SESSION_ERR; const char *username = NULL, *user_prompt; char *lockfile; const char *tty = NULL; D(("called.")); _pam_log(pamh, LOG_ERR, TRUE, "pam_console open_session"); _args_parse(pamh, argc, argv); if(pam_get_item(pamh, PAM_USER_PROMPT, (const void **) &user_prompt) != PAM_SUCCESS) { user_prompt = "user name: "; } username = NULL; pam_get_user(pamh, &username, user_prompt); _pam_log(pamh, LOG_DEBUG, TRUE, "user is \"%s\"", username ? username : "(null)"); if (!username || !username[0]) { _pam_log(pamh, LOG_DEBUG, TRUE, "user is \"%s\"", username ? username : "(null)"); return PAM_SESSION_ERR; } if (is_root(pamh, username)) { _pam_log(pamh, LOG_DEBUG, TRUE, "user \"%s\" is root", username); return PAM_SUCCESS; } pam_get_item(pamh, PAM_TTY, CAST_ME_HARDER &tty); if (!tty || !tty[0]) { _pam_log(pamh, LOG_ERR, TRUE, "TTY not defined"); return PAM_SESSION_ERR; } /* get configuration */ if (!configfileparsed) { console_parse_handlers(pamh, consolehandlers); configfileparsed = 1; } /* return success quietly if not a terminal login */ if (!check_console_name(pamh, tty, allow_nonroot_tty, TRUE)) return PAM_SUCCESS; if (!lock_console(pamh, username)) got_console = 1; lockfile = _do_malloc(strlen(consolerefs) + strlen(username) + 2); sprintf(lockfile, "%s%s", consolerefs, username); /* trusted data */ count = use_count(pamh, lockfile , 1, 0); if (count < 0) { ret = PAM_SESSION_ERR; } else if (got_console) { _pam_log(pamh, LOG_DEBUG, TRUE, "%s is console user", username); /* errors will be logged and are not critical */ console_run_handlers(pamh, TRUE, username, tty); } free(lockfile); return ret; } PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags UNUSED, int argc, const char **argv) { /* Get /var/run/console/ use count, leave it locked * If use count is now 1: * If /var/run/console/console.lock contains " * Revert file ownerships and permissions as given in * /etc/security/console.perms * Decrement /var/run/console/, removing both it and * /var/run/console/console.lock if 0, unlocking /var/run/console/ * in any case. */ int fd; int count = 0; int err = PAM_SUCCESS; int delete_consolelock = 0; const char *username = NULL, *user_prompt; char *lockfile = NULL; char *consoleuser = NULL; const char *tty = NULL; struct stat st; D(("called.")); _args_parse(pamh, argc, argv); if(pam_get_item(pamh, PAM_USER_PROMPT, (const void **) &user_prompt) != PAM_SUCCESS) { user_prompt = "user name: "; } pam_get_user(pamh, &username, user_prompt); if (!username || !username[0]) return PAM_SESSION_ERR; if (is_root(pamh, username)) return PAM_SUCCESS; pam_get_item(pamh, PAM_TTY, CAST_ME_HARDER &tty); if (!tty || !tty[0]) return PAM_SESSION_ERR; /* get configuration */ if (!configfileparsed) { console_parse_handlers(pamh, consolehandlers); configfileparsed = 1; } /* return success quietly if not a terminal login */ if (!check_console_name(pamh, tty, allow_nonroot_tty, FALSE)) return PAM_SUCCESS; lockfile = _do_malloc(strlen(consolerefs) + strlen(username) + 2); sprintf(lockfile, "%s%s", consolerefs, username); /* trusted data */ count = use_count(pamh, lockfile, 0, 0); if (count < 0) { err = PAM_SESSION_ERR; goto return_error; } if (count == 1) { fd = open(consolelock, O_RDONLY); if (fd != -1) { if (fstat (fd, &st)) { _pam_log(pamh, LOG_ERR, FALSE, "\"impossible\" fstat error on %s", consolelock); close(fd); err = PAM_SESSION_ERR; goto decrement; } consoleuser = _do_malloc(st.st_size+1); if (st.st_size) { if (pam_modutil_read (fd, consoleuser, st.st_size) == -1) { _pam_log(pamh, LOG_ERR, FALSE, "\"impossible\" read error on %s", consolelock); err = PAM_SESSION_ERR; close(fd); goto decrement; } consoleuser[st.st_size] = '\0'; } close (fd); if (!strcmp(username, consoleuser)) { delete_consolelock = 1; /* errors will be logged and at this stage we cannot do * anything about them... */ console_run_handlers(pamh, FALSE, username, tty); } } } decrement: count = use_count(pamh, lockfile, -1, 1); if (count < 1 && delete_consolelock) { if (unlink(consolelock)) { _pam_log(pamh, LOG_ERR, FALSE, "\"impossible\" unlink error on %s", consolelock); err = PAM_SESSION_ERR; } } return_error: if (lockfile) free(lockfile); if (consoleuser) free (consoleuser); return err; } #ifdef PAM_STATIC /* static module data */ struct pam_module _pam_console_modstruct = { "pam_console", pam_sm_authenticate, pam_sm_setcred, NULL, pam_sm_open_session, pam_sm_close_session, NULL, }; #endif /* end of module definition */