]> granicus.if.org Git - sudo/commitdiff
Change to a single event loop in sudoreplay and use signal events.
authorTodd C. Miller <Todd.Miller@courtesan.com>
Thu, 27 Jul 2017 15:45:35 +0000 (09:45 -0600)
committerTodd C. Miller <Todd.Miller@courtesan.com>
Thu, 27 Jul 2017 15:45:35 +0000 (09:45 -0600)
plugins/sudoers/sudoreplay.c

index 3504c8166f7bf69776a88b5cbd56c9e8b6a04189..f4ea7ce508d4d1c54f455ac90b71e0fcff79cfc6 100644 (file)
@@ -91,27 +91,35 @@ struct log_info {
     int cols;
 };
 
-/*
- * I/O log timing entry.
- */
-struct iolog_timing {
-    double seconds;
-    int idx;
-    union {
-       struct {
-           int rows;
-           int cols;
-       } winsize;
-       size_t nbytes;
-    } u;
-};
-
-/* Closure for write_output */
-struct write_closure {
-    struct sudo_event *wevent;
-    struct iovec *iov;
-    unsigned int iovcnt;
-    size_t nbytes;
+struct replay_closure {
+    struct sudo_event_base *evbase;
+    struct sudo_event *delay_ev;
+    struct sudo_event *keyboard_ev;
+    struct sudo_event *output_ev;
+    struct sudo_event *sighup_ev;
+    struct sudo_event *sigint_ev;
+    struct sudo_event *sigquit_ev;
+    struct sudo_event *sigterm_ev;
+    struct sudo_event *sigtstp_ev;
+    struct timing_closure {
+       const char *decimal;
+       double max_delay;
+       int idx;
+       union {
+           struct {
+               int rows;
+               int cols;
+           } winsize;
+           size_t nbytes; // XXX
+       } u;
+    } timing;
+    bool interactive;
+    struct io_buffer {
+       unsigned int len; /* buffer length (how much produced) */
+       unsigned int off; /* write position (how much already consumed) */
+       unsigned int toread; /* how much remains to be read */
+       char buf[64 * 1024];
+    } iobuf;
 };
 
 /*
@@ -181,14 +189,13 @@ extern time_t get_date(char *);
 static int list_sessions(int, char **, const char *, const char *, const char *);
 static int open_io_fd(char *path, int len, struct io_log_file *iol);
 static int parse_expr(struct search_node_list *, char **, bool);
-static int parse_timing(const char *buf, const char *decimal, struct iolog_timing *timing);
+static bool parse_timing(const char *buf, double *seconds, struct timing_closure *timing);
 static struct log_info *parse_logfile(char *logfile);
-static void check_input(int fd, int what, void *v);
+static void read_keyboard(int fd, int what, void *v);
 static void free_log_info(struct log_info *li);
 static void help(void) __attribute__((__noreturn__));
-static void replay_session(const double max_wait, const char *decimal, bool interactive);
+static int replay_session(double max_wait, const char *decimal, bool interactive);
 static void sudoreplay_cleanup(void);
-static void sudoreplay_handler(int);
 static void usage(int);
 static void write_output(int fd, int what, void *v);
 static void restore_terminal_size(void);
@@ -219,14 +226,14 @@ main(int argc, char *argv[])
     const char *decimal, *id, *user = NULL, *pattern = NULL, *tty = NULL;
     char *cp, *ep, path[PATH_MAX];
     struct log_info *li;
-    double max_wait = 0;
+    double max_delay = 0;
     debug_decl(main, SUDO_DEBUG_MAIN)
 
 #if defined(SUDO_DEVEL) && defined(__OpenBSD__)
     {
        extern char *malloc_options;
        malloc_options = "S";
-    }  
+    }
 #endif
 
     initprogname(argc > 0 ? argv[0] : "sudoreplay");
@@ -275,7 +282,7 @@ main(int argc, char *argv[])
            break;
        case 'm':
            errno = 0;
-           max_wait = strtod(optarg, &ep);
+           max_delay = strtod(optarg, &ep);
            if (*ep != '\0' || errno != 0)
                sudo_fatalx(U_("invalid max wait: %s"), optarg);
            break;
@@ -337,7 +344,7 @@ main(int argc, char *argv[])
 
     /* Open files for replay, applying replay filter for the -f flag. */
     for (idx = 0; idx < IOFD_MAX; idx++) {
-       if (open_io_fd(path, plen, &io_log_files[idx]) == -1) 
+       if (open_io_fd(path, plen, &io_log_files[idx]) == -1)
            sudo_fatal(U_("unable to open %s"), path);
     }
 
@@ -360,7 +367,7 @@ main(int argc, char *argv[])
     li = NULL;
 
     /* Replay session corresponding to io_log_files[]. */
-    replay_session(max_wait, decimal, interactive);
+    exitcode = replay_session(max_delay, decimal, interactive);
 
     restore_terminal_size();
     sudo_term_restore(ttyfd, true);
@@ -393,6 +400,20 @@ io_log_read(int idx, char *buf, size_t nbytes)
     debug_return_ssize_t(nread);
 }
 
