timestamp.c   [plain text]


/*
 * SPDX-License-Identifier: ISC
 *
 * Copyright (c) 2014-2019 Todd C. Miller <Todd.Miller@sudo.ws>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/*
 * This is an open source non-commercial project. Dear PVS-Studio, please check it.
 * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
 */

#include <config.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#if defined(HAVE_STDINT_H)
# include <stdint.h>
#elif defined(HAVE_INTTYPES_H)
# include <inttypes.h>
#endif
#ifdef HAVE_STRING_H
# include <string.h>
#endif /* HAVE_STRING_H */
#ifdef HAVE_STRINGS_H
# include <strings.h>
#endif /* HAVE_STRINGS_H */
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <pwd.h>
#include <grp.h>
#include <signal.h>

#include "sudoers.h"
#include "check.h"

#define TIMESTAMP_OPEN_ERROR	-1
#define TIMESTAMP_PERM_ERROR	-2

/*
 * Each user has a single time stamp file that contains multiple records.
 * Records are locked to ensure that changes are serialized.
 *
 * The first record is of type TS_LOCKEXCL and is used to gain exclusive
 * access to create new records.  This is a short-term lock and sudo
 * should not sleep while holding it (or the user will not be able to sudo).
 * The TS_LOCKEXCL entry must be unlocked before locking the actual record.
 */

struct ts_cookie {
    char *fname;
    int fd;
    pid_t sid;
    bool locked;
    off_t pos;
    struct timestamp_entry key;
};

/*
 * Returns true if entry matches key, else false.
 * We don't match on the sid or actual time stamp.
 */
static bool
ts_match_record(struct timestamp_entry *key, struct timestamp_entry *entry,
    unsigned int recno)
{
    debug_decl(ts_match_record, SUDOERS_DEBUG_AUTH)

    if (entry->version != key->version) {
	sudo_debug_printf(SUDO_DEBUG_DEBUG,
	    "%s:%u record version mismatch (want %u, got %u)", __func__, recno,
	    key->version, entry->version);
	debug_return_bool(false);
    }
    if (!ISSET(key->flags, TS_ANYUID) && entry->auth_uid != key->auth_uid) {
	sudo_debug_printf(SUDO_DEBUG_DEBUG,
	    "%s:%u record uid mismatch (want %u, got %u)", __func__, recno,
	    (unsigned int)key->auth_uid, (unsigned int)entry->auth_uid);
	debug_return_bool(false);
    }
    if (entry->type != key->type) {
	sudo_debug_printf(SUDO_DEBUG_DEBUG,
	    "%s:%u record type mismatch (want %u, got %u)", __func__, recno,
	    key->type, entry->type);
	debug_return_bool(false);
    }
    switch (entry->type) {
    case TS_GLOBAL:
	/* no ppid or tty to match */
	break;
    case TS_PPID:
	/* verify parent pid */
	if (entry->u.ppid != key->u.ppid) {
	    sudo_debug_printf(SUDO_DEBUG_DEBUG,
		"%s:%u record ppid mismatch (want %d, got %d)", __func__, recno,
		(int)key->u.ppid, (int)entry->u.ppid);
	    debug_return_bool(false);
	}
	if (sudo_timespeccmp(&entry->start_time, &key->start_time, !=)) {
	    sudo_debug_printf(SUDO_DEBUG_DEBUG,
		"%s:%u ppid start time mismatch", __func__, recno);
	    debug_return_bool(false);
	}
	break;
    case TS_TTY:
	if (entry->u.ttydev != key->u.ttydev) {
	    sudo_debug_printf(SUDO_DEBUG_DEBUG,
		"%s:%u record tty mismatch (want 0x%x, got 0x%x)", __func__,
		recno, (unsigned int)key->u.ttydev, (unsigned int)entry->u.ttydev);
	    debug_return_bool(false);
	}
	if (sudo_timespeccmp(&entry->start_time, &key->start_time, !=)) {
	    sudo_debug_printf(SUDO_DEBUG_DEBUG,
		"%s:%u session leader start time mismatch", __func__, recno);
	    debug_return_bool(false);
	}
	break;
    default:
	/* unknown record type, ignore it */
	sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
	    "%s:%u unknown time stamp record type %d", __func__, recno,
	    entry->type);
	debug_return_bool(false);
    }
    debug_return_bool(true);
}

