Imported Upstream version 1.8.2
[debian/sudo] / plugins / sudoers / iolog.c
1 /*
2  * Copyright (c) 2009-2011 Todd C. Miller <Todd.Miller@courtesan.com>
3  *
4  * Permission to use, copy, modify, and distribute this software for any
5  * purpose with or without fee is hereby granted, provided that the above
6  * copyright notice and this permission notice appear in all copies.
7  *
8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15  */
16
17 #include <config.h>
18
19 #include <sys/types.h>
20 #include <sys/param.h>
21 #include <sys/stat.h>
22 #include <sys/time.h>
23 #include <stdio.h>
24 #ifdef STDC_HEADERS
25 # include <stdlib.h>
26 # include <stddef.h>
27 #else
28 # ifdef HAVE_STDLIB_H
29 #  include <stdlib.h>
30 # endif
31 #endif /* STDC_HEADERS */
32 #ifdef HAVE_STRING_H
33 # include <string.h>
34 #endif /* HAVE_STRING_H */
35 #ifdef HAVE_STRINGS_H
36 # include <strings.h>
37 #endif /* HAVE_STRINGS_H */
38 #ifdef HAVE_UNISTD_H
39 # include <unistd.h>
40 #endif /* HAVE_UNISTD_H */
41 #if TIME_WITH_SYS_TIME
42 # include <time.h>
43 #endif
44 #include <errno.h>
45 #include <fcntl.h>
46 #include <signal.h>
47 #include <setjmp.h>
48 #include <pwd.h>
49 #include <grp.h>
50 #ifdef HAVE_ZLIB_H
51 # include <zlib.h>
52 #endif
53
54 #include "sudoers.h"
55
56 /* plugin_error.c */
57 extern sigjmp_buf error_jmp;
58
59 union io_fd {
60     FILE *f;
61 #ifdef HAVE_ZLIB_H
62     gzFile g;
63 #endif
64     void *v;
65 };
66
67 struct script_buf {
68     int len; /* buffer length (how much read in) */
69     int off; /* write position (how much already consumed) */
70     char buf[16 * 1024];
71 };
72
73 /* XXX - separate sudoers.h and iolog.h? */
74 #undef runas_pw
75 #undef runas_gr
76
77 struct iolog_details {
78     const char *cwd;
79     const char *tty;
80     const char *user;
81     const char *command;
82     const char *iolog_path;
83     struct passwd *runas_pw;
84     struct group *runas_gr;
85     int iolog_stdin;
86     int iolog_stdout;
87     int iolog_stderr;
88     int iolog_ttyin;
89     int iolog_ttyout;
90 };
91
92 #define IOFD_STDIN      0
93 #define IOFD_STDOUT     1
94 #define IOFD_STDERR     2
95 #define IOFD_TTYIN      3
96 #define IOFD_TTYOUT     4
97 #define IOFD_TIMING     5
98 #define IOFD_MAX        6
99
100 #define SESSID_MAX      2176782336U
101
102 static int iolog_compress;
103 static struct timeval last_time;
104 static union io_fd io_fds[IOFD_MAX];
105 extern struct io_plugin sudoers_io;
106
107 /*
108  * Create parent directories for path as needed, but not path itself.
109  */
110 static void
111 mkdir_parents(char *path)
112 {
113     struct stat sb;
114     char *slash = path;
115
116     for (;;) {
117         if ((slash = strchr(slash + 1, '/')) == NULL)
118             break;
119         *slash = '\0';
120         if (stat(path, &sb) != 0) {
121             if (mkdir(path, S_IRWXU) != 0)
122                 log_error(USE_ERRNO, _("unable to mkdir %s"), path);
123         } else if (!S_ISDIR(sb.st_mode)) {
124             log_error(0, _("%s: %s"), path, strerror(ENOTDIR));
125         }
126         *slash = '/';
127     }
128 }
129
130 /*
131  * Read the on-disk sequence number, set sessid to the next
132  * number, and update the on-disk copy.
133  * Uses file locking to avoid sequence number collisions.
134  */
135 void
136 io_nextid(char *iolog_dir, char sessid[7])
137 {
138     struct stat sb;
139     char buf[32], *ep;
140     int fd, i;
141     unsigned long id = 0;
142     int len;
143     ssize_t nread;
144     char pathbuf[PATH_MAX];
145     static const char b36char[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
146
147     /*
148      * Create I/O log directory if it doesn't already exist.
149      */
150     mkdir_parents(iolog_dir);
151     if (stat(iolog_dir, &sb) != 0) {
152         if (mkdir(iolog_dir, S_IRWXU) != 0)
153             log_error(USE_ERRNO, _("unable to mkdir %s"), iolog_dir);
154     } else if (!S_ISDIR(sb.st_mode)) {
155         log_error(0, _("%s exists but is not a directory (0%o)"),
156             iolog_dir, (unsigned int) sb.st_mode);
157     }
158
159     /*
160      * Open sequence file
161      */
162     len = snprintf(pathbuf, sizeof(pathbuf), "%s/seq", iolog_dir);
163     if (len <= 0 || len >= sizeof(pathbuf)) {
164         errno = ENAMETOOLONG;
165         log_error(USE_ERRNO, "%s/seq", pathbuf);
166     }
167     fd = open(pathbuf, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
168     if (fd == -1)
169         log_error(USE_ERRNO, _("unable to open %s"), pathbuf);
170     lock_file(fd, SUDO_LOCK);
171
172     /* Read seq number (base 36). */
173     nread = read(fd, buf, sizeof(buf));
174     if (nread != 0) {
175         if (nread == -1)
176             log_error(USE_ERRNO, _("unable to read %s"), pathbuf);
177         id = strtoul(buf, &ep, 36);
178         if (buf == ep || id >= SESSID_MAX)
179             log_error(0, _("invalid sequence number %s"), pathbuf);
180     }
181     id++;
182
183     /*
184      * Convert id to a string and stash in sessid.
185      * Note that that least significant digits go at the end of the string.
186      */
187     for (i = 5; i >= 0; i--) {
188         buf[i] = b36char[id % 36];
189         id /= 36;
190     }
191     buf[6] = '\n';
192
193     /* Stash id logging purposes */
194     memcpy(sessid, buf, 6);
195     sessid[6] = '\0';
196
197     /* Rewind and overwrite old seq file. */
198     if (lseek(fd, 0, SEEK_SET) == (off_t)-1 || write(fd, buf, 7) != 7)
199         log_error(USE_ERRNO, _("unable to write to %s"), pathbuf);
200     close(fd);
201 }
202
203 /*
204  * Copy iolog_path to pathbuf and create the directory and any intermediate
205  * directories.  If iolog_path ends in 'XXXXXX', use mkdtemp().
206  */
207 static size_t
208 mkdir_iopath(const char *iolog_path, char *pathbuf, size_t pathsize)
209 {
210     size_t len;
211
212     len = strlcpy(pathbuf, iolog_path, pathsize);
213     if (len >= pathsize) {
214         errno = ENAMETOOLONG;
215         log_error(USE_ERRNO, "%s", iolog_path);
216     }
217
218     /*
219      * Create path and intermediate subdirs as needed.
220      * If path ends in at least 6 Xs (ala POSIX mktemp), use mkdtemp().
221      */
222     mkdir_parents(pathbuf);
223     if (len >= 6 && strcmp(&pathbuf[len - 6], "XXXXXX") == 0) {
224         if (mkdtemp(pathbuf) == NULL)
225             log_error(USE_ERRNO, _("unable to create %s"), pathbuf);
226     } else {
227         if (mkdir(pathbuf, S_IRWXU) != 0)
228             log_error(USE_ERRNO, _("unable to create %s"), pathbuf);
229     }
230
231     return len;
232 }
233
234 /*
235  * Append suffix to pathbuf after len chars and open the resulting file.
236  * Note that the size of pathbuf is assumed to be PATH_MAX.
237  * Uses zlib if docompress is TRUE.
238  * Returns the open file handle which has the close-on-exec flag set.
239  */
240 static void *
241 open_io_fd(char *pathbuf, size_t len, const char *suffix, int docompress)
242 {
243     void *vfd = NULL;
244     int fd;
245
246     pathbuf[len] = '\0';
247     strlcat(pathbuf, suffix, PATH_MAX);
248     fd = open(pathbuf, O_CREAT|O_EXCL|O_WRONLY, S_IRUSR|S_IWUSR);
249     if (fd != -1) {
250         fcntl(fd, F_SETFD, FD_CLOEXEC);
251 #ifdef HAVE_ZLIB_H
252         if (docompress)
253             vfd = gzdopen(fd, "w");
254         else
255 #endif
256             vfd = fdopen(fd, "w");
257     }
258     return vfd;
259 }
260
261 /*
262  * Pull out I/O log related data from user_info and command_info arrays.
263  */
264 static void
265 iolog_deserialize_info(struct iolog_details *details, char * const user_info[],
266     char * const command_info[])
267 {
268     const char *runas_uid_str = "0", *runas_euid_str = NULL;
269     const char *runas_gid_str = "0", *runas_egid_str = NULL;
270     char id[MAX_UID_T_LEN + 2], *ep;
271     char * const *cur;
272     unsigned long ulval;
273     uid_t runas_uid = 0;
274     gid_t runas_gid = 0;
275
276     memset(details, 0, sizeof(*details));
277
278     for (cur = user_info; *cur != NULL; cur++) {
279         switch (**cur) {
280         case 'c':
281             if (strncmp(*cur, "cwd=", sizeof("cwd=") - 1) == 0) {
282                 details->cwd = *cur + sizeof("cwd=") - 1;
283                 continue;
284             }
285             break;
286         case 't':
287             if (strncmp(*cur, "tty=", sizeof("tty=") - 1) == 0) {
288                 details->tty = *cur + sizeof("tty=") - 1;
289                 continue;
290             }
291             break;
292         case 'u':
293             if (strncmp(*cur, "user=", sizeof("user=") - 1) == 0) {
294                 details->user = *cur + sizeof("user=") - 1;
295                 continue;
296             }
297             break;
298         }
299     }
300
301     for (cur = command_info; *cur != NULL; cur++) {
302         switch (**cur) {
303         case 'c':
304             if (strncmp(*cur, "command=", sizeof("command=") - 1) == 0) {
305                 details->command = *cur + sizeof("command=") - 1;
306                 continue;
307             }
308             break;
309         case 'i':
310             if (strncmp(*cur, "iolog_path=", sizeof("iolog_path=") - 1) == 0) {
311                 details->iolog_path = *cur + sizeof("iolog_path=") - 1;
312                 continue;
313             }
314             if (strncmp(*cur, "iolog_stdin=", sizeof("iolog_stdin=") - 1) == 0) {
315                 if (atobool(*cur + sizeof("iolog_stdin=") - 1) == TRUE)
316                     details->iolog_stdin = TRUE;
317                 continue;
318             }
319             if (strncmp(*cur, "iolog_stdout=", sizeof("iolog_stdout=") - 1) == 0) {
320                 if (atobool(*cur + sizeof("iolog_stdout=") - 1) == TRUE)
321                     details->iolog_stdout = TRUE;
322                 continue;
323             }
324             if (strncmp(*cur, "iolog_stderr=", sizeof("iolog_stderr=") - 1) == 0) {
325                 if (atobool(*cur + sizeof("iolog_stderr=") - 1) == TRUE)
326                     details->iolog_stderr = TRUE;
327                 continue;
328             }
329             if (strncmp(*cur, "iolog_ttyin=", sizeof("iolog_ttyin=") - 1) == 0) {
330                 if (atobool(*cur + sizeof("iolog_ttyin=") - 1) == TRUE)
331                     details->iolog_ttyin = TRUE;
332                 continue;
333             }
334             if (strncmp(*cur, "iolog_ttyout=", sizeof("iolog_ttyout=") - 1) == 0) {
335                 if (atobool(*cur + sizeof("iolog_ttyout=") - 1) == TRUE)
336                     details->iolog_ttyout = TRUE;
337                 continue;
338             }
339             if (strncmp(*cur, "iolog_compress=", sizeof("iolog_compress=") - 1) == 0) {
340                 if (atobool(*cur + sizeof("iolog_compress=") - 1) == TRUE)
341                     iolog_compress = TRUE; /* must be global */
342                 continue;
343             }
344             break;
345         case 'r':
346             if (strncmp(*cur, "runas_gid=", sizeof("runas_gid=") - 1) == 0) {
347                 runas_gid_str = *cur + sizeof("runas_gid=") - 1;
348                 continue;
349             }
350             if (strncmp(*cur, "runas_egid=", sizeof("runas_egid=") - 1) == 0) {
351                 runas_egid_str = *cur + sizeof("runas_egid=") - 1;
352                 continue;
353             }
354             if (strncmp(*cur, "runas_uid=", sizeof("runas_uid=") - 1) == 0) {
355                 runas_uid_str = *cur + sizeof("runas_uid=") - 1;
356                 continue;
357             }
358             if (strncmp(*cur, "runas_euid=", sizeof("runas_euid=") - 1) == 0) {
359                 runas_euid_str = *cur + sizeof("runas_euid=") - 1;
360                 continue;
361             }
362             break;
363         }
364     }
365
366     /*
367      * Lookup runas user and group, preferring effective over real uid/gid.
368      */
369     if (runas_euid_str != NULL)
370         runas_uid_str = runas_euid_str;
371     if (runas_uid_str != NULL) {
372         errno = 0;
373         ulval = strtoul(runas_uid_str, &ep, 0);
374         if (*runas_uid_str != '\0' && *ep == '\0' &&
375             (errno != ERANGE || ulval != ULONG_MAX)) {
376             runas_uid = (uid_t)ulval;
377         }
378     }
379     if (runas_egid_str != NULL)
380         runas_gid_str = runas_egid_str;
381     if (runas_gid_str != NULL) {
382         errno = 0;
383         ulval = strtoul(runas_gid_str, &ep, 0);
384         if (*runas_gid_str != '\0' && *ep == '\0' &&
385             (errno != ERANGE || ulval != ULONG_MAX)) {
386             runas_gid = (gid_t)ulval;
387         }
388     }
389
390     details->runas_pw = sudo_getpwuid(runas_uid);
391     if (details->runas_pw == NULL) {
392         id[0] = '#';
393         strlcpy(&id[1], runas_uid_str, sizeof(id) - 1);
394         details->runas_pw = sudo_fakepwnam(id, runas_gid);
395     }
396
397     if (runas_gid != details->runas_pw->pw_gid) {
398         details->runas_gr = sudo_getgrgid(runas_gid);
399         if (details->runas_gr == NULL) {
400             id[0] = '#';
401             strlcpy(&id[1], runas_gid_str, sizeof(id) - 1);
402             details->runas_gr = sudo_fakegrnam(id);
403         }
404     }
405 }
406
407 static int
408 sudoers_io_open(unsigned int version, sudo_conv_t conversation,
409     sudo_printf_t plugin_printf, char * const settings[],
410     char * const user_info[], char * const command_info[],
411     int argc, char * const argv[], char * const user_env[])
412 {
413     struct iolog_details details;
414     char pathbuf[PATH_MAX], sessid[7];
415     char *tofree = NULL;
416     char * const *cur;
417     FILE *io_logfile;
418     size_t len;
419     int rval = -1;
420
421     if (!sudo_conv)
422         sudo_conv = conversation;
423     if (!sudo_printf)
424         sudo_printf = plugin_printf;
425
426     /* If we have no command (because -V was specified) just return. */
427     if (argc == 0)
428         return TRUE;
429
430     if (sigsetjmp(error_jmp, 1)) {
431         /* called via error(), errorx() or log_error() */
432         rval = -1;
433         goto done;
434     }
435
436     bindtextdomain("sudoers", LOCALEDIR);
437
438     sudo_setpwent();
439     sudo_setgrent();
440
441     /*
442      * Pull iolog settings out of command_info, if any.
443      */
444     iolog_deserialize_info(&details, user_info, command_info);
445     /* Did policy module disable I/O logging? */
446     if (!details.iolog_stdin && !details.iolog_ttyin &&
447         !details.iolog_stdout && !details.iolog_stderr &&
448         !details.iolog_ttyout) {
449         rval = FALSE;
450         goto done;
451     }
452
453     /* If no I/O log path defined we need to figure it out ourselves. */
454     if (details.iolog_path == NULL) {
455         /* Get next session ID and convert it into a path. */
456         tofree = emalloc(sizeof(_PATH_SUDO_IO_LOGDIR) + sizeof(sessid) + 2);
457         memcpy(tofree, _PATH_SUDO_IO_LOGDIR, sizeof(_PATH_SUDO_IO_LOGDIR));
458         io_nextid(tofree, sessid);
459         snprintf(tofree + sizeof(_PATH_SUDO_IO_LOGDIR), sizeof(sessid) + 2,
460             "%c%c/%c%c/%c%c", sessid[0], sessid[1], sessid[2], sessid[3],
461             sessid[4], sessid[5]);
462         details.iolog_path = tofree;
463     }
464
465     /*
466      * Make local copy of I/O log path and create it, along with any
467      * intermediate subdirs.  Calls mkdtemp() if iolog_path ends in XXXXXX.
468      */
469     len = mkdir_iopath(details.iolog_path, pathbuf, sizeof(pathbuf));
470     if (len >= sizeof(pathbuf))
471         goto done;
472
473     /*
474      * We create 7 files: a log file, a timing file and 5 for input/output.
475      */
476     io_logfile = open_io_fd(pathbuf, len, "/log", FALSE);
477     if (io_logfile == NULL)
478         log_error(USE_ERRNO, _("unable to create %s"), pathbuf);
479
480     io_fds[IOFD_TIMING].v = open_io_fd(pathbuf, len, "/timing",
481         iolog_compress);
482     if (io_fds[IOFD_TIMING].v == NULL)
483         log_error(USE_ERRNO, _("unable to create %s"), pathbuf);
484
485     if (details.iolog_ttyin) {
486         io_fds[IOFD_TTYIN].v = open_io_fd(pathbuf, len, "/ttyin",
487             iolog_compress);
488         if (io_fds[IOFD_TTYIN].v == NULL)
489             log_error(USE_ERRNO, _("unable to create %s"), pathbuf);
490     } else {
491         sudoers_io.log_ttyin = NULL;
492     }
493     if (details.iolog_stdin) {
494         io_fds[IOFD_STDIN].v = open_io_fd(pathbuf, len, "/stdin",
495             iolog_compress);
496         if (io_fds[IOFD_STDIN].v == NULL)
497             log_error(USE_ERRNO, _("unable to create %s"), pathbuf);
498     } else {
499         sudoers_io.log_stdin = NULL;
500     }
501     if (details.iolog_ttyout) {
502         io_fds[IOFD_TTYOUT].v = open_io_fd(pathbuf, len, "/ttyout",
503             iolog_compress);
504         if (io_fds[IOFD_TTYOUT].v == NULL)
505             log_error(USE_ERRNO, _("unable to create %s"), pathbuf);
506     } else {
507         sudoers_io.log_ttyout = NULL;
508     }
509     if (details.iolog_stdout) {
510         io_fds[IOFD_STDOUT].v = open_io_fd(pathbuf, len, "/stdout",
511             iolog_compress);
512         if (io_fds[IOFD_STDOUT].v == NULL)
513             log_error(USE_ERRNO, _("unable to create %s"), pathbuf);
514     } else {
515         sudoers_io.log_stdout = NULL;
516     }
517     if (details.iolog_stderr) {
518         io_fds[IOFD_STDERR].v = open_io_fd(pathbuf, len, "/stderr",
519             iolog_compress);
520         if (io_fds[IOFD_STDERR].v == NULL)
521             log_error(USE_ERRNO, _("unable to create %s"), pathbuf);
522     } else {
523         sudoers_io.log_stderr = NULL;
524     }
525
526     gettimeofday(&last_time, NULL);
527
528     fprintf(io_logfile, "%ld:%s:%s:%s:%s\n", (long)last_time.tv_sec,
529         details.user ? details.user : "unknown", details.runas_pw->pw_name,
530         details.runas_gr ? details.runas_gr->gr_name : "",
531         details.tty ? details.tty : "unknown");
532     fputs(details.cwd ? details.cwd : "unknown", io_logfile);
533     fputc('\n', io_logfile);
534     fputs(details.command ? details.command : "unknown", io_logfile);
535     for (cur = &argv[1]; *cur != NULL; cur++) {
536         fputc(' ', io_logfile);
537         fputs(*cur, io_logfile);
538     }
539     fputc('\n', io_logfile);
540     fclose(io_logfile);
541
542     rval = TRUE;
543
544 done:
545     efree(tofree);
546     if (details.runas_pw)
547         pw_delref(details.runas_pw);
548     sudo_endpwent();
549     if (details.runas_gr)
550         gr_delref(details.runas_gr);
551     sudo_endgrent();
552
553     return rval;
554 }
555
556 static void
557 sudoers_io_close(int exit_status, int error)
558 {
559     int i;
560
561     if (sigsetjmp(error_jmp, 1)) {
562         /* called via error(), errorx() or log_error() */
563         return;
564     }
565
566     for (i = 0; i < IOFD_MAX; i++) {
567         if (io_fds[i].v == NULL)
568             continue;
569 #ifdef HAVE_ZLIB_H
570         if (iolog_compress)
571             gzclose(io_fds[i].g);
572         else
573 #endif
574             fclose(io_fds[i].f);
575     }
576 }
577
578 static int
579 sudoers_io_version(int verbose)
580 {
581     if (sigsetjmp(error_jmp, 1)) {
582         /* called via error(), errorx() or log_error() */
583         return -1;
584     }
585
586     sudo_printf(SUDO_CONV_INFO_MSG, "Sudoers I/O plugin version %s\n",
587         PACKAGE_VERSION);
588
589     return TRUE;
590 }
591
592 /*
593  * Generic I/O logging function.  Called by the I/O logging entry points.
594  */
595 static int
596 sudoers_io_log(const char *buf, unsigned int len, int idx)
597 {
598     struct timeval now, delay;
599
600     gettimeofday(&now, NULL);
601
602     if (sigsetjmp(error_jmp, 1)) {
603         /* called via error(), errorx() or log_error() */
604         return -1;
605     }
606
607 #ifdef HAVE_ZLIB_H
608     if (iolog_compress)
609         gzwrite(io_fds[idx].g, buf, len);
610     else
611 #endif
612         fwrite(buf, 1, len, io_fds[idx].f);
613     delay.tv_sec = now.tv_sec;
614     delay.tv_usec = now.tv_usec;
615     timevalsub(&delay, &last_time);
616 #ifdef HAVE_ZLIB_H
617     if (iolog_compress)
618         gzprintf(io_fds[IOFD_TIMING].g, "%d %f %d\n", idx,
619             delay.tv_sec + ((double)delay.tv_usec / 1000000), len);
620     else
621 #endif
622         fprintf(io_fds[IOFD_TIMING].f, "%d %f %d\n", idx,
623             delay.tv_sec + ((double)delay.tv_usec / 1000000), len);
624     last_time.tv_sec = now.tv_sec;
625     last_time.tv_usec = now.tv_usec;
626
627     return TRUE;
628 }
629
630 static int
631 sudoers_io_log_ttyin(const char *buf, unsigned int len)
632 {
633     return sudoers_io_log(buf, len, IOFD_TTYIN);
634 }
635
636 static int
637 sudoers_io_log_ttyout(const char *buf, unsigned int len)
638 {
639     return sudoers_io_log(buf, len, IOFD_TTYOUT);
640 }
641
642 static int
643 sudoers_io_log_stdin(const char *buf, unsigned int len)
644 {
645     return sudoers_io_log(buf, len, IOFD_STDIN);
646 }
647
648 static int
649 sudoers_io_log_stdout(const char *buf, unsigned int len)
650 {
651     return sudoers_io_log(buf, len, IOFD_STDOUT);
652 }
653
654 static int
655 sudoers_io_log_stderr(const char *buf, unsigned int len)
656 {
657     return sudoers_io_log(buf, len, IOFD_STDERR);
658 }
659
660 struct io_plugin sudoers_io = {
661     SUDO_IO_PLUGIN,
662     SUDO_API_VERSION,
663     sudoers_io_open,
664     sudoers_io_close,
665     sudoers_io_version,
666     sudoers_io_log_ttyin,
667     sudoers_io_log_ttyout,
668     sudoers_io_log_stdin,
669     sudoers_io_log_stdout,
670     sudoers_io_log_stderr
671 };