+static int
+io_log_eof(int idx)
+{
+    int ret;
+    debug_decl(io_log_eof, SUDO_DEBUG_UTIL)
+
+#ifdef HAVE_ZLIB_H
+    ret = gzeof(io_log_files[idx].fd.g);
+#else
+    ret = feof(io_log_files[idx].fd.f);
+#endif
+    debug_return_int(ret);
+}
+
 static char *
 io_log_gets(int idx, char *buf, size_t nbytes)
 {
@@ -608,6 +629,7 @@ xterm_set_size(int rows, int cols)
     char buf[1024];
     debug_decl(xterm_set_size, SUDO_DEBUG_UTIL)
 
+    /* XXX - save cursor and position restore after resizing */
     len = snprintf(buf, sizeof(buf), setsize_fmt, rows, cols);
     if (len < 0 || len >= (int)sizeof(buf)) {
        /* not possible due to size of buf */
@@ -620,6 +642,7 @@ xterm_set_size(int rows, int cols)
            "%s: error writing xterm resize request", __func__);
        goto done;
     }
+    /* XXX - keyboard input will interfere with this */
     if (!xterm_get_size(&new_rows, &new_cols))
        goto done;
     if (rows == new_rows && cols == new_cols)
@@ -732,197 +755,277 @@ restore_terminal_size(void)
     debug_return;
 }
 