/*
 * Searches the time stamp file descriptor for a record that matches key.
 * On success, fills in entry with the matching record and returns true.
 * On failure, returns false.
 *
 * Note that records are searched starting at the current file offset,
 * which may not be the beginning of the file.
 */
static bool
ts_find_record(int fd, struct timestamp_entry *key, struct timestamp_entry *entry)
{
    struct timestamp_entry cur;
    unsigned int recno = 0;
    debug_decl(ts_find_record, SUDOERS_DEBUG_AUTH)

    /*
     * Find a matching record (does not match sid or time stamp value).
     */
    while (read(fd, &cur, sizeof(cur)) == sizeof(cur)) {
	recno++;
	if (cur.size != sizeof(cur)) {
	    /* wrong size, seek to start of next record */
	    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
		"wrong sized record, got %hu, expected %zu",
		cur.size, sizeof(cur));
	    if (lseek(fd, (off_t)cur.size - (off_t)sizeof(cur), SEEK_CUR) == -1) {
		sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO,
		    "unable to seek forward %d",
		    (int)cur.size - (int)sizeof(cur));
		break;
	    }
	    if (cur.size == 0)
		break;			/* size must be non-zero */
	    continue;
	}
	if (ts_match_record(key, &cur, recno)) {
	    memcpy(entry, &cur, sizeof(struct timestamp_entry));
	    debug_return_bool(true);
	}
    }
    debug_return_bool(false);
}

/*
 * Create a directory and any missing parent directories with the
 * specified mode.
 * Returns true on success.
 * Returns false on failure and displays a warning to stderr.
 */
static bool
ts_mkdirs(char *path, uid_t owner, gid_t group, mode_t mode,
    mode_t parent_mode, bool quiet)
{
    bool ret;
    mode_t omask;
    debug_decl(ts_mkdirs, SUDOERS_DEBUG_AUTH)

    /* umask must not be more restrictive than the file modes. */
    omask = umask(ACCESSPERMS & ~(mode|parent_mode));
    ret = sudo_mkdir_parents(path, owner, group, parent_mode, quiet);
    if (ret) {
	/* Create final path component. */
	sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
	    "mkdir %s, mode 0%o, uid %d, gid %d", path, (unsigned int)mode,
	    (int)owner, (int)group);
	if (mkdir(path, mode) != 0 && errno != EEXIST) {
	    if (!quiet)
		sudo_warn(U_("unable to mkdir %s"), path);
	    ret = false;
	} else {
	    if (chown(path, owner, group) != 0) {
		sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
		    "%s: unable to chown %d:%d %s", __func__,
		    (int)owner, (int)group, path);
	    }
	}
    }
    umask(omask);
    debug_return_bool(ret);
}

/*
 * Check that path is owned by timestamp_uid and not writable by
 * group or other.  If path is missing and make_it is true, create
 * the directory and its parent dirs.
 * Returns true on success or false on failure, setting errno.
 */
static bool
ts_secure_dir(char *path, bool make_it, bool quiet)
{
    struct stat sb;
    bool ret = false;
    debug_decl(ts_secure_dir, SUDOERS_DEBUG_AUTH)

    sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "checking %s", path);
    switch (sudo_secure_dir(path, timestamp_uid, -1, &sb)) {
    case SUDO_PATH_SECURE:
	ret = true;
	break;
    case SUDO_PATH_MISSING:
	if (make_it && ts_mkdirs(path, timestamp_uid, timestamp_gid, S_IRWXU,
	    S_IRWXU|S_IXGRP|S_IXOTH, quiet)) {
	    ret = true;
	    break;
	}
	errno = ENOENT;
	break;
    case SUDO_PATH_BAD_TYPE:
	errno = ENOTDIR;
	if (!quiet)
	    sudo_warn("%s", path);
	break;
    case SUDO_PATH_WRONG_OWNER:
	if (!quiet) {
	    sudo_warnx(U_("%s is owned by uid %u, should be %u"),
		path, (unsigned int) sb.st_uid,
		(unsigned int) timestamp_uid);
	}
	errno = EACCES;
	break;
    case SUDO_PATH_GROUP_WRITABLE:
	if (!quiet)
	    sudo_warnx(U_("%s is group writable"), path);
	errno = EACCES;
	break;
    }
    debug_return_bool(ret);
}

/*
 * Open the specified timestamp or lecture file and set the
 * close on exec flag.
 * Returns open file descriptor on success.
 * Returns TIMESTAMP_OPEN_ERROR or TIMESTAMP_PERM_ERROR on error.
 */
