diff --git a/config.h.in b/config.h.in index 196cfa254..85ecf5db7 100644 --- a/config.h.in +++ b/config.h.in @@ -229,9 +229,6 @@ /* Define to 1 if you have the `fgetln' function. */ #undef HAVE_FGETLN -/* Define to 1 if you have the `flock' function. */ -#undef HAVE_FLOCK - /* Define to 1 if you have the `fnmatch' function. */ #undef HAVE_FNMATCH diff --git a/configure b/configure index bd4f7ef73..a600d7c1a 100755 --- a/configure +++ b/configure @@ -15165,9 +15165,8 @@ fi test -z "$with_pam" && AUTH_EXCL_DEF="PAM" ;; *-*-gnu*) - # lockf() is broken on the Hurd -- use flock instead + # lockf() is broken on the Hurd ac_cv_func_lockf=no - ac_cv_func_flock=yes ;; *-*-ultrix*) OS="ultrix" @@ -15477,9 +15476,8 @@ fi fi ;; *-*-nextstep*) - # lockf() is broken on the NeXT -- use flock instead + # lockf() is broken on the NeXT ac_cv_func_lockf=no - ac_cv_func_flock=yes RTLD_PRELOAD_VAR="DYLD_INSERT_LIBRARIES" RTLD_PRELOAD_ENABLE_VAR="DYLD_FORCE_FLAT_NAMESPACE" ;; @@ -18759,13 +18757,12 @@ fi done fi -for ac_func in lockf flock +for ac_func in lockf do : - as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh` -ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var" -if eval test \"x\$"$as_ac_var"\" = x"yes"; then : + ac_fn_c_check_func "$LINENO" "lockf" "ac_cv_func_lockf" +if test "x$ac_cv_func_lockf" = xyes; then : cat >>confdefs.h <<_ACEOF -#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1 +#define HAVE_LOCKF 1 _ACEOF break fi diff --git a/configure.ac b/configure.ac index 7ba501acc..a997e9130 100644 --- a/configure.ac +++ b/configure.ac @@ -1943,9 +1943,8 @@ case "$host" in test -z "$with_pam" && AUTH_EXCL_DEF="PAM" ;; *-*-gnu*) - # lockf() is broken on the Hurd -- use flock instead + # lockf() is broken on the Hurd ac_cv_func_lockf=no - ac_cv_func_flock=yes ;; *-*-ultrix*) OS="ultrix" @@ -2119,9 +2118,8 @@ case "$host" in fi ;; *-*-nextstep*) - # lockf() is broken on the NeXT -- use flock instead + # lockf() is broken on the NeXT ac_cv_func_lockf=no - ac_cv_func_flock=yes RTLD_PRELOAD_VAR="DYLD_INSERT_LIBRARIES" RTLD_PRELOAD_ENABLE_VAR="DYLD_FORCE_FLAT_NAMESPACE" ;; @@ -2521,7 +2519,7 @@ AC_CHECK_FUNCS_ONCE([seteuid]) if test X"$with_interfaces" != X"no"; then AC_CHECK_FUNCS([getifaddrs], [AC_CHECK_FUNCS([freeifaddrs])]) fi -AC_CHECK_FUNCS([lockf flock], [break]) +AC_CHECK_FUNCS([lockf], [break]) AC_CHECK_FUNCS([innetgr], [ AC_CHECK_DECLS([innetgr], [], [], [ AC_INCLUDES_DEFAULT diff --git a/include/sudo_util.h b/include/sudo_util.h index 89c9f89f5..d20bcb1d4 100644 --- a/include/sudo_util.h +++ b/include/sudo_util.h @@ -182,6 +182,8 @@ __dso_public char *sudo_new_key_val_v1(const char *key, const char *value); #define SUDO_UNLOCK 4 /* unlock a file */ __dso_public bool sudo_lock_file_v1(int fd, int action); #define sudo_lock_file(_a, _b) sudo_lock_file_v1((_a), (_b)) +__dso_public bool sudo_lock_region_v1(int fd, int action, off_t len); +#define sudo_lock_region(_a, _b, _c) sudo_lock_region_v1((_a), (_b), (_c)) /* parseln.c */ __dso_public ssize_t sudo_parseln_v1(char **buf, size_t *bufsize, unsigned int *lineno, FILE *fp); diff --git a/lib/util/locking.c b/lib/util/locking.c index ca53cb6ac..b33845759 100644 --- a/lib/util/locking.c +++ b/lib/util/locking.c @@ -22,9 +22,6 @@ #include #include -#ifdef HAVE_FLOCK -# include -#endif /* HAVE_FLOCK */ #include #ifdef HAVE_STRING_H # include @@ -46,16 +43,22 @@ #include "sudo_debug.h" /* - * Lock/unlock a file. + * Lock/unlock all or part of a file. */ #ifdef HAVE_LOCKF bool -sudo_lock_file_v1(int fd, int lockit) +sudo_lock_file_v1(int fd, int type) { - int op = 0; - debug_decl(sudo_lock_file, SUDO_DEBUG_UTIL) + return sudo_lock_region_v1(fd, type, 0); +} - switch (lockit) { +bool +sudo_lock_region_v1(int fd, int type, off_t len) +{ + int op; + debug_decl(sudo_lock_region, SUDO_DEBUG_UTIL) + + switch (type) { case SUDO_LOCK: op = F_LOCK; break; @@ -65,48 +68,32 @@ sudo_lock_file_v1(int fd, int lockit) case SUDO_UNLOCK: op = F_ULOCK; break; + default: + debug_return_bool(false); } - debug_return_bool(lockf(fd, op, 0) == 0); -} -#elif defined(HAVE_FLOCK) -bool -sudo_lock_file_v1(int fd, int lockit) -{ - int op = 0; - debug_decl(sudo_lock_file, SUDO_DEBUG_UTIL) - - switch (lockit) { - case SUDO_LOCK: - op = LOCK_EX; - break; - case SUDO_TLOCK: - op = LOCK_EX | LOCK_NB; - break; - case SUDO_UNLOCK: - op = LOCK_UN; - break; - } - debug_return_bool(flock(fd, op) == 0); + debug_return_bool(lockf(fd, op, len) == 0); } #else bool -sudo_lock_file_v1(int fd, int lockit) +sudo_lock_file_v1(int fd, int type) +{ + return sudo_lock_region_v1(fd, type, 0); +} + +bool +sudo_lock_region_v1(int fd, int type, off_t len) { -#ifdef F_SETLK int func; struct flock lock; debug_decl(sudo_lock_file, SUDO_DEBUG_UTIL) lock.l_start = 0; - lock.l_len = 0; - lock.l_pid = getpid(); - lock.l_type = (lockit == SUDO_UNLOCK) ? F_UNLCK : F_WRLCK; - lock.l_whence = SEEK_SET; - func = (lockit == SUDO_LOCK) ? F_SETLKW : F_SETLK; + lock.l_len = len; + lock.l_pid = 0; + lock.l_type = (type == SUDO_UNLOCK) ? F_UNLCK : F_WRLCK; + lock.l_whence = len ? SEEK_CUR : SEEK_SET; + func = (type == SUDO_LOCK) ? F_SETLKW : F_SETLK; debug_return_bool(fcntl(fd, func, &lock) == 0); -#else - return true; -#endif } #endif diff --git a/lib/util/util.exp.in b/lib/util/util.exp.in index 995567fbc..c024f61ee 100644 --- a/lib/util/util.exp.in +++ b/lib/util/util.exp.in @@ -66,6 +66,7 @@ sudo_lbuf_error_v1 sudo_lbuf_init_v1 sudo_lbuf_print_v1 sudo_lock_file_v1 +sudo_lock_region_v1 sudo_new_key_val_v1 sudo_parse_gids_v1 sudo_parseln_v1 diff --git a/plugins/sudoers/check.c b/plugins/sudoers/check.c index 6dbddabd1..94e320d4e 100644 --- a/plugins/sudoers/check.c +++ b/plugins/sudoers/check.c @@ -48,13 +48,15 @@ static struct passwd *get_authpw(int); /* * Returns true if the user successfully authenticates, false if not - * or -1 on error. + * or -1 on fatal error. */ static int check_user_interactive(int validated, int mode, struct passwd *auth_pw) { - int status, rval = -1; + int status = TS_ERROR; + int rval = -1; char *prompt; + void *cookie; bool lectured; debug_decl(check_user_interactive, SUDOERS_DEBUG_AUTH) @@ -62,10 +64,11 @@ check_user_interactive(int validated, int mode, struct passwd *auth_pw) if (ISSET(mode, MODE_IGNORE_TICKET)) SET(validated, FLAG_CHECK_USER); - if (build_timestamp(auth_pw) == -1) - goto done; + /* Open timestamp file and check its status. */ + cookie = timestamp_open(user_name, user_sid); + if (timestamp_lock(cookie, auth_pw)) + status = timestamp_status(cookie, auth_pw); - status = timestamp_status(auth_pw); switch (status) { case TS_FATAL: /* Fatal error (usually setuid failure), unsafe to proceed. */ @@ -97,21 +100,23 @@ check_user_interactive(int validated, int mode, struct passwd *auth_pw) goto done; rval = verify_user(auth_pw, prompt, validated, NULL); /* XXX */ - if (rval == true && lectured) { - if (set_lectured() == -1) - rval = -1; - } + if (rval == true && lectured) + (void)set_lectured(); /* lecture error not fatal */ free(prompt); break; } - /* Only update timestamp if user was validated. */ + /* + * Only update timestamp if user was validated. + * Failure to update the timestamp is not a fatal error. + */ if (rval == true && ISSET(validated, VALIDATE_SUCCESS) && !ISSET(mode, MODE_IGNORE_TICKET) && status != TS_ERROR) { - if (update_timestamp(auth_pw) == -1) - rval = -1; + (void)timestamp_update(cookie, auth_pw); } done: + if (cookie != NULL) + timestamp_close(cookie); debug_return_int(rval); } diff --git a/plugins/sudoers/check.h b/plugins/sudoers/check.h index 5ef5b4f48..83ca9231d 100644 --- a/plugins/sudoers/check.h +++ b/plugins/sudoers/check.h @@ -26,9 +26,8 @@ #define TS_CURRENT 0 #define TS_OLD 1 #define TS_MISSING 2 -#define TS_NOFILE 3 -#define TS_ERROR 4 -#define TS_FATAL 5 +#define TS_ERROR 3 +#define TS_FATAL 4 /* * Time stamps are now stored in a single file which contains multiple @@ -41,6 +40,7 @@ #define TS_GLOBAL 0x01 #define TS_TTY 0x02 #define TS_PPID 0x03 +#define TS_LOCKEXCL 0x04 /* Time stamp flags */ #define TS_DISABLED 0x01 /* entry disabled */ @@ -61,9 +61,12 @@ struct timestamp_entry { } u; }; +void *timestamp_open(const char *user, pid_t sid); +void timestamp_close(void *vcookie); +bool timestamp_lock(void *vcookie, struct passwd *pw); +bool timestamp_update(void *vcookie, struct passwd *pw); +int timestamp_status(void *vcookie, struct passwd *pw); bool already_lectured(int status); -int update_timestamp(struct passwd *pw); -int build_timestamp(struct passwd *pw); -int timestamp_status(struct passwd *pw); +int set_lectured(void); #endif /* SUDOERS_CHECK_H */ diff --git a/plugins/sudoers/policy.c b/plugins/sudoers/policy.c index a92b7a7a9..3dafb7ebf 100644 --- a/plugins/sudoers/policy.c +++ b/plugins/sudoers/policy.c @@ -725,7 +725,7 @@ sudoers_policy_invalidate(int remove) user_cmnd = "kill"; /* XXX - plugin API should support a return value for fatal errors. */ - remove_timestamp(remove); + timestamp_remove(remove); sudoers_cleanup(); debug_return; diff --git a/plugins/sudoers/sudoers.h b/plugins/sudoers/sudoers.h index c76b9f489..cff82c4bb 100644 --- a/plugins/sudoers/sudoers.h +++ b/plugins/sudoers/sudoers.h @@ -238,8 +238,7 @@ bool user_is_exempt(void); char *expand_prompt(const char *old_prompt, const char *auth_user); /* timestamp.c */ -int remove_timestamp(bool); -int set_lectured(void); +int timestamp_remove(bool unlinkit); /* sudo_auth.c */ bool sudo_auth_needs_end_session(void); diff --git a/plugins/sudoers/timestamp.c b/plugins/sudoers/timestamp.c index 17efb07a6..81f0d5f35 100644 --- a/plugins/sudoers/timestamp.c +++ b/plugins/sudoers/timestamp.c @@ -21,6 +21,11 @@ #include #include #include +#if defined(HAVE_STDINT_H) +# include +#elif defined(HAVE_INTTYPES_H) +# include +#endif #ifdef HAVE_STRING_H # include #endif /* HAVE_STRING_H */ @@ -45,12 +50,30 @@ #define TIMESTAMP_OPEN_ERROR -1 #define TIMESTAMP_PERM_ERROR -2 -static char timestamp_file[PATH_MAX]; -static off_t timestamp_hint = (off_t)-1; -static struct timestamp_entry timestamp_key; +/* + * 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. + */ + +/* TODO: unlock on suspend, make locking interruptible */ +/* TODO: use pread/pwrite when possible */ + +struct ts_cookie { + int fd; + pid_t sid; + off_t pos; + char *fname; + 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) @@ -98,8 +121,7 @@ ts_find_record(int fd, struct timestamp_entry *key, struct timestamp_entry *entr debug_decl(ts_find_record, SUDOERS_DEBUG_AUTH) /* - * Look for a matching record. - * We don't match on the sid or actual time stamp. + * Find a matching record (does not match sid or time stamp value). */ while (read(fd, &cur, sizeof(cur)) == sizeof(cur)) { if (cur.size != sizeof(cur)) { @@ -120,73 +142,6 @@ ts_find_record(int fd, struct timestamp_entry *key, struct timestamp_entry *entr debug_return_bool(false); } -/* - * Find matching record to update or append a new one. - * Returns true if the entry was written successfully, else false. - */ -static bool -ts_update_record(int fd, struct timestamp_entry *entry, off_t timestamp_hint) -{ - struct timestamp_entry cur; - ssize_t nwritten; - off_t old_eof = (off_t)-1; - debug_decl(ts_update_record, SUDOERS_DEBUG_AUTH) - - /* First try the hint if one is given. */ - if (timestamp_hint != (off_t)-1) { - if (lseek(fd, timestamp_hint, SEEK_SET) != -1) { - if (read(fd, &cur, sizeof(cur)) == sizeof(cur)) { - if (ts_match_record(entry, &cur)) { - sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, - "found existing time stamp record using hint"); - goto found_it; - } - } - } - } - - /* Search for matching record. */ - sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, - "searching for time stamp record"); - lseek(fd, (off_t)0, SEEK_SET); - if (ts_find_record(fd, entry, &cur)) { - sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, - "found existing time stamp record"); -found_it: - /* back up over old record */ - lseek(fd, (off_t)0 - (off_t)cur.size, SEEK_CUR); - } else { - sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, - "appending new time stamp record"); - old_eof = lseek(fd, (off_t)0, SEEK_CUR); - } - - /* Overwrite existing record or append to end. */ - nwritten = write(fd, entry, sizeof(struct timestamp_entry)); - if ((size_t)nwritten == sizeof(struct timestamp_entry)) - debug_return_bool(true); - - if (nwritten == -1) { - log_warning(SLOG_SEND_MAIL, - N_("unable to write to %s"), timestamp_file); - } else { - log_warningx(SLOG_SEND_MAIL, - N_("unable to write to %s"), timestamp_file); - } - - /* Truncate on partial write to be safe. */ - if (nwritten > 0 && old_eof != (off_t)-1) { - 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_bool(false); -} - /* * Create a directory and any missing parent directories with the * specified mode. @@ -287,37 +242,17 @@ ts_secure_dir(char *path, bool make_it, bool quiet) } /* - * Fills in the timestamp_file[] global variable. - * Returns the length of timestamp_file. - */ -int -build_timestamp(struct passwd *pw) -{ - int len; - debug_decl(build_timestamp, SUDOERS_DEBUG_AUTH) - - len = snprintf(timestamp_file, sizeof(timestamp_file), "%s/%s", - def_timestampdir, user_name); - if (len <= 0 || (size_t)len >= sizeof(timestamp_file)) { - log_warningx(SLOG_SEND_MAIL, - N_("timestamp path too long: %s/%s"), def_timestampdir, user_name); - len = -1; - } - - debug_return_int(len); -} - -/* - * Open and lock the specified timestamp or lecture file. - * Returns open and locked file descriptor on success. + * 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 -open_timestamp(const char *path, int flags) +ts_open(const char *path, int flags) { bool uid_changed = false; int fd; - debug_decl(open_timestamp, SUDOERS_DEBUG_AUTH) + debug_decl(ts_open, SUDOERS_DEBUG_AUTH) if (timestamp_uid != 0) uid_changed = set_perms(PERM_TIMESTAMP); @@ -332,179 +267,320 @@ open_timestamp(const char *path, int flags) } } if (fd >= 0) - sudo_lock_file(fd, SUDO_LOCK); + (void)fcntl(fd, F_SETFD, FD_CLOEXEC); debug_return_int(fd); } -/* - * Update the time on the timestamp file/dir or create it if necessary. - * Returns true on success, false on failure or -1 on setuid failure. - */ -int -update_timestamp(struct passwd *pw) +static ssize_t +ts_write(int fd, const char *fname, struct timestamp_entry *entry) { - struct timestamp_entry entry; - int rval = false; - int fd = -1; - debug_decl(update_timestamp, SUDOERS_DEBUG_AUTH) + ssize_t nwritten; + off_t old_eof; + debug_decl(ts_write, SUDOERS_DEBUG_AUTH) - /* Zero timeout means don't update the time stamp file. */ - if (def_timestamp_timeout == 0) - goto done; + old_eof = lseek(fd, (off_t)0, SEEK_CUR); + nwritten = write(fd, entry, entry->size); + 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); + } - /* Check/create parent directories as needed. */ - if (!ts_secure_dir(def_timestampdir, true, false)) - goto done; - - /* Fill in time stamp. */ - memcpy(&entry, ×tamp_key, sizeof(struct timestamp_entry)); - if (sudo_gettime_mono(&entry.ts) == -1) { - log_warning(0, N_("unable to read the clock")); - goto done; + /* 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_size_t(-1); } - - /* Open time stamp file and lock it for exclusive access. */ - fd = open_timestamp(timestamp_file, O_RDWR|O_CREAT); - switch (fd) { - case TIMESTAMP_OPEN_ERROR: - log_warning(SLOG_SEND_MAIL, N_("unable to open %s"), timestamp_file); - goto done; - case TIMESTAMP_PERM_ERROR: - /* Already logged set_perms/restore_perms error. */ - rval = -1; - goto done; - } - - /* Update record or append a new one. */ - ts_update_record(fd, &entry, timestamp_hint); - close(fd); - - rval = true; - -done: - debug_return_int(rval); + debug_return_size_t(nwritten); } /* - * Check the timestamp file and directory and return their status. - * Returns one of TS_CURRENT, TS_OLD, TS_MISSING, TS_NOFILE, TS_ERROR. + * Full in struct timestamp_entry with the specified flags + * based on auth user pw. Does not set the time stamp. */ -int -timestamp_status(struct passwd *pw) +static void +ts_fill(struct timestamp_entry *entry, struct passwd *pw, int flags) { - struct timestamp_entry entry; - struct timespec diff, timeout; - int status = TS_ERROR; /* assume the worst */ struct stat sb; - int fd = -1; - debug_decl(timestamp_status, SUDOERS_DEBUG_AUTH) + debug_decl(ts_fill, SUDOERS_DEBUG_AUTH) - /* Reset time stamp offset hint. */ - timestamp_hint = (off_t)-1; - - /* Zero timeout means ignore time stamp files. */ - if (def_timestamp_timeout == 0) { - status = TS_OLD; /* XXX - could also be TS_MISSING */ - goto done; - } - - /* Ignore time stamp files in an insecure directory. */ - if (!ts_secure_dir(def_timestampdir, false, false)) { - if (errno != ENOENT) { - status = TS_ERROR; - goto done; - } - status = TS_MISSING; /* not insecure, just missing */ - } - - /* - * Create a key used for matching entries in the time stamp file. - * The actual time stamp in the key is used below as the time "now". - */ - memset(×tamp_key, 0, sizeof(timestamp_key)); - timestamp_key.version = TS_VERSION; - timestamp_key.size = sizeof(timestamp_key); - timestamp_key.type = TS_GLOBAL; /* may be overriden below */ + memset(entry, 0, sizeof(*entry)); + entry->version = TS_VERSION; + entry->size = sizeof(*entry); + entry->type = TS_GLOBAL; /* may be overriden below */ + entry->flags = flags; if (pw != NULL) { - timestamp_key.auth_uid = pw->pw_uid; + entry->auth_uid = pw->pw_uid; } else { - timestamp_key.flags = TS_ANYUID; + entry->flags |= TS_ANYUID; } - timestamp_key.sid = user_sid; + entry->sid = user_sid; if (def_tty_tickets) { if (user_ttypath != NULL && stat(user_ttypath, &sb) == 0) { /* tty-based time stamp */ - timestamp_key.type = TS_TTY; - timestamp_key.u.ttydev = sb.st_rdev; + entry->type = TS_TTY; + entry->u.ttydev = sb.st_rdev; } else { /* ppid-based time stamp */ - timestamp_key.type = TS_PPID; - timestamp_key.u.ppid = getppid(); + entry->type = TS_PPID; + entry->u.ppid = getppid(); } } - if (sudo_gettime_mono(×tamp_key.ts) == -1) { - log_warning(0, N_("unable to read the clock")); - status = TS_ERROR; + + debug_return; +} + +/* + * 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 = NULL; + char *fname = NULL; + int fd = -1; + debug_decl(timestamp_open, SUDOERS_DEBUG_AUTH) + + /* Zero timeout means don't use the time stamp file. */ + if (def_timestamp_timeout == 0) { + errno = ENOENT; + goto bad; } - /* If the time stamp dir is missing there is nothing to do. */ - if (status == TS_MISSING) - goto done; + /* Sanity check timestamp dir and create if missing. */ + if (!ts_secure_dir(def_timestampdir, true, false)) + goto bad; - /* Open time stamp file and lock it for exclusive access. */ - fd = open_timestamp(timestamp_file, O_RDWR); + /* 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; + } + fd = ts_open(fname, O_RDWR|O_CREAT); switch (fd) { case TIMESTAMP_OPEN_ERROR: - status = TS_MISSING; - goto done; + log_warning(SLOG_SEND_MAIL, N_("unable to open %s"), fname); + goto bad; case TIMESTAMP_PERM_ERROR: /* Already logged set_perms/restore_perms error. */ - status = TS_FATAL; + goto bad; + } + + /* XXX - if mtime on file predates boot time ignore/unlink? */ + + /* 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(cookie); + free(fname); + debug_return_ptr(NULL); +} + +/* + * 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; + 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 (lseek(cookie->fd, 0, SEEK_SET) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO, + "unable to seek to %lld", (long long)cookie->pos); + debug_return_bool(false); + } + sudo_lock_region(cookie->fd, SUDO_LOCK, sizeof(struct timestamp_entry)); + + /* 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) + 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) == -1) + debug_return_bool(false); + } + + /* Search for record that matches the key or append a new one. */ + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "searching for time stamp record"); + ts_fill(&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 time stamp 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 time stamp record"); + cookie->pos = lseek(cookie->fd, 0, SEEK_CUR); + if (ts_write(cookie->fd, cookie->fname, &cookie->key) == -1) + debug_return_bool(false); + } + sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO, + "time stamp position is %lld", (long long)cookie->pos); + + /* Unlock the TS_LOCKEXCL record. */ + lseek(cookie->fd, 0, SEEK_SET); + sudo_lock_region(cookie->fd, SUDO_UNLOCK, sizeof(struct timestamp_entry)); + + /* Lock the real record (may sleep). */ + lseek(cookie->fd, cookie->pos, SEEK_SET); + sudo_lock_region(cookie->fd, SUDO_LOCK, (off_t)entry.size); + + 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, timeout; + 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 (def_timestamp_timeout == 0) { + 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; } - /* Ignore and clear time stamp file if mtime predates boot time. */ - if (fstat(fd, &sb) == 0) { - struct timespec boottime, mtime; - - mtim_get(&sb, mtime); - if (get_boottime(&boottime) && sudo_timespeccmp(&mtime, &boottime, <)) { - ignore_result(ftruncate(fd, (off_t)0)); - status = TS_MISSING; - goto done; - } + /* + * Seek to the record position and read it. + * If the file was truncated we might get all zeroes, + * in which case we need to unlock and try again (XXX - todo) + */ + 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); + debug_return_int(TS_ERROR); + } + nread = read(cookie->fd, &entry, sizeof(entry)); + 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)); + /* XXX - could this happen if truncated? check. */ + debug_return_int(TS_ERROR); } - /* Read existing record, if any. */ - if (!ts_find_record(fd, ×tamp_key, &entry)) { - status = TS_MISSING; + /* 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; } - /* Set record position hint for use by update_timestamp() */ - timestamp_hint = lseek(fd, (off_t)0, SEEK_CUR); - if (timestamp_hint != (off_t)-1) - timestamp_hint -= entry.size; - 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 != timestamp_key.sid) { + 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). */ if (def_timestamp_timeout < 0) { + 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. */ - sudo_timespecsub(×tamp_key.ts, &entry.ts, &diff); + 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); timeout.tv_sec = 60 * def_timestamp_timeout; timeout.tv_nsec = ((60.0 * def_timestamp_timeout) - (double)timeout.tv_sec) * 1000000000.0; @@ -517,11 +593,11 @@ timestamp_status(struct passwd *pw) N_("ignoring time stamp from the future")); status = TS_OLD; SET(entry.flags, TS_DISABLED); - ts_update_record(fd, &entry, timestamp_hint); + ts_write(cookie->fd, cookie->fname, &entry); } #else /* Check for bogus (future) time in the stampfile. */ - sudo_timespecsub(&entry.ts, ×tamp_key.ts, &diff); + sudo_timespecsub(&entry.ts, &now, &diff); timeout.tv_sec *= 2; if (sudo_timespeccmp(&diff, &timeout, >)) { time_t tv_sec = (time_t)entry.ts.tv_sec; @@ -530,7 +606,7 @@ timestamp_status(struct passwd *pw) 4 + ctime(&tv_sec)); status = TS_OLD; SET(entry.flags, TS_DISABLED); - ts_update_record(fd, &entry, timestamp_hint); + ts_write(cookie->fd, cookie->fname, &entry); } #endif /* CLOCK_MONOTONIC */ } else { @@ -538,57 +614,79 @@ timestamp_status(struct passwd *pw) } done: - if (fd != -1) - close(fd); 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 rval = false; + debug_decl(timestamp_update, SUDOERS_DEBUG_AUTH) + + /* Zero timeout means don't use time stamp files. */ + if (def_timestamp_timeout == 0) { + 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; + } + + /* 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. */ + 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; + } + if (ts_write(cookie->fd, cookie->fname, &cookie->key) != -1) + rval = true; + +done: + debug_return_int(rval); +} + /* * 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 -remove_timestamp(bool unlink_it) +timestamp_remove(bool unlink_it) { - struct timestamp_entry entry; - int fd, rval = true; - debug_decl(remove_timestamp, SUDOERS_DEBUG_AUTH) + struct timestamp_entry key, entry; + int fd = -1, rval = true; + char *fname = NULL; + debug_decl(timestamp_remove, SUDOERS_DEBUG_AUTH) - if (build_timestamp(NULL) == -1) { + if (asprintf(&fname, "%s/%s", def_timestampdir, user_name) == -1) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); rval = -1; goto done; } /* For "sudo -K" simply unlink the time stamp file. */ if (unlink_it) { - rval = unlink(timestamp_file) ? -1 : true; + rval = unlink(fname) ? -1 : true; goto done; } - /* - * Create a key used for matching entries in the time stamp file. - */ - memset(×tamp_key, 0, sizeof(timestamp_key)); - timestamp_key.version = TS_VERSION; - timestamp_key.size = sizeof(timestamp_key); - timestamp_key.type = TS_GLOBAL; /* may be overriden below */ - timestamp_key.flags = TS_ANYUID; - if (def_tty_tickets) { - struct stat sb; - if (user_ttypath != NULL && stat(user_ttypath, &sb) == 0) { - /* tty-based time stamp */ - timestamp_key.type = TS_TTY; - timestamp_key.u.ttydev = sb.st_rdev; - } else { - /* ppid-based time stamp */ - timestamp_key.type = TS_PPID; - timestamp_key.u.ppid = getppid(); - } - } - /* Open time stamp file and lock it for exclusive access. */ - fd = open_timestamp(timestamp_file, O_RDWR); + fd = ts_open(fname, O_RDWR); switch (fd) { case TIMESTAMP_OPEN_ERROR: if (errno != ENOENT) @@ -599,23 +697,25 @@ remove_timestamp(bool unlink_it) rval = -1; goto done; } + /* Lock first record to gain exclusive access. */ + sudo_lock_region(fd, SUDO_LOCK, sizeof(struct timestamp_entry)); /* * Find matching entries and invalidate them. */ - while (ts_find_record(fd, ×tamp_key, &entry)) { - /* Set record position hint for use by update_timestamp() */ - timestamp_hint = lseek(fd, (off_t)0, SEEK_CUR); - if (timestamp_hint != (off_t)-1) - timestamp_hint -= (off_t)entry.size; - /* Disable the entry. */ + ts_fill(&key, NULL, TS_DISABLED); + while (ts_find_record(fd, &key, &entry)) { + /* Back up and disable the entry. */ + lseek(fd, (off_t)0 - sizeof(entry), SEEK_CUR); SET(entry.flags, TS_DISABLED); - if (!ts_update_record(fd, &entry, timestamp_hint)) + if (ts_write(fd, fname, &entry) == -1) rval = false; } - close(fd); done: + if (fd != -1) + close(fd); + free(fname); debug_return_int(rval); } @@ -666,7 +766,7 @@ set_lectured(void) goto done; /* Create lecture file. */ - fd = open_timestamp(lecture_status, O_RDWR|O_CREAT|O_TRUNC); + fd = ts_open(lecture_status, O_WRONLY|O_CREAT|O_EXCL); switch (fd) { case TIMESTAMP_OPEN_ERROR: /* Failed to open, not a fatal error. */