-static void
-replay_session(const double max_wait, const char *decimal, bool interactive)
+/*
+ * Read the next record from the timing file and schedule a delay
+ * event with the specified timeout.
+ * Return 0 on success, 1 on EOF and -1 on error.
+ */
+static int
+read_timing_record(struct replay_closure *closure)
 {
-    struct sudo_event *input_ev, *output_ev;
-    unsigned int i, iovcnt = 0, iovmax = 0;
-    struct sudo_event_base *evbase;
-    struct iovec iovb, *iov = &iovb;
-    struct write_closure wc;
+    struct timeval timeout;
     char buf[LINE_MAX];
-    struct sigaction sa;
-    debug_decl(replay_session, SUDO_DEBUG_UTIL)
+    double delay;
+    debug_decl(read_timing_record, SUDO_DEBUG_UTIL)
+
+    /* Read next record from timing file. */
+    if (io_log_gets(IOFD_TIMING, buf, sizeof(buf)) == NULL) {
+       /* EOF or error reading timing file, we are done. */
+       debug_return_int(io_log_eof(IOFD_TIMING) ? 1 : -1);
+    }
 
-    /* Restore terminal if interrupted. */
-    memset(&sa, 0, sizeof(sa));
-    sigemptyset(&sa.sa_mask);
-    sa.sa_flags = SA_RESETHAND;
-    sa.sa_handler = sudoreplay_handler;
-    (void) sigaction(SIGINT, &sa, NULL);
-    (void) sigaction(SIGTERM, &sa, NULL);
-    (void) sigaction(SIGHUP, &sa, NULL);
-    (void) sigaction(SIGQUIT, &sa, NULL);
-
-    /* Don't suspend as we cannot restore the screen on resume. */
-    sa.sa_flags = SA_RESTART;
-    sa.sa_handler = SIG_IGN;
-    (void) sigaction(SIGTSTP, &sa, NULL);
+    /* Parse timing file record. */
+    buf[strcspn(buf, "\n")] = '\0';
+    if (!parse_timing(buf, &delay, &closure->timing))
+       sudo_fatalx(U_("invalid timing file line: %s"), buf);
+
+    /* Record number bytes to read. */
+    /* XXX - remove timing->nbytes? */
+    if (closure->timing.idx != IOFD_TIMING) {
+       closure->iobuf.len = 0;
+       closure->iobuf.off = 0;
+       closure->iobuf.toread = closure->timing.u.nbytes;
+    }
+
+    /* Adjust delay using speed factor and clamp to max_delay */
+    delay /= speed_factor;
+    if (closure->timing.max_delay && delay > closure->timing.max_delay)
+       delay = closure->timing.max_delay;
+
+    /* Convert delay to a timeval. */
+    timeout.tv_sec = delay;
+    timeout.tv_usec = (delay - timeout.tv_sec) * 1000000.0;
+
+    /* Schedule the delay event. */
+    if (sudo_ev_add(closure->evbase, closure->delay_ev, &timeout, false) == -1)
+       sudo_fatal(U_("unable to add event to queue"));
+
+    debug_return_int(0);
+}
+
+static bool
+fill_iobuf(struct replay_closure *closure)
+{
+    const size_t space = sizeof(closure->iobuf.buf) - closure->iobuf.len;
+    const struct timing_closure *timing = &closure->timing;
+    ssize_t nread;
+    size_t len;
+    debug_decl(fill_iobuf, SUDO_DEBUG_UTIL)
+
+    if (closure->iobuf.toread != 0 && space != 0) {
+       len = closure->iobuf.toread < space ? closure->iobuf.toread : space;
+       nread = io_log_read(timing->idx,
+           closure->iobuf.buf + closure->iobuf.off, len);
+       if (nread <= 0) {
+           if (nread == 0) {
+               sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+                   "%s: premature EOF, expected %u bytes",
+                   io_log_files[timing->idx].suffix, closure->iobuf.toread);
+           } else {
+               sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO,
+                   "%s: read error", io_log_files[timing->idx].suffix);
+           }
+           sudo_warnx(U_("unable to read %s"),
+               io_log_files[timing->idx].suffix);
+           debug_return_bool(false);
+       }
+       closure->iobuf.toread -= nread;
+       closure->iobuf.len += nread;
+    }
+
+    debug_return_bool(true);
+}
+
+/*
+ * Called when the inter-record delay has expired.
+ * Depending on the record type, either reads the next
+ * record or changes window size.
+ */
+static void
+delay_cb(int fd, int what, void *v)
+{
+    struct replay_closure *closure = v;
+    const struct timing_closure *timing = &closure->timing;
+    debug_decl(delay_cb, SUDO_DEBUG_UTIL)
 
+    /* Delay done, read I/O log record or change window size. */
+    if (timing->idx == IOFD_TIMING) {
+       resize_terminal(timing->u.winsize.rows, timing->u.winsize.cols);
+       switch (read_timing_record(closure)) {
+       case 0:
+           /* success */
+           break;
+       case 1:
+           /* EOF */
+           sudo_ev_loopexit(closure->evbase);
+           break;
+       default:
+           /* error */
+           sudo_ev_loopbreak(closure->evbase);
+           break;
+       }
+       debug_return;
+    }
+
+    /* Even if we are not replaying, we still have to delay. */
+    if (timing->idx >= IOFD_MAX || io_log_files[timing->idx].fd.v == NULL)
+       debug_return;
+
+    /* Enable write event. */
+    if (sudo_ev_add(closure->evbase, closure->output_ev, NULL, false) == -1)
+       sudo_fatal(U_("unable to add event to queue"));
+
+    debug_return;
+}
+
+static void
+replay_closure_free(struct replay_closure *closure)
+{
     /*
-     * Setup event base and input/output events.
-     * If interactive, take input from and write to /dev/tty.
-     * If not interactive, delay instead of reading input and write to stdout.
+     * Free events and event base, then the closure itself.
      */
-    evbase = sudo_ev_base_alloc();
-    if (evbase == NULL)
-       sudo_fatal(NULL);
-    input_ev = sudo_ev_alloc(ttyfd, interactive ? SUDO_EV_READ :
-       SUDO_EV_TIMEOUT, check_input, sudo_ev_self_cbarg());
-    if (input_ev == NULL)
-        sudo_fatal(NULL);
-    output_ev = sudo_ev_alloc(interactive ? ttyfd : STDOUT_FILENO,
-       SUDO_EV_WRITE, write_output, &wc);
-    if (output_ev == NULL)
-        sudo_fatal(NULL);
+    sudo_ev_free(closure->delay_ev);
+    sudo_ev_free(closure->keyboard_ev);
+    sudo_ev_free(closure->output_ev);
+    sudo_ev_free(closure->sighup_ev);
+    sudo_ev_free(closure->sigint_ev);
+    sudo_ev_free(closure->sigquit_ev);
+    sudo_ev_free(closure->sigterm_ev);
+    sudo_ev_free(closure->sigtstp_ev);
+    sudo_ev_base_free(closure->evbase);
+    free(closure);
+}
+
+static void
+signal_cb(int signo, int what, void *v)
+{
+    struct replay_closure *closure = v;
+    debug_decl(signal_cb, SUDO_DEBUG_UTIL)
+
+    switch (signo) {
+    case SIGHUP:
+    case SIGINT:
+    case SIGQUIT:
+    case SIGTERM:
+       /* Free the event base and restore signal handlers. */
+       replay_closure_free(closure);
+
+       /* Restore the terminal and die. */
+       sudoreplay_cleanup();
+       kill(getpid(), signo);
+       break;
+    case SIGTSTP:
+       /* Ignore ^Z since we have no way to restore the screen. */
+       break;
+    }
+
+    debug_return;
+}
+
+static struct replay_closure *
+replay_closure_alloc(double max_delay, const char *decimal, bool interactive)
+{
+    struct replay_closure *closure;
+    debug_decl(replay_closure_alloc, SUDO_DEBUG_UTIL)
+
+    if ((closure = calloc(1, sizeof(*closure))) == NULL)
+       debug_return_ptr(NULL);
+
+    closure->interactive = interactive;
+    closure->timing.max_delay = max_delay;
+    closure->timing.decimal = decimal;
 
     /*
-     * Read each line of the timing file, displaying the output streams.
+     * Setup event base and delay, input and output events.
+     * If interactive, take input from and write to /dev/tty.
+     * If not interactive there is no input event.
      */
-    while (io_log_gets(IOFD_TIMING, buf, sizeof(buf)) != NULL) {
-       size_t len, nread;
-       double to_wait;
-       struct timeval timeout;
-       struct iolog_timing timing;
-       bool need_nlcr = false;
-       char last_char = '\0';
-
-       buf[strcspn(buf, "\n")] = '\0';
-       if (!parse_timing(buf, decimal, &timing))
-           sudo_fatalx(U_("invalid timing file line: %s"), buf);
-
-       /* Adjust delay using speed factor and clamp to max_wait */
-       to_wait = timing.seconds / speed_factor;
-       if (max_wait && to_wait > max_wait)
-           to_wait = max_wait;
-
-       /* Convert delay to a timeval. */
-       timeout.tv_sec = to_wait;
-       timeout.tv_usec = (to_wait - timeout.tv_sec) * 1000000.0;
-
-       /* Run event event loop to delay and get keyboard input. */
-       if (sudo_ev_add(evbase, input_ev, &timeout, false) == -1)
+    closure->evbase = sudo_ev_base_alloc();
+    if (closure->evbase == NULL)
+       goto bad;
+    closure->delay_ev = sudo_ev_alloc(-1, SUDO_EV_TIMEOUT, delay_cb, closure);
+    if (closure->delay_ev == NULL)
+        goto bad;
+    if (interactive) {
+       closure->keyboard_ev = sudo_ev_alloc(ttyfd, SUDO_EV_READ|SUDO_EV_PERSIST,
+           read_keyboard, closure);
+       if (closure->keyboard_ev == NULL)
+           goto bad;
+       if (sudo_ev_add(closure->evbase, closure->keyboard_ev, NULL, false) == -1)
            sudo_fatal(U_("unable to add event to queue"));
-       sudo_ev_loop(evbase, 0);
+    }
+    closure->output_ev = sudo_ev_alloc(interactive ? ttyfd : STDOUT_FILENO,
+       SUDO_EV_WRITE, write_output, closure);
+    if (closure->output_ev == NULL)
+        goto bad;
 
-       if (timing.idx == IOFD_TIMING) {
-           resize_terminal(timing.u.winsize.rows, timing.u.winsize.cols);
-           continue;
-       }
+    /*
+     * Setup signal events, we need to restore the terminal if killed.
+     */
+    closure->sighup_ev = sudo_ev_alloc(SIGHUP, SUDO_EV_SIGNAL, signal_cb,
+       closure);
+    if (closure->sighup_ev == NULL)
+       goto bad;
+    if (sudo_ev_add(closure->evbase, closure->sighup_ev, NULL, false) == -1)
+       sudo_fatal(U_("unable to add event to queue"));
 
-       /* Even if we are not replaying, we still have to delay. */
-       if (timing.idx >= IOFD_MAX || io_log_files[timing.idx].fd.v == NULL)
-           continue;
+    closure->sigint_ev = sudo_ev_alloc(SIGINT, SUDO_EV_SIGNAL, signal_cb,
+       closure);
+    if (closure->sigint_ev == NULL)
+       goto bad;
+    if (sudo_ev_add(closure->evbase, closure->sigint_ev, NULL, false) == -1)
+       sudo_fatal(U_("unable to add event to queue"));
 
-       /* Check whether we need to convert newline to CR LF pairs. */
-       if (interactive) 
-           need_nlcr = (timing.idx == IOFD_STDOUT || timing.idx == IOFD_STDERR);
+    closure->sigquit_ev = sudo_ev_alloc(SIGQUIT, SUDO_EV_SIGNAL, signal_cb,
+       closure);
+    if (closure->sigquit_ev == NULL)
+       goto bad;
+    if (sudo_ev_add(closure->evbase, closure->sigquit_ev, NULL, false) == -1)
+       sudo_fatal(U_("unable to add event to queue"));
 
-       /* All output is sent to stdout. */
-       /* XXX - assumes no wall clock time spent writing output. */
-       while (timing.u.nbytes != 0) {
-           if (timing.u.nbytes > sizeof(buf))
-               len = sizeof(buf);
-           else
-               len = timing.u.nbytes;
-           nread = io_log_read(timing.idx, buf, len);
-           if (nread <= 0) {
-               if (nread == 0) {
-                   sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
-                       "%s: premature EOF, expected %zu bytes",
-                       io_log_files[timing.idx].suffix, timing.u.nbytes);
-               } else {
-                   sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO|SUDO_DEBUG_LINENO,
-                       "%s: read error", io_log_files[timing.idx].suffix);
-               }
-               break;
-           }
-           timing.u.nbytes -= nread;
-
-           /* Convert newline to carriage return + linefeed if needed. */
-           if (need_nlcr) {
-               size_t remainder = nread;
-               size_t linelen;
-               char *line = buf;
-               char *nl, *cp = buf;
-
-               /*
-                * Handle a "\r\n" pair that spans a buffer.
-                * The newline will be written as part of the next line.
-                */
-               if (last_char == '\r' && *cp == '\n') {
-                   cp++;
-                   remainder--;
-               }
+    closure->sigterm_ev = sudo_ev_alloc(SIGTERM, SUDO_EV_SIGNAL, signal_cb,
+       closure);
+    if (closure->sigterm_ev == NULL)
+       goto bad;
+    if (sudo_ev_add(closure->evbase, closure->sigterm_ev, NULL, false) == -1)
+       sudo_fatal(U_("unable to add event to queue"));
 
-               iovcnt = 0;
-               while ((nl = memchr(cp, '\n', remainder)) != NULL) {
-                   /*
-                    * If there is already a carriage return, keep going.
-                    * We'll include it as part of the next line written.
-                    */
-                   if (cp != nl && nl[-1] == '\r') {
-                       remainder = (size_t)(&buf[nread - 1] - nl);
-                       cp = nl + 1;
-                       continue;
-                   }
-
-                   /* Store the line in iov followed by \r\n pair. */
-                   if (iovcnt + 3 > iovmax) {
-                       iov = iovmax ?
-                           reallocarray(iov, iovmax <<= 1, sizeof(*iov)) :
-                           reallocarray(NULL, iovmax = 32, sizeof(*iov));
-                       if (iov == NULL)
-                           sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
-                   }
-                   linelen = (size_t)(nl - line) + 1;
-                   iov[iovcnt].iov_base = line;
-                   iov[iovcnt].iov_len = linelen - 1; /* not including \n */
-                   iovcnt++;
-                   iov[iovcnt].iov_base = "\r\n";
-                   iov[iovcnt].iov_len = 2;
-                   iovcnt++;
-                   line = cp = nl + 1;
-                   remainder -= linelen;
-               }
-               if ((size_t)(line - buf) != nread) {
-                   /*
-                    * Partial line without a linefeed or multiple lines
-                    * that already had \r\n pairs.
-                    */
-                   iov[iovcnt].iov_base = line;
-                   iov[iovcnt].iov_len = nread - (line - buf);
-                   iovcnt++;
-               }
-               last_char = buf[nread - 1]; /* stash last char of old buffer */
-           } else {
-               /* No conversion needed. */
-               iov[0].iov_base = buf;
-               iov[0].iov_len = nread;
-               iovcnt = 1;
-           }
+    closure->sigtstp_ev = sudo_ev_alloc(SIGTSTP, SUDO_EV_SIGNAL, signal_cb,
+       closure);
+    if (closure->sigtstp_ev == NULL)
+       goto bad;
+    if (sudo_ev_add(closure->evbase, closure->sigtstp_ev, NULL, false) == -1)
+       sudo_fatal(U_("unable to add event to queue"));
 
-           /* Setup closure for write_output. */
-           wc.wevent = output_ev;
-           wc.iov = iov;
-           wc.iovcnt = iovcnt;
-           wc.nbytes = 0;
-           for (i = 0; i < iovcnt; i++)
-               wc.nbytes += iov[i].iov_len;
-
-           /* Run event event loop to write output. */
-           /* XXX - should use a single event loop with a circular buffer. */
-           if (sudo_ev_add(evbase, output_ev, NULL, false) == -1)
-               sudo_fatal(U_("unable to add event to queue"));
-           sudo_ev_loop(evbase, 0);
-       }
+    debug_return_ptr(closure);
+bad:
+    replay_closure_free(closure);
+    debug_return_ptr(NULL);
+}
+
+static int
+replay_session(double max_delay, const char *decimal, bool interactive)
+{
+    struct replay_closure *closure;
+    int ret = 0;
+    debug_decl(replay_session, SUDO_DEBUG_UTIL)
+
+    /* Allocate the delay closure and read the first timing record. */
+    closure = replay_closure_alloc(max_delay, decimal, interactive);
+    if (read_timing_record(closure) != 0) {
+       ret = 1;
+       goto done;
     }
-    if (iov != &iovb)
-       free(iov);
-    sudo_ev_base_free(evbase);
-    sudo_ev_free(input_ev);
-    sudo_ev_free(output_ev);
-    debug_return;
+
+    /* Run event loop. */
+    sudo_ev_loop(closure->evbase, 0);
+    if (sudo_ev_got_break(closure->evbase))
+       ret = 1;
+
+done:
+    /* Clean up and return. */
+    replay_closure_free(closure);
+    debug_return_int(ret);
 }
 
 static int