static int
ts_open(const char *path, int flags)
{
    bool uid_changed = false;
    int fd;
    debug_decl(ts_open, SUDOERS_DEBUG_AUTH)

    if (timestamp_uid != 0)
	uid_changed = set_perms(PERM_TIMESTAMP);
    fd = open(path, flags, S_IRUSR|S_IWUSR);
    if (uid_changed && !restore_perms()) {
	/* Unable to restore permissions, should not happen. */
	if (fd != -1) {
	    int serrno = errno;
	    close(fd);
	    errno = serrno;
	    fd = TIMESTAMP_PERM_ERROR;
	}
    }
    if (fd >= 0)
	(void)fcntl(fd, F_SETFD, FD_CLOEXEC);

    debug_return_int(fd);
}

static ssize_t
ts_write(int fd, const char *fname, struct timestamp_entry *entry, off_t offset)
{
    ssize_t nwritten;
    off_t old_eof;
    debug_decl(ts_write, SUDOERS_DEBUG_AUTH)

    if (offset == -1) {
	old_eof = lseek(fd, 0, SEEK_CUR);
	nwritten = write(fd, entry, entry->size);
    } else {
	old_eof = offset;
#ifdef HAVE_PWRITE
	nwritten = pwrite(fd, entry, entry->size, offset);
#else
	if (lseek(fd, offset, SEEK_SET) == -1) {
	    sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO,
		"unable to seek to %lld", (long long)offset);
	    nwritten = -1;
	} else {
	    nwritten = write(fd, entry, entry->size);
	}
#endif
    }
    if ((size_t)nwritten != entry->size) {
	if (nwritten == -1) {
	    log_warning(SLOG_SEND_MAIL,
		N_("unable to write to %s"), fname);
	} else {
	    log_warningx(SLOG_SEND_MAIL,
		N_("unable to write to %s"), fname);
	}

	/* Truncate on partial write to be safe (assumes end of file). */
	if (nwritten > 0) {
	    sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
		"short write, truncating partial time stamp record");
	    if (ftruncate(fd, old_eof) != 0) {
		sudo_warn(U_("unable to truncate time stamp file to %lld bytes"),
		    (long long)old_eof);
	    }
	}
	debug_return_ssize_t(-1);
    }
    debug_return_ssize_t(nwritten);
}

/*
 * Full in struct timestamp_entry with the specified flags
 * based on auth user pw.  Does not set the time stamp.
 */
static void
ts_init_key(struct timestamp_entry *entry, struct passwd *pw, int flags,
    enum def_tuple ticket_type)
{
    struct stat sb;
    debug_decl(ts_init_key, SUDOERS_DEBUG_AUTH)

    memset(entry, 0, sizeof(*entry));
    entry->version = TS_VERSION;
    entry->size = sizeof(*entry);
    entry->flags = flags;
    if (pw != NULL) {
	entry->auth_uid = pw->pw_uid;
    } else {
	entry->flags |= TS_ANYUID;
    }
    entry->sid = user_sid;
    switch (ticket_type) {
    default:
	/* Unknown time stamp ticket type, treat as tty (should not happen). */
	sudo_warnx("unknown time stamp ticket type %d", ticket_type);
	/* FALLTHROUGH */
    case tty:
	if (user_ttypath != NULL && stat(user_ttypath, &sb) == 0) {
	    /* tty-based time stamp */
	    entry->type = TS_TTY;
	    entry->u.ttydev = sb.st_rdev;
	    if (entry->sid != -1)
		get_starttime(entry->sid, &entry->start_time);
	    break;
	}
	/* FALLTHROUGH */
    case kernel:
    case ppid:
	/* ppid-based time stamp */
	entry->type = TS_PPID;
	entry->u.ppid = getppid();
	get_starttime(entry->u.ppid, &entry->start_time);
	break;
    case global:
	/* global time stamp */
	entry->type = TS_GLOBAL;
	break;
    }

    debug_return;
}

static void
ts_init_key_nonglobal(struct timestamp_entry *entry, struct passwd *pw, int flags)
{
    /*
     * Even if the timestamp type is global or kernel we still want to do
     * per-tty or per-ppid locking so sudo works predictably in a pipeline.
     */
    ts_init_key(entry, pw, flags,
	def_timestamp_type == ppid ? ppid : tty);
}

