Imported Upstream version 1.8.1p2
[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, "Can't 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, "Can't 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, "cannot 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, "cannot 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, "Can't 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, "Can't create %s", pathbuf);
226     } else {
227         if (mkdir(pathbuf, S_IRWXU) != 0)
228             log_error(USE_ERRNO, "Can't 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     sudo_setpwent();
437     sudo_setgrent();
438
439     /*
440      * Pull iolog settings out of command_info, if any.
441      */
442     iolog_deserialize_info(&details, user_info, command_info);
443     /* Did policy module disable I/O logging? */
444     if (!details.iolog_stdin && !details.iolog_ttyin &&
445         !details.iolog_stdout && !details.iolog_stderr &&
446         !details.iolog_ttyout) {
447         rval = FALSE;
448         goto done;
449     }
450
451     /* If no I/O log path defined we need to figure it out ourselves. */
452     if (details.iolog_path == NULL) {
453         /* Get next session ID and convert it into a path. */
454         tofree = emalloc(sizeof(_PATH_SUDO_IO_LOGDIR) + sizeof(sessid) + 2);
455         memcpy(tofree, _PATH_SUDO_IO_LOGDIR, sizeof(_PATH_SUDO_IO_LOGDIR));
456         io_nextid(tofree, sessid);
457         snprintf(tofree + sizeof(_PATH_SUDO_IO_LOGDIR), sizeof(sessid) + 2,
458             "%c%c/%c%c/%c%c", sessid[0], sessid[1], sessid[2], sessid[3],
459             sessid[4], sessid[5]);
460         details.iolog_path = tofree;
461     }
462
463     /*
464      * Make local copy of I/O log path and create it, along with any
465      * intermediate subdirs.  Calls mkdtemp() if iolog_path ends in XXXXXX.
466      */
467     len = mkdir_iopath(details.iolog_path, pathbuf, sizeof(pathbuf));
468     if (len >= sizeof(pathbuf))
469         goto done;
470
471     /*
472      * We create 7 files: a log file, a timing file and 5 for input/output.
473      */
474     io_logfile = open_io_fd(pathbuf, len, "/log", FALSE);
475     if (io_logfile == NULL)
476         log_error(USE_ERRNO, "Can't create %s", pathbuf);
477
478     io_fds[IOFD_TIMING].v = open_io_fd(pathbuf, len, "/timing",
479         iolog_compress);
480     if (io_fds[IOFD_TIMING].v == NULL)
481         log_error(USE_ERRNO, "Can't create %s", pathbuf);
482
483     if (details.iolog_ttyin) {
484         io_fds[IOFD_TTYIN].v = open_io_fd(pathbuf, len, "/ttyin",
485             iolog_compress);
486         if (io_fds[IOFD_TTYIN].v == NULL)
487             log_error(USE_ERRNO, "Can't create %s", pathbuf);
488     } else {
489         sudoers_io.log_ttyin = NULL;
490     }
491     if (details.iolog_stdin) {
492         io_fds[IOFD_STDIN].v = open_io_fd(pathbuf, len, "/stdin",
493             iolog_compress);
494         if (io_fds[IOFD_STDIN].v == NULL)
495             log_error(USE_ERRNO, "Can't create %s", pathbuf);
496     } else {
497         sudoers_io.log_stdin = NULL;
498     }
499     if (details.iolog_ttyout) {
500         io_fds[IOFD_TTYOUT].v = open_io_fd(pathbuf, len, "/ttyout",
501             iolog_compress);
502         if (io_fds[IOFD_TTYOUT].v == NULL)
503             log_error(USE_ERRNO, "Can't create %s", pathbuf);
504     } else {
505         sudoers_io.log_ttyout = NULL;
506     }
507     if (details.iolog_stdout) {
508         io_fds[IOFD_STDOUT].v = open_io_fd(pathbuf, len, "/stdout",
509             iolog_compress);
510         if (io_fds[IOFD_STDOUT].v == NULL)
511             log_error(USE_ERRNO, "Can't create %s", pathbuf);
512     } else {
513         sudoers_io.log_stdout = NULL;
514     }
515     if (details.iolog_stderr) {
516         io_fds[IOFD_STDERR].v = open_io_fd(pathbuf, len, "/stderr",
517             iolog_compress);
518         if (io_fds[IOFD_STDERR].v == NULL)
519             log_error(USE_ERRNO, "Can't create %s", pathbuf);
520     } else {
521         sudoers_io.log_stderr = NULL;
522     }
523
524     gettimeofday(&last_time, NULL);
525
526     fprintf(io_logfile, "%ld:%s:%s:%s:%s\n", (long)last_time.tv_sec,
527         details.user ? details.user : "unknown", details.runas_pw->pw_name,
528         details.runas_gr ? details.runas_gr->gr_name : "",
529         details.tty ? details.tty : "unknown");
530     fputs(details.cwd ? details.cwd : "unknown", io_logfile);
531     fputc('\n', io_logfile);
532     fputs(details.command ? details.command : "unknown", io_logfile);
533     for (cur = &argv[1]; *cur != NULL; cur++) {
534         if (cur != &argv[1])
535             fputc(' ', io_logfile);
536         fputs(*cur, io_logfile);
537     }
538     fputc('\n', io_logfile);
539     fclose(io_logfile);
540
541     rval = TRUE;
542
543 done:
544     efree(tofree);
545     if (details.runas_pw)
546         pw_delref(details.runas_pw);
547     sudo_endpwent();
548     if (details.runas_gr)
549         gr_delref(details.runas_gr);
550     sudo_endgrent();
551
552     return rval;
553 }
554
555 static void
556 sudoers_io_close(int exit_status, int error)
557 {
558     int i;
559
560     if (sigsetjmp(error_jmp, 1)) {
561         /* called via error(), errorx() or log_error() */
562         return;
563     }
564
565     for (i = 0; i < IOFD_MAX; i++) {
566         if (io_fds[i].v == NULL)
567             continue;
568 #ifdef HAVE_ZLIB_H
569         if (iolog_compress)
570             gzclose(io_fds[i].g);
571         else
572 #endif
573             fclose(io_fds[i].f);
574     }
575 }
576
577 static int
578 sudoers_io_version(int verbose)
579 {
580     if (sigsetjmp(error_jmp, 1)) {
581         /* called via error(), errorx() or log_error() */
582         return -1;
583     }
584
585     sudo_printf(SUDO_CONV_INFO_MSG, "Sudoers I/O plugin version %s\n",
586         PACKAGE_VERSION);
587
588     return TRUE;
589 }
590
591 /*
592  * Generic I/O logging function.  Called by the I/O logging entry points.
593  */
594 static int
595 sudoers_io_log(const char *buf, unsigned int len, int idx)
596 {
597     struct timeval now, delay;
598
599     gettimeofday(&now, NULL);
600
601     if (sigsetjmp(error_jmp, 1)) {
602         /* called via error(), errorx() or log_error() */
603         return -1;
604     }
605
606 #ifdef HAVE_ZLIB_H
607     if (iolog_compress)
608         gzwrite(io_fds[idx].g, buf, len);
609     else
610 #endif
611         fwrite(buf, 1, len, io_fds[idx].f);
612     delay.tv_sec = now.tv_sec;
613     delay.tv_usec = now.tv_usec;
614     timevalsub(&delay, &last_time);
615 #ifdef HAVE_ZLIB_H
616     if (iolog_compress)
617         gzprintf(io_fds[IOFD_TIMING].g, "%d %f %d\n", idx,
618             delay.tv_sec + ((double)delay.tv_usec / 1000000), len);
619     else
620 #endif
621         fprintf(io_fds[IOFD_TIMING].f, "%d %f %d\n", idx,
622             delay.tv_sec + ((double)delay.tv_usec / 1000000), len);
623     last_time.tv_sec = now.tv_sec;
624     last_time.tv_usec = now.tv_usec;
625
626     return TRUE;
627 }
628
629 static int
630 sudoers_io_log_ttyin(const char *buf, unsigned int len)
631 {
632     return sudoers_io_log(buf, len, IOFD_TTYIN);
633 }
634
635 static int
636 sudoers_io_log_ttyout(const char *buf, unsigned int len)
637 {
638     return sudoers_io_log(buf, len, IOFD_TTYOUT);
639 }
640
641 static int
642 sudoers_io_log_stdin(const char *buf, unsigned int len)
643 {
644     return sudoers_io_log(buf, len, IOFD_STDIN);
645 }
646
647 static int
648 sudoers_io_log_stdout(const char *buf, unsigned int len)
649 {
650     return sudoers_io_log(buf, len, IOFD_STDOUT);
651 }
652
653 static int
654 sudoers_io_log_stderr(const char *buf, unsigned int len)
655 {
656     return sudoers_io_log(buf, len, IOFD_STDERR);
657 }
658
659 struct io_plugin sudoers_io = {
660     SUDO_IO_PLUGIN,
661     SUDO_API_VERSION,
662     sudoers_io_open,
663     sudoers_io_close,
664     sudoers_io_version,
665     sudoers_io_log_ttyin,
666     sudoers_io_log_ttyout,
667     sudoers_io_log_stdin,
668     sudoers_io_log_stdout,
669     sudoers_io_log_stderr
670 };