@@ -943,48 +1046,82 @@ open_io_fd(char *path, int len, struct io_log_file *iol)
     debug_return_int(iol->fd.v ? 0 : -1);
 }
 
+/*
+ * Write the I/O buffer.
+ */
 static void
 write_output(int fd, int what, void *v)
 {
-    struct write_closure *wc = v;
-    size_t nwritten;
-    unsigned int i;
+    struct replay_closure *closure = v;
+    struct io_buffer *iobuf = &closure->iobuf;
+    unsigned iovcnt = 1;
+    struct iovec iov[2];
+    bool need_nlcr = false;
+    size_t nbytes, nwritten;
     debug_decl(write_output, SUDO_DEBUG_UTIL)
 
-    nwritten = writev(fd, wc->iov, wc->iovcnt);
+    /* Refill iobuf if there is more to read and buf is empty. */
+    if (!fill_iobuf(closure)) {
+       sudo_ev_loopbreak(closure->evbase);
+       debug_return;
+    }
+
+    if (closure->interactive)
+       need_nlcr = (fd == IOFD_STDOUT || fd == IOFD_STDERR);
+
+    nbytes = iobuf->len - iobuf->off;
+    iov[0].iov_base = iobuf->buf + iobuf->off;
+    iov[0].iov_len = nbytes;
+
+    if (need_nlcr) {
+       char *nl;
+
+       /* We may need to add a carriage return after the newline. */
+       nl = memchr(iov[0].iov_base, '\n', iov[0].iov_len);
+       if (nl != NULL) {
+           iov[0].iov_len--;   /* skip the existing newline */
+           iov[1].iov_base = "\r\n";
+           iov[1].iov_len = 2;
+           iovcnt = 2;
+           nbytes++;           /* account for the added carriage return */
+       }
+    }
+
+    nwritten = writev(fd, iov, iovcnt);
     switch ((ssize_t)nwritten) {
     case -1:
        if (errno != EINTR && errno != EAGAIN)
            sudo_fatal(U_("unable to write to %s"), "stdout");
        break;
-    case 0:
-       break;
     default:
-       if (wc->nbytes == nwritten) {
-           /* writev completed */
-           debug_return;
-       }
-
-       /* short writev, adjust iov so we can write the remainder. */
-       wc->nbytes -= nwritten;
-       i = wc->iovcnt;
-       while (i--) {
-           if (wc->iov[0].iov_len > nwritten) {
-               /* Partial write, adjust base and len and reschedule. */
-               wc->iov[0].iov_base = (char *)wc->iov[0].iov_base + nwritten;
-               wc->iov[0].iov_len -= nwritten;
-               break;
-           }
-           nwritten -= wc->iov[0].iov_len;
-           wc->iov++;
-           wc->iovcnt--;
+       if (iovcnt == 2 && nwritten == nbytes) {
+               /* subtract one for the carriage return we added above. */
+               nwritten--;
        }
+       iobuf->off += nwritten;
        break;
     }
 
-    /* Reschedule event to write remainder. */
-    if (sudo_ev_add(NULL, wc->wevent, NULL, false) == -1)
-       sudo_fatal(U_("unable to add event to queue"));
+    if (iobuf->off == iobuf->len) {
+       /* Write complete, go to next timing entry if possible. */
+       switch (read_timing_record(closure)) {
+       case 0:
+           /* success */
+           break;
+       case 1:
+           /* EOF */
+           sudo_ev_loopexit(closure->evbase);
+           break;
+       default:
+           /* error */
+           sudo_ev_loopbreak(closure->evbase);
+           break;
+       }
+    } else {
+       /* Reschedule event to write remainder. */
+       if (sudo_ev_add(NULL, closure->output_ev, NULL, false) == -1)
+           sudo_fatal(U_("unable to add event to queue"));
+    }
     debug_return;
 }
 
@@ -1473,62 +1610,82 @@ list_sessions(int argc, char **argv, const char *pattern, const char *user,
 }
 
 /*
- * Check input for ' ', '<', '>', return
+ * Check keyboard for ' ', '<', '>', return
  * pause, slow, fast, next
  */
 static void
-check_input(int fd, int what, void *v)
+read_keyboard(int fd, int what, void *v)
 {
-    struct sudo_event *ev = v;
-    struct timeval tv, *timeout = NULL;
-    static bool paused = 0;
+    struct replay_closure *closure = v;
+    static bool paused = false;
+    struct timeval tv;
     char ch;
-    debug_decl(check_input, SUDO_DEBUG_UTIL)
+    debug_decl(read_keyboard, SUDO_DEBUG_UTIL)
 
-    if (ISSET(what, SUDO_EV_READ)) {
-       switch (read(fd, &ch, 1)) {
-       case -1:
-           if (errno != EINTR && errno != EAGAIN)
-               sudo_fatal(U_("unable to read %s"), "stdin");
+    switch (read(fd, &ch, 1)) {
+    case -1:
+       if (errno != EINTR && errno != EAGAIN)
+           sudo_fatal(U_("unable to read %s"), "stdin");
+       break;
+    case 0:
+       /* Ignore EOF. */
+       break;
+    default:
+       if (paused) {
+           /* Any key will unpause, run the delay callback directly. */
+           paused = false;
+           delay_cb(-1, SUDO_EV_TIMEOUT, closure);
+           debug_return;
+       }
+       switch (ch) {
+       case ' ':
+           paused = true;
+           /* Disable the delay event until we unpause. */
+           sudo_ev_del(closure->evbase, closure->delay_ev);
            break;
-       case 0:
-           /* Ignore EOF. */
+       case '<':
+           speed_factor /= 2;
+            sudo_ev_get_timeleft(closure->delay_ev, &tv);
+            if (sudo_timevalisset(&tv)) {
+               /* Double remaining timeout. */
+               tv.tv_sec *= 2;
+               tv.tv_usec *= 2;
+               if (tv.tv_usec >= 1000000) {
+                   tv.tv_sec++;
+                   tv.tv_usec -= 1000000;
+               }
+               if (sudo_ev_add(NULL, closure->delay_ev, &tv, false) == -1) {
+                   sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+                       "failed to double remaining delay timeout");
+               }
+            }
+           break;
+       case '>':
+           speed_factor *= 2;
+            sudo_ev_get_timeleft(closure->delay_ev, &tv);
+            if (sudo_timevalisset(&tv)) {
+               /* Halve remaining timeout. */
+               if (tv.tv_sec & 1)
+                   tv.tv_usec += 500000;
+               tv.tv_sec /= 2;
+               tv.tv_usec /= 2;
+               if (sudo_ev_add(NULL, closure->delay_ev, &tv, false) == -1) {
+                   sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
+                       "failed to halve remaining delay timeout");
+               }
+            }
+           break;
+       case '\r':
+       case '\n':
+           /* Cancel existing delay, run callback directly. */
+           sudo_ev_del(closure->evbase, closure->delay_ev);
+           delay_cb(-1, SUDO_EV_TIMEOUT, closure);
            break;
        default:
-           if (paused) {
-               /* Any key will unpause, event is finished. */
-               /* XXX - pause time could be less than timeout */
-               paused = false;
-               debug_return; /* XXX */
-           }
-           switch (ch) {
-           case ' ':
-               paused = true;
-               break;
-           case '<':
-               speed_factor /= 2;
-               break;
-           case '>':
-               speed_factor *= 2;
-               break;
-           case '\r':
-           case '\n':
-               debug_return; /* XXX */
-           }
+           /* Unknown key, nothing to do. */
            break;
        }
-       if (!paused) {
-           /* Determine remaining timeout, if any. */
-           sudo_ev_get_timeleft(ev, &tv);
-           if (!sudo_timevalisset(&tv)) {
-               /* No time left, event is done. */
-               debug_return;
-           }
-           timeout = &tv;
-       }
-       /* Re-enable event. */
-       if (sudo_ev_add(NULL, ev, timeout, false) == -1)
-           sudo_fatal(U_("unable to add event to queue"));
+       break;
     }
     debug_return;
 }
@@ -1538,10 +1695,10 @@ check_input(int fd, int what, void *v)
  *     index sleep_time num_bytes
  * Where index is IOFD_*, sleep_time is the number of seconds to sleep
  * before writing the data and num_bytes is the number of bytes to output.
- * Returns 1 on success and 0 on failure.
+ * Returns true on success and false on failure.
  */
-static int
-parse_timing(const char *buf, const char *decimal, struct iolog_timing *timing)
+static bool
+parse_timing(const char *buf, double *seconds, struct timing_closure *timing)
 {
     unsigned long ul;
     long l;
@@ -1571,19 +1728,19 @@ parse_timing(const char *buf, const char *decimal, struct iolog_timing *timing)
      */
     errno = 0;
     l = strtol(cp, &ep, 10);
-    if (ep == cp || (*ep != '.' && strncmp(ep, decimal, strlen(decimal)) != 0))
+    if (ep == cp || (*ep != '.' && strncmp(ep, timing->decimal, strlen(timing->decimal)) != 0))
        goto bad;
     if (l < 0 || l > INT_MAX || (errno == ERANGE && l == LONG_MAX))
        goto bad;
-    timing->seconds = (double)l;
-    cp = ep + (*ep == '.' ? 1 : strlen(decimal));
+    *seconds = (double)l;
+    cp = ep + (*ep == '.' ? 1 : strlen(timing->decimal));
     d = 10.0;
     while (isdigit((unsigned char) *cp)) {
        fract += (*cp - '0') / d;
        d *= 10;
        cp++;
     }
-    timing->seconds += fract;
+    *seconds += fract;
     while (isspace((unsigned char) *cp))
        cp++;
 
@@ -1615,9 +1772,9 @@ parse_timing(const char *buf, const char *decimal, struct iolog_timing *timing)
        timing->u.nbytes = (size_t)ul;
     }
 
-    debug_return_int(1);
+    debug_return_bool(true);
 bad:
-    debug_return_int(0);
+    debug_return_bool(false);
 }
 
 static void
@@ -1658,14 +1815,3 @@ sudoreplay_cleanup(void)
     restore_terminal_size();
     sudo_term_restore(ttyfd, false);
 }
-
-/*
- * Signal handler for SIGINT, SIGTERM, SIGHUP, SIGQUIT
- * Must be installed with SA_RESETHAND enabled.
- */
-static void
-sudoreplay_handler(int signo)
-{
-    sudoreplay_cleanup();
-    kill(getpid(), signo);
-}