/*
 * Open the user's time stamp file.
 * Returns a cookie or NULL on error, does not lock the file.
 */
void *
timestamp_open(const char *user, pid_t sid)
{
    struct ts_cookie *cookie;
    char *fname = NULL;
    int tries, fd = -1;
    debug_decl(timestamp_open, SUDOERS_DEBUG_AUTH)

    /* Zero timeout means don't use the time stamp file. */
    if (!sudo_timespecisset(&def_timestamp_timeout)) {
	errno = ENOENT;
	goto bad;
    }

    /* Sanity check timestamp dir and create if missing. */
    if (!ts_secure_dir(def_timestampdir, true, false))
	goto bad;

    /* Open time stamp file. */
    if (asprintf(&fname, "%s/%s", def_timestampdir, user) == -1) {
	sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
	goto bad;
    }
    for (tries = 1; ; tries++) {
	struct stat sb;

	fd = ts_open(fname, O_RDWR|O_CREAT);
	switch (fd) {
	case TIMESTAMP_OPEN_ERROR:
	    log_warning(SLOG_SEND_MAIL, N_("unable to open %s"), fname);
	    goto bad;
	case TIMESTAMP_PERM_ERROR:
	    /* Already logged set_perms/restore_perms error. */
	    goto bad;
	}

	/* Remove time stamp file if its mtime predates boot time. */
	if (tries == 1 && fstat(fd, &sb) == 0) {
	    struct timespec boottime, mtime, now;

	    if (sudo_gettime_real(&now) == 0 && get_boottime(&boottime)) {
		/* Ignore a boot time that is in the future. */
		if (sudo_timespeccmp(&now, &boottime, <)) {
		    sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
			"ignoring boot time that is in the future");
		} else {
		    mtim_get(&sb, mtime);
		    if (sudo_timespeccmp(&mtime, &boottime, <)) {
			/* Time stamp file too old, remove it. */
			sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO,
			    "removing time stamp file that predates boot time");
			close(fd);
			unlink(fname);
			continue;
		    }
		}
	    }
	}
	break;
    }

    /* Allocate and fill in cookie to store state. */
    cookie = malloc(sizeof(*cookie));
    if (cookie == NULL) {
	sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
	goto bad;
    }
    cookie->fd = fd;
    cookie->fname = fname;
    cookie->sid = sid;
    cookie->pos = -1;

    debug_return_ptr(cookie);
bad:
    if (fd != -1)
	close(fd);
    free(fname);
    debug_return_ptr(NULL);
}

static volatile sig_atomic_t got_signal;

static void
timestamp_handler(int s)
{
    got_signal = s;
}

/*
 * Wrapper for sudo_lock_region() that is interruptible.
 */
static bool
timestamp_lock_record(int fd, off_t pos, off_t len)
{
    struct sigaction sa, saveint, savequit;
    sigset_t mask, omask;
    bool ret;
    debug_decl(timestamp_lock_record, SUDOERS_DEBUG_AUTH)

    if (pos >= 0 && lseek(fd, pos, SEEK_SET) == -1) {
	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO,
	    "unable to seek to %lld", (long long)pos);
	debug_return_bool(false);
    }

    /* Allow SIGINT and SIGQUIT to interrupt a lock. */
    got_signal = 0;
    memset(&sa, 0, sizeof(sa));
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0; /* don't restart system calls */
    sa.sa_handler = timestamp_handler;
    (void) sigaction(SIGINT, &sa, &saveint);
    (void) sigaction(SIGQUIT, &sa, &savequit);
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGQUIT);
    (void) sigprocmask(SIG_UNBLOCK, &mask, &omask);

    ret = sudo_lock_region(fd, SUDO_LOCK, len);
    if (!ret) {
	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO,
	    "failed to lock fd %d [%lld, %lld]", fd,
	    (long long)pos, (long long)len);
    }

    /* Restore the old mask (SIGINT and SIGQUIT blocked) and handlers. */
    (void) sigprocmask(SIG_SETMASK, &omask, NULL);
    (void) sigaction(SIGINT, &saveint, NULL);
    (void) sigaction(SIGQUIT, &savequit, NULL);

    /* Re-deliver the signal that interrupted the lock, if any. */
    if (!ret && got_signal)
	kill(getpid(), got_signal);

    debug_return_bool(ret);
}

static bool
timestamp_unlock_record(int fd, off_t pos, off_t len)
{
    debug_decl(timestamp_unlock_record, SUDOERS_DEBUG_AUTH)

    if (pos >= 0 && lseek(fd, pos, SEEK_SET) == -1) {
	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO,
	    "unable to seek to %lld", (long long)pos);
	debug_return_bool(false);
    }
    debug_return_bool(sudo_lock_region(fd, SUDO_UNLOCK, len));
}

/*
 * Seek to the record's position and read it, locking as needed.
 */
static ssize_t
ts_read(struct ts_cookie *cookie, struct timestamp_entry *entry)
{
    ssize_t nread = -1;
    bool should_unlock = false;
    debug_decl(ts_read, SUDOERS_DEBUG_AUTH)

    /* If the record is not already locked, lock it now.  */
    if (!cookie->locked) {
	if (!timestamp_lock_record(cookie->fd, cookie->pos, sizeof(*entry)))
	    goto done;
	should_unlock = true;
    }

    /* Seek to the record position and read it.  */
#ifdef HAVE_PREAD
    nread = pread(cookie->fd, entry, sizeof(*entry), cookie->pos);
#else
    if (lseek(cookie->fd, cookie->pos, SEEK_SET) == -1) {
	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO,
	    "unable to seek to %lld", (long long)cookie->pos);
	goto done;
    }
    nread = read(cookie->fd, entry, sizeof(*entry));
#endif
    if (nread != sizeof(*entry)) {
	/* short read, should not happen */
	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
	    "short read (%zd vs %zu), truncated time stamp file?",
	    nread, sizeof(*entry));
	goto done;
    }
    sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
	"read %zd byte record at %lld", nread, (long long)cookie->pos);

done:
    /* If the record was not locked initially, unlock it. */
    if (should_unlock)
	timestamp_unlock_record(cookie->fd, cookie->pos, sizeof(*entry));

    debug_return_ssize_t(nread);
}

/*
 * Lock a record in the time stamp file for exclusive access.
 * If the record does not exist, it is created (as disabled).
 */
bool
timestamp_lock(void *vcookie, struct passwd *pw)
{
    struct ts_cookie *cookie = vcookie;
    struct timestamp_entry entry;
    off_t lock_pos;
    ssize_t nread;
    debug_decl(timestamp_lock, SUDOERS_DEBUG_AUTH)

    if (cookie == NULL) {
	sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
	    "called with a NULL cookie!");
	debug_return_bool(false);
    }

    /*
     * Take a lock on the "write" record (the first record in the file).
     * This will let us seek for the record or extend as needed
     * without colliding with anyone else.
     */
    if (!timestamp_lock_record(cookie->fd, 0, sizeof(struct timestamp_entry)))
	debug_return_bool(false);

    /* Make sure the first record is of type TS_LOCKEXCL. */
    memset(&entry, 0, sizeof(entry));
    nread = read(cookie->fd, &entry, sizeof(entry));
    if (nread == 0) {
	/* New file, add TS_LOCKEXCL record. */
	entry.version = TS_VERSION;
	entry.size = sizeof(entry);
	entry.type = TS_LOCKEXCL;
	if (ts_write(cookie->fd, cookie->fname, &entry, -1) == -1)
	    debug_return_bool(false);
    } else if (entry.type != TS_LOCKEXCL) {
	/* Old sudo record, convert it to TS_LOCKEXCL. */
	entry.type = TS_LOCKEXCL;
	memset((char *)&entry + offsetof(struct timestamp_entry, type), 0,
	    nread - offsetof(struct timestamp_entry, type));
	if (ts_write(cookie->fd, cookie->fname, &entry, 0) == -1)
	    debug_return_bool(false);
    }
    if (entry.size != sizeof(entry)) {
	/* Reset position if the lock record has an unexpected size. */
	if (lseek(cookie->fd, entry.size, SEEK_SET) == -1) {
	    sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO,
		"unable to seek to %lld", (long long)entry.size);
	    debug_return_bool(false);
	}
    }

    /* Search for a tty/ppid-based record or append a new one. */
    sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
	"searching for %s time stamp record",
	def_timestamp_type == ppid ? "ppid" : "tty");
    ts_init_key_nonglobal(&cookie->key, pw, TS_DISABLED);
    if (ts_find_record(cookie->fd, &cookie->key, &entry)) {
	sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
	    "found existing %s time stamp record",
	    def_timestamp_type == ppid ? "ppid" : "tty");
	lock_pos = lseek(cookie->fd, 0, SEEK_CUR) - (off_t)entry.size;
    } else {
	sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
	    "appending new %s time stamp record",
	    def_timestamp_type == ppid ? "ppid" : "tty");
	lock_pos = lseek(cookie->fd, 0, SEEK_CUR);
	if (ts_write(cookie->fd, cookie->fname, &cookie->key, -1) == -1)
	    debug_return_bool(false);
    }
    sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
	"%s time stamp position is %lld",
	def_timestamp_type == ppid ? "ppid" : "tty", (long long)lock_pos);

    if (def_timestamp_type == global) {
	/*
	 * For global tickets we use a separate record lock that we
	 * cannot hold long-term since it is shared between all ttys.
	 */
	cookie->locked = false;
	cookie->key.type = TS_GLOBAL;	/* find a global record */

	if (lseek(cookie->fd, 0, SEEK_SET) == -1) {
	    sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO,
		"unable to rewind fd");
	    debug_return_bool(false);
	}
	if (ts_find_record(cookie->fd, &cookie->key, &entry)) {
	    sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
		"found existing global record");
	    cookie->pos = lseek(cookie->fd, 0, SEEK_CUR) - (off_t)entry.size;
	} else {
	    sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
		"appending new global record");
	    cookie->pos = lseek(cookie->fd, 0, SEEK_CUR);
	    if (ts_write(cookie->fd, cookie->fname, &cookie->key, -1) == -1)
		debug_return_bool(false);
	}
    } else {
	/* For tty/ppid tickets the tty lock is the same as the record lock. */
	cookie->pos = lock_pos;
	cookie->locked = true;
    }

    /* Unlock the TS_LOCKEXCL record. */
    timestamp_unlock_record(cookie->fd, 0, sizeof(struct timestamp_entry));

    /* Lock the per-tty record (may sleep). */
    if (!timestamp_lock_record(cookie->fd, lock_pos, sizeof(struct timestamp_entry)))
	debug_return_bool(false);

    debug_return_bool(true);
}

void
timestamp_close(void *vcookie)
{
    struct ts_cookie *cookie = vcookie;
    debug_decl(timestamp_close, SUDOERS_DEBUG_AUTH)

    if (cookie != NULL) {
	close(cookie->fd);
	free(cookie->fname);
	free(cookie);
    }

    debug_return;
}

/*
 * Check the time stamp file and directory and return their status.
 * Called with the file position before the locked record to read.
 * Returns one of TS_CURRENT, TS_OLD, TS_MISSING, TS_ERROR, TS_FATAL.
 * Fills in fdp with an open file descriptor positioned at the
 * appropriate (and locked) record.
 */
int
timestamp_status(void *vcookie, struct passwd *pw)
{
    struct ts_cookie *cookie = vcookie;
    struct timestamp_entry entry;
    struct timespec diff, now;
    int status = TS_ERROR;		/* assume the worst */
    ssize_t nread;
    debug_decl(timestamp_status, SUDOERS_DEBUG_AUTH)

    /* Zero timeout means don't use time stamp files. */
    if (!sudo_timespecisset(&def_timestamp_timeout)) {
	sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
	    "timestamps disabled");
	status = TS_OLD;
	goto done;
    }
    if (cookie == NULL || cookie->pos < 0) {
	sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
	    "NULL cookie or invalid position");
	status = TS_OLD;
	goto done;
    }

#ifdef TIOCCHKVERAUTH
    if (def_timestamp_type == kernel) {
	int fd = open(_PATH_TTY, O_RDWR);
	if (fd != -1) {
	    if (ioctl(fd, TIOCCHKVERAUTH) == 0)
		status = TS_CURRENT;
	    else
		status = TS_OLD;
	    close(fd);
	    goto done;
	}
    }
#endif

    /* Read the record at the correct position. */
    if ((nread = ts_read(cookie, &entry)) != sizeof(entry))
	goto done;

    /* Make sure what we read matched the expected record. */
    if (entry.version != TS_VERSION || entry.size != nread) {
	/* do something else? */
	sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
	    "invalid time stamp file @ %lld", (long long)cookie->pos);
	status = TS_OLD;
	goto done;
    }

    if (ISSET(entry.flags, TS_DISABLED)) {
	sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
	    "time stamp record disabled");
	status = TS_OLD;	/* disabled via sudo -k */
	goto done;
    }

    if (entry.type != TS_GLOBAL && entry.sid != cookie->sid) {
	sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
	    "time stamp record sid mismatch");
	status = TS_OLD;	/* belongs to different session */
	goto done;
    }

    /* Negative timeouts only expire manually (sudo -k).  */
    sudo_timespecclear(&diff);
    if (sudo_timespeccmp(&def_timestamp_timeout, &diff, <)) {
	sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
	    "time stamp record does not expire");
	status = TS_CURRENT;
	goto done;
    }

    /* Compare stored time stamp with current time. */
    if (sudo_gettime_mono(&now) == -1) {
	log_warning(0, N_("unable to read the clock"));
	status = TS_ERROR;
	goto done;
    }
    sudo_timespecsub(&now, &entry.ts, &diff);
    if (sudo_timespeccmp(&diff, &def_timestamp_timeout, <)) {
	status = TS_CURRENT;
#if defined(CLOCK_MONOTONIC) || defined(__MACH__)
	/* A monotonic clock should never run backwards. */
	if (diff.tv_sec < 0) {
	    log_warningx(SLOG_SEND_MAIL,
		N_("ignoring time stamp from the future"));
	    status = TS_OLD;
	    SET(entry.flags, TS_DISABLED);
	    (void)ts_write(cookie->fd, cookie->fname, &entry, cookie->pos);
	}
#else
	/*
	 * Check for bogus (future) time in the stampfile.
	 * If diff / 2 > timeout, someone has been fooling with the clock.
	 */
	sudo_timespecsub(&entry.ts, &now, &diff);
	diff.tv_nsec /= 2;
	if (diff.tv_sec & 1)
	    diff.tv_nsec += 500000000;
	diff.tv_sec /= 2;
	while (diff.tv_nsec >= 1000000000) {
	    diff.tv_sec++;
	    diff.tv_nsec -= 1000000000;
	}

	if (sudo_timespeccmp(&diff, &def_timestamp_timeout, >)) {
	    time_t tv_sec = (time_t)entry.ts.tv_sec;
	    log_warningx(SLOG_SEND_MAIL,
		N_("time stamp too far in the future: %20.20s"),
		4 + ctime(&tv_sec));
	    status = TS_OLD;
	    SET(entry.flags, TS_DISABLED);
	    (void)ts_write(cookie->fd, cookie->fname, &entry, cookie->pos);
	}
#endif /* CLOCK_MONOTONIC */
    } else {
	status = TS_OLD;
    }

done:
    debug_return_int(status);
}

/*
 * Update the time on the time stamp file/dir or create it if necessary.
 * Returns true on success, false on failure or -1 on setuid failure.
 */
bool
timestamp_update(void *vcookie, struct passwd *pw)
{
    struct ts_cookie *cookie = vcookie;
    int ret = false;
    debug_decl(timestamp_update, SUDOERS_DEBUG_AUTH)

    /* Zero timeout means don't use time stamp files. */
    if (!sudo_timespecisset(&def_timestamp_timeout)) {
	sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
	    "timestamps disabled");
	goto done;
    }
    if (cookie == NULL || cookie->pos < 0) {
	sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
	    "NULL cookie or invalid position");
	goto done;
    }

#ifdef TIOCSETVERAUTH
    if (def_timestamp_type == kernel) {
	int fd = open(_PATH_TTY, O_RDWR);
	if (fd != -1) {
	    int secs = def_timestamp_timeout.tv_sec;
	    if (secs > 0) {
		if (secs > 3600)
		    secs = 3600;	/* OpenBSD limitation */
		if (ioctl(fd, TIOCSETVERAUTH, &secs) != 0)
		    sudo_warn("TIOCSETVERAUTH");
	    }
	    close(fd);
	    goto done;
	}
    }
#endif

    /* Update timestamp in key and enable it. */
    CLR(cookie->key.flags, TS_DISABLED);
    if (sudo_gettime_mono(&cookie->key.ts) == -1) {
	log_warning(0, N_("unable to read the clock"));
	goto done;
    }

    /* Write out the locked record. */
    sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
	"writing %zu byte record at %lld", sizeof(cookie->key),
	(long long)cookie->pos);
    if (ts_write(cookie->fd, cookie->fname, &cookie->key, cookie->pos) != -1)
	ret = true;

done:
    debug_return_int(ret);
}

/*
 * Remove the timestamp entry or file if unlink_it is set.
 * Returns true on success, false on failure or -1 on setuid failure.
 * A missing timestamp entry is not considered an error.
 */
int
timestamp_remove(bool unlink_it)
{
    struct timestamp_entry key, entry;
    int fd = -1, ret = true;
    char *fname = NULL;
    debug_decl(timestamp_remove, SUDOERS_DEBUG_AUTH)

#ifdef TIOCCLRVERAUTH
    if (def_timestamp_type == kernel) {
	fd = open(_PATH_TTY, O_RDWR);
	if (fd != -1) {
	    ioctl(fd, TIOCCLRVERAUTH);
	    goto done;
	}
    }
#endif

    if (asprintf(&fname, "%s/%s", def_timestampdir, user_name) == -1) {
	sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
	ret = -1;
	goto done;
    }

    /* For "sudo -K" simply unlink the time stamp file. */
    if (unlink_it) {
	ret = unlink(fname) ? -1 : true;
	goto done;
    }

    /* Open time stamp file and lock it for exclusive access. */
    fd = ts_open(fname, O_RDWR);
    switch (fd) {
    case TIMESTAMP_OPEN_ERROR:
	if (errno != ENOENT)
	    ret = false;
	goto done;
    case TIMESTAMP_PERM_ERROR:
	/* Already logged set_perms/restore_perms error. */
	ret = -1;
	goto done;
    }
    /* Lock first record to gain exclusive access. */
    if (!timestamp_lock_record(fd, -1, sizeof(struct timestamp_entry))) {
	sudo_warn(U_("unable to lock time stamp file %s"), fname);
	ret = -1;
	goto done;
    }

    /*
     * Find matching entries and invalidate them.
     */
    ts_init_key(&key, NULL, 0, def_timestamp_type);
    while (ts_find_record(fd, &key, &entry)) {
	/* Back up and disable the entry. */
	if (!ISSET(entry.flags, TS_DISABLED)) {
	    SET(entry.flags, TS_DISABLED);
	    if (lseek(fd, 0 - (off_t)sizeof(entry), SEEK_CUR) != -1) {
		if (ts_write(fd, fname, &entry, -1) == -1)
		    ret = false;
	    }
	}
    }

done:
    if (fd != -1)
	close(fd);
    free(fname);
    debug_return_int(ret);
}

/*
 * Returns true if the user has already been lectured.
 */
bool
already_lectured(int unused)
{
    char status_file[PATH_MAX];
    struct stat sb;
    int len;
    debug_decl(already_lectured, SUDOERS_DEBUG_AUTH)

    if (ts_secure_dir(def_lecture_status_dir, false, true)) {
	len = snprintf(status_file, sizeof(status_file), "%s/%s",
	    def_lecture_status_dir, user_name);
	if (len > 0 && len < ssizeof(status_file)) {
	    debug_return_bool(stat(status_file, &sb) == 0);
	}
	log_warningx(SLOG_SEND_MAIL, N_("lecture status path too long: %s/%s"),
	    def_lecture_status_dir, user_name);
    }
    debug_return_bool(false);
}

/*
 * Create the lecture status file.
 * Returns true on success, false on failure or -1 on setuid failure.
 */
int
set_lectured(void)
{
    char lecture_status[PATH_MAX];
    int len, fd, ret = false;
    debug_decl(set_lectured, SUDOERS_DEBUG_AUTH)

    len = snprintf(lecture_status, sizeof(lecture_status), "%s/%s",
	def_lecture_status_dir, user_name);
    if (len < 0 || len >= ssizeof(lecture_status)) {
	log_warningx(SLOG_SEND_MAIL, N_("lecture status path too long: %s/%s"),
	    def_lecture_status_dir, user_name);
	goto done;
    }

    /* Sanity check lecture dir and create if missing. */
    if (!ts_secure_dir(def_lecture_status_dir, true, false))
	goto done;

    /* Create lecture file. */
    fd = ts_open(lecture_status, O_WRONLY|O_CREAT|O_EXCL);
    switch (fd) {
    case TIMESTAMP_OPEN_ERROR:
	/* Failed to open, not a fatal error. */
	break;
    case TIMESTAMP_PERM_ERROR:
	/* Already logged set_perms/restore_perms error. */
	ret = -1;
	break;
    default:
	/* Success. */
	close(fd);
	ret = true;
	break;
    }

done:
    debug_return_int(ret);
}