clean up changelog and tag confusion in my checked out tree
[debian/sudo] / check.c
1 /*
2  * Copyright (c) 1993-1996,1998-2005, 2007-2010
3  *      Todd C. Miller <Todd.Miller@courtesan.com>
4  *
5  * Permission to use, copy, modify, and distribute this software for any
6  * purpose with or without fee is hereby granted, provided that the above
7  * copyright notice and this permission notice appear in all copies.
8  *
9  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16  *
17  * Sponsored in part by the Defense Advanced Research Projects
18  * Agency (DARPA) and Air Force Research Laboratory, Air Force
19  * Materiel Command, USAF, under agreement number F39502-99-1-0512.
20  */
21
22 #include <config.h>
23
24 #include <sys/types.h>
25 #include <sys/param.h>
26 #include <sys/time.h>
27 #include <sys/stat.h>
28 #ifdef __linux__
29 # include <sys/vfs.h>
30 #endif
31 #if defined(__sun) && defined(__SVR4)
32 # include <sys/statvfs.h>
33 #endif
34 #ifndef __TANDEM
35 # include <sys/file.h>
36 #endif
37 #include <stdio.h>
38 #ifdef STDC_HEADERS
39 # include <stdlib.h>
40 # include <stddef.h>
41 #else
42 # ifdef HAVE_STDLIB_H
43 #  include <stdlib.h>
44 # endif
45 #endif /* STDC_HEADERS */
46 #ifdef HAVE_STRING_H
47 # include <string.h>
48 #endif /* HAVE_STRING_H */
49 #ifdef HAVE_STRINGS_H
50 # include <strings.h>
51 #endif /* HAVE_STRINGS_H */
52 #ifdef HAVE_UNISTD_H
53 # include <unistd.h>
54 #endif /* HAVE_UNISTD_H */
55 #if TIME_WITH_SYS_TIME
56 # include <time.h>
57 #endif
58 #include <errno.h>
59 #include <fcntl.h>
60 #include <signal.h>
61 #include <pwd.h>
62 #include <grp.h>
63
64 #include "sudo.h"
65
66 /* Status codes for timestamp_status() */
67 #define TS_CURRENT              0
68 #define TS_OLD                  1
69 #define TS_MISSING              2
70 #define TS_NOFILE               3
71 #define TS_ERROR                4
72
73 /* Flags for timestamp_status() */
74 #define TS_MAKE_DIRS            1
75 #define TS_REMOVE               2
76
77 /*
78  * Info stored in tty ticket from stat(2) to help with tty matching.
79  */
80 static struct tty_info {
81     dev_t dev;                  /* ID of device tty resides on */
82     dev_t rdev;                 /* tty device ID */
83     ino_t ino;                  /* tty inode number */
84     struct timeval ctime;       /* tty inode change time */
85 } tty_info;
86
87 static void  build_timestamp    __P((char **, char **));
88 static int   timestamp_status   __P((char *, char *, char *, int));
89 static char *expand_prompt      __P((char *, char *, char *));
90 static void  lecture            __P((int));
91 static void  update_timestamp   __P((char *, char *));
92 static int   tty_is_devpts      __P((const char *));
93
94 /*
95  * This function only returns if the user can successfully
96  * verify who he/she is.
97  */
98 void
99 check_user(validated, mode)
100     int validated;
101     int mode;
102 {
103     char *timestampdir = NULL;
104     char *timestampfile = NULL;
105     char *prompt;
106     struct stat sb;
107     int status;
108
109     /* Stash the tty's ctime for tty ticket comparison. */
110     if (def_tty_tickets && user_ttypath && stat(user_ttypath, &sb) == 0) {
111         tty_info.dev = sb.st_dev;
112         tty_info.ino = sb.st_ino;
113         tty_info.rdev = sb.st_rdev;
114         if (tty_is_devpts(user_ttypath))
115             ctim_get(&sb, &tty_info.ctime);
116     }
117
118     /* Always prompt for a password when -k was specified with the command. */
119     if (ISSET(mode, MODE_INVALIDATE)) {
120         SET(validated, FLAG_CHECK_USER);
121     } else {
122         if (user_uid == 0 || user_uid == runas_pw->pw_uid || user_is_exempt())
123             return;
124     }
125
126     build_timestamp(&timestampdir, &timestampfile);
127     status = timestamp_status(timestampdir, timestampfile, user_name,
128         TS_MAKE_DIRS);
129
130     if (status != TS_CURRENT || ISSET(validated, FLAG_CHECK_USER)) {
131         /* Bail out if we are non-interactive and a password is required */
132         if (ISSET(mode, MODE_NONINTERACTIVE))
133             errorx(1, "sorry, a password is required to run %s", getprogname());
134
135         /* If user specified -A, make sure we have an askpass helper. */
136         if (ISSET(tgetpass_flags, TGP_ASKPASS)) {
137             if (user_askpass == NULL)
138                 log_error(NO_MAIL,
139                     "no askpass program specified, try setting SUDO_ASKPASS");
140         } else if (!ISSET(tgetpass_flags, TGP_STDIN)) {
141             /* If no tty but DISPLAY is set, use askpass if we have it. */
142             if (!user_ttypath && !tty_present()) {
143                 if (user_askpass && user_display && *user_display != '\0') {
144                     SET(tgetpass_flags, TGP_ASKPASS);
145                 } else if (!def_visiblepw) {
146                     log_error(NO_MAIL,
147                         "no tty present and no askpass program specified");
148                 }
149             }
150         }
151
152         if (!ISSET(tgetpass_flags, TGP_ASKPASS))
153             lecture(status);
154
155         /* Expand any escapes in the prompt. */
156         prompt = expand_prompt(user_prompt ? user_prompt : def_passprompt,
157             user_name, user_shost);
158
159         verify_user(auth_pw, prompt);
160     }
161     /* Only update timestamp if user was validated. */
162     if (ISSET(validated, VALIDATE_OK) && !ISSET(mode, MODE_INVALIDATE) && status != TS_ERROR)
163         update_timestamp(timestampdir, timestampfile);
164     efree(timestampdir);
165     efree(timestampfile);
166 }
167
168 /*
169  * Standard sudo lecture.
170  */
171 static void
172 lecture(status)
173     int status;
174 {
175     FILE *fp;
176     char buf[BUFSIZ];
177     ssize_t nread;
178
179     if (def_lecture == never ||
180         (def_lecture == once && status != TS_MISSING && status != TS_ERROR))
181         return;
182
183     if (def_lecture_file && (fp = fopen(def_lecture_file, "r")) != NULL) {
184         while ((nread = fread(buf, sizeof(char), sizeof(buf), fp)) != 0)
185             fwrite(buf, nread, 1, stderr);
186         fclose(fp);
187     } else {
188         (void) fputs("\n\
189 We trust you have received the usual lecture from the local System\n\
190 Administrator. It usually boils down to these three things:\n\
191 \n\
192     #1) Respect the privacy of others.\n\
193     #2) Think before you type.\n\
194     #3) With great power comes great responsibility.\n\n",
195     stderr);
196     }
197 }
198
199 /*
200  * Update the time on the timestamp file/dir or create it if necessary.
201  */
202 static void
203 update_timestamp(timestampdir, timestampfile)
204     char *timestampdir;
205     char *timestampfile;
206 {
207     /* If using tty timestamps but we have no tty there is nothing to do. */
208     if (def_tty_tickets && !user_ttypath)
209         return;
210
211     if (timestamp_uid != 0)
212         set_perms(PERM_TIMESTAMP);
213     if (timestampfile) {
214         /*
215          * Store tty info in timestamp file
216          */
217         int fd = open(timestampfile, O_WRONLY|O_CREAT, 0600);
218         if (fd == -1)
219             log_error(NO_EXIT|USE_ERRNO, "Can't open %s", timestampfile);
220         else {
221             lock_file(fd, SUDO_LOCK);
222             write(fd, &tty_info, sizeof(tty_info));
223             close(fd);
224         }
225     } else {
226         if (touch(-1, timestampdir, NULL) == -1) {
227             if (mkdir(timestampdir, 0700) == -1)
228                 log_error(NO_EXIT|USE_ERRNO, "Can't mkdir %s", timestampdir);
229         }
230     }
231     if (timestamp_uid != 0)
232         set_perms(PERM_ROOT);
233 }
234
235 /*
236  * Expand %h and %u escapes in the prompt and pass back the dynamically
237  * allocated result.  Returns the same string if there are no escapes.
238  */
239 static char *
240 expand_prompt(old_prompt, user, host)
241     char *old_prompt;
242     char *user;
243     char *host;
244 {
245     size_t len, n;
246     int subst;
247     char *p, *np, *new_prompt, *endp;
248
249     /* How much space do we need to malloc for the prompt? */
250     subst = 0;
251     for (p = old_prompt, len = strlen(old_prompt); *p; p++) {
252         if (p[0] =='%') {
253             switch (p[1]) {
254                 case 'h':
255                     p++;
256                     len += strlen(user_shost) - 2;
257                     subst = 1;
258                     break;
259                 case 'H':
260                     p++;
261                     len += strlen(user_host) - 2;
262                     subst = 1;
263                     break;
264                 case 'p':
265                     p++;
266                     if (def_rootpw)
267                             len += 2;
268                     else if (def_targetpw || def_runaspw)
269                             len += strlen(runas_pw->pw_name) - 2;
270                     else
271                             len += strlen(user_name) - 2;
272                     subst = 1;
273                     break;
274                 case 'u':
275                     p++;
276                     len += strlen(user_name) - 2;
277                     subst = 1;
278                     break;
279                 case 'U':
280                     p++;
281                     len += strlen(runas_pw->pw_name) - 2;
282                     subst = 1;
283                     break;
284                 case '%':
285                     p++;
286                     len--;
287                     subst = 1;
288                     break;
289                 default:
290                     break;
291             }
292         }
293     }
294
295     if (subst) {
296         new_prompt = (char *) emalloc(++len);
297         endp = new_prompt + len;
298         for (p = old_prompt, np = new_prompt; *p; p++) {
299             if (p[0] =='%') {
300                 switch (p[1]) {
301                     case 'h':
302                         p++;
303                         n = strlcpy(np, user_shost, np - endp);
304                         if (n >= np - endp)
305                             goto oflow;
306                         np += n;
307                         continue;
308                     case 'H':
309                         p++;
310                         n = strlcpy(np, user_host, np - endp);
311                         if (n >= np - endp)
312                             goto oflow;
313                         np += n;
314                         continue;
315                     case 'p':
316                         p++;
317                         if (def_rootpw)
318                                 n = strlcpy(np, "root", np - endp);
319                         else if (def_targetpw || def_runaspw)
320                                 n = strlcpy(np, runas_pw->pw_name, np - endp);
321                         else
322                                 n = strlcpy(np, user_name, np - endp);
323                         if (n >= np - endp)
324                                 goto oflow;
325                         np += n;
326                         continue;
327                     case 'u':
328                         p++;
329                         n = strlcpy(np, user_name, np - endp);
330                         if (n >= np - endp)
331                             goto oflow;
332                         np += n;
333                         continue;
334                     case 'U':
335                         p++;
336                         n = strlcpy(np,  runas_pw->pw_name, np - endp);
337                         if (n >= np - endp)
338                             goto oflow;
339                         np += n;
340                         continue;
341                     case '%':
342                         /* convert %% -> % */
343                         p++;
344                         break;
345                     default:
346                         /* no conversion */
347                         break;
348                 }
349             }
350             *np++ = *p;
351             if (np >= endp)
352                 goto oflow;
353         }
354         *np = '\0';
355     } else
356         new_prompt = old_prompt;
357
358     return(new_prompt);
359
360 oflow:
361     /* We pre-allocate enough space, so this should never happen. */
362     errorx(1, "internal error, expand_prompt() overflow");
363 }
364
365 /*
366  * Checks if the user is exempt from supplying a password.
367  */
368 int
369 user_is_exempt()
370 {
371     if (!def_exempt_group)
372         return(FALSE);
373     return(user_in_group(sudo_user.pw, def_exempt_group));
374 }
375
376 /*
377  * Fills in timestampdir as well as timestampfile if using tty tickets.
378  */
379 static void
380 build_timestamp(timestampdir, timestampfile)
381     char **timestampdir;
382     char **timestampfile;
383 {
384     char *dirparent;
385     int len;
386
387     dirparent = def_timestampdir;
388     len = easprintf(timestampdir, "%s/%s", dirparent, user_name);
389     if (len >= PATH_MAX)
390         log_error(0, "timestamp path too long: %s", *timestampdir);
391
392     /*
393      * Timestamp file may be a file in the directory or NUL to use
394      * the directory as the timestamp.
395      */
396     if (def_tty_tickets) {
397         char *p;
398
399         if ((p = strrchr(user_tty, '/')))
400             p++;
401         else
402             p = user_tty;
403         if (def_targetpw)
404             len = easprintf(timestampfile, "%s/%s/%s:%s", dirparent, user_name,
405                 p, runas_pw->pw_name);
406         else
407             len = easprintf(timestampfile, "%s/%s/%s", dirparent, user_name, p);
408         if (len >= PATH_MAX)
409             log_error(0, "timestamp path too long: %s", *timestampfile);
410     } else if (def_targetpw) {
411         len = easprintf(timestampfile, "%s/%s/%s", dirparent, user_name,
412             runas_pw->pw_name);
413         if (len >= PATH_MAX)
414             log_error(0, "timestamp path too long: %s", *timestampfile);
415     } else
416         *timestampfile = NULL;
417 }
418
419 /*
420  * Check the timestamp file and directory and return their status.
421  */
422 static int
423 timestamp_status(timestampdir, timestampfile, user, flags)
424     char *timestampdir;
425     char *timestampfile;
426     char *user;
427     int flags;
428 {
429     struct stat sb;
430     struct timeval boottime, mtime;
431     time_t now;
432     char *dirparent = def_timestampdir;
433     int status = TS_ERROR;              /* assume the worst */
434
435     if (timestamp_uid != 0)
436         set_perms(PERM_TIMESTAMP);
437
438     /*
439      * Sanity check dirparent and make it if it doesn't already exist.
440      * We start out assuming the worst (that the dir is not sane) and
441      * if it is ok upgrade the status to ``no timestamp file''.
442      * Note that we don't check the parent(s) of dirparent for
443      * sanity since the sudo dir is often just located in /tmp.
444      */
445     if (lstat(dirparent, &sb) == 0) {
446         if (!S_ISDIR(sb.st_mode))
447             log_error(NO_EXIT, "%s exists but is not a directory (0%o)",
448                 dirparent, (unsigned int) sb.st_mode);
449         else if (sb.st_uid != timestamp_uid)
450             log_error(NO_EXIT, "%s owned by uid %lu, should be uid %lu",
451                 dirparent, (unsigned long) sb.st_uid,
452                 (unsigned long) timestamp_uid);
453         else if ((sb.st_mode & 0000022))
454             log_error(NO_EXIT,
455                 "%s writable by non-owner (0%o), should be mode 0700",
456                 dirparent, (unsigned int) sb.st_mode);
457         else {
458             if ((sb.st_mode & 0000777) != 0700)
459                 (void) chmod(dirparent, 0700);
460             status = TS_MISSING;
461         }
462     } else if (errno != ENOENT) {
463         log_error(NO_EXIT|USE_ERRNO, "can't stat %s", dirparent);
464     } else {
465         /* No dirparent, try to make one. */
466         if (ISSET(flags, TS_MAKE_DIRS)) {
467             if (mkdir(dirparent, S_IRWXU))
468                 log_error(NO_EXIT|USE_ERRNO, "can't mkdir %s",
469                     dirparent);
470             else
471                 status = TS_MISSING;
472         }
473     }
474     if (status == TS_ERROR) {
475         if (timestamp_uid != 0)
476             set_perms(PERM_ROOT);
477         return(status);
478     }
479
480     /*
481      * Sanity check the user's ticket dir.  We start by downgrading
482      * the status to TS_ERROR.  If the ticket dir exists and is sane
483      * this will be upgraded to TS_OLD.  If the dir does not exist,
484      * it will be upgraded to TS_MISSING.
485      */
486     status = TS_ERROR;                  /* downgrade status again */
487     if (lstat(timestampdir, &sb) == 0) {
488         if (!S_ISDIR(sb.st_mode)) {
489             if (S_ISREG(sb.st_mode)) {
490                 /* convert from old style */
491                 if (unlink(timestampdir) == 0)
492                     status = TS_MISSING;
493             } else
494                 log_error(NO_EXIT, "%s exists but is not a directory (0%o)",
495                     timestampdir, (unsigned int) sb.st_mode);
496         } else if (sb.st_uid != timestamp_uid)
497             log_error(NO_EXIT, "%s owned by uid %lu, should be uid %lu",
498                 timestampdir, (unsigned long) sb.st_uid,
499                 (unsigned long) timestamp_uid);
500         else if ((sb.st_mode & 0000022))
501             log_error(NO_EXIT,
502                 "%s writable by non-owner (0%o), should be mode 0700",
503                 timestampdir, (unsigned int) sb.st_mode);
504         else {
505             if ((sb.st_mode & 0000777) != 0700)
506                 (void) chmod(timestampdir, 0700);
507             status = TS_OLD;            /* do date check later */
508         }
509     } else if (errno != ENOENT) {
510         log_error(NO_EXIT|USE_ERRNO, "can't stat %s", timestampdir);
511     } else
512         status = TS_MISSING;
513
514     /*
515      * If there is no user ticket dir, AND we are in tty ticket mode,
516      * AND the TS_MAKE_DIRS flag is set, create the user ticket dir.
517      */
518     if (status == TS_MISSING && timestampfile && ISSET(flags, TS_MAKE_DIRS)) {
519         if (mkdir(timestampdir, S_IRWXU) == -1) {
520             status = TS_ERROR;
521             log_error(NO_EXIT|USE_ERRNO, "can't mkdir %s", timestampdir);
522         }
523     }
524
525     /*
526      * Sanity check the tty ticket file if it exists.
527      */
528     if (timestampfile && status != TS_ERROR) {
529         if (status != TS_MISSING)
530             status = TS_NOFILE;                 /* dir there, file missing */
531         if (def_tty_tickets && !user_ttypath)
532             goto done;                          /* no tty, always prompt */
533         if (lstat(timestampfile, &sb) == 0) {
534             if (!S_ISREG(sb.st_mode)) {
535                 status = TS_ERROR;
536                 log_error(NO_EXIT, "%s exists but is not a regular file (0%o)",
537                     timestampfile, (unsigned int) sb.st_mode);
538             } else {
539                 /* If bad uid or file mode, complain and kill the bogus file. */
540                 if (sb.st_uid != timestamp_uid) {
541                     log_error(NO_EXIT,
542                         "%s owned by uid %lu, should be uid %lu",
543                         timestampfile, (unsigned long) sb.st_uid,
544                         (unsigned long) timestamp_uid);
545                     (void) unlink(timestampfile);
546                 } else if ((sb.st_mode & 0000022)) {
547                     log_error(NO_EXIT,
548                         "%s writable by non-owner (0%o), should be mode 0600",
549                         timestampfile, (unsigned int) sb.st_mode);
550                     (void) unlink(timestampfile);
551                 } else {
552                     /* If not mode 0600, fix it. */
553                     if ((sb.st_mode & 0000777) != 0600)
554                         (void) chmod(timestampfile, 0600);
555
556                     /*
557                      * Check for stored tty info.  If the file is zero-sized
558                      * it is an old-style timestamp with no tty info in it.
559                      * If removing, we don't care about the contents.
560                      * The actual mtime check is done later.
561                      */
562                     if (ISSET(flags, TS_REMOVE)) {
563                         status = TS_OLD;
564                     } else if (sb.st_size != 0) {
565                         struct tty_info info;
566                         int fd = open(timestampfile, O_RDONLY, 0644);
567                         if (fd != -1) {
568                             if (read(fd, &info, sizeof(info)) == sizeof(info) &&
569                                 memcmp(&info, &tty_info, sizeof(info)) == 0) {
570                                 status = TS_OLD;
571                             }
572                             close(fd);
573                         }
574                     }
575                 }
576             }
577         } else if (errno != ENOENT) {
578             log_error(NO_EXIT|USE_ERRNO, "can't stat %s", timestampfile);
579             status = TS_ERROR;
580         }
581     }
582
583     /*
584      * If the file/dir exists and we are not removing it, check its mtime.
585      */
586     if (status == TS_OLD && !ISSET(flags, TS_REMOVE)) {
587         mtim_get(&sb, &mtime);
588         /* Negative timeouts only expire manually (sudo -k). */
589         if (def_timestamp_timeout < 0 && mtime.tv_sec != 0)
590             status = TS_CURRENT;
591         else {
592             now = time(NULL);
593             if (def_timestamp_timeout &&
594                 now - mtime.tv_sec < 60 * def_timestamp_timeout) {
595                 /*
596                  * Check for bogus time on the stampfile.  The clock may
597                  * have been set back or someone could be trying to spoof us.
598                  */
599                 if (mtime.tv_sec > now + 60 * def_timestamp_timeout * 2) {
600                     time_t tv_sec = (time_t)mtime.tv_sec;
601                     log_error(NO_EXIT,
602                         "timestamp too far in the future: %20.20s",
603                         4 + ctime(&tv_sec));
604                     if (timestampfile)
605                         (void) unlink(timestampfile);
606                     else
607                         (void) rmdir(timestampdir);
608                     status = TS_MISSING;
609                 } else if (get_boottime(&boottime) && timevalcmp(&mtime, &boottime, <)) {
610                     status = TS_OLD;
611                 } else {
612                     status = TS_CURRENT;
613                 }
614             }
615         }
616     }
617
618 done:
619     if (timestamp_uid != 0)
620         set_perms(PERM_ROOT);
621     return(status);
622 }
623
624 /*
625  * Remove the timestamp ticket file/dir.
626  */
627 void
628 remove_timestamp(remove)
629     int remove;
630 {
631     struct timeval tv;
632     char *timestampdir, *timestampfile, *path;
633     int status;
634
635     build_timestamp(&timestampdir, &timestampfile);
636     status = timestamp_status(timestampdir, timestampfile, user_name,
637         TS_REMOVE);
638     if (status == TS_OLD || status == TS_CURRENT) {
639         path = timestampfile ? timestampfile : timestampdir;
640         if (remove) {
641             if (timestampfile)
642                 status = unlink(timestampfile);
643             else
644                 status = rmdir(timestampdir);
645             if (status == -1 && errno != ENOENT) {
646                 log_error(NO_EXIT, "can't remove %s (%s), will reset to Epoch",
647                     path, strerror(errno));
648                 remove = FALSE;
649             }
650         } else {
651             timevalclear(&tv);
652             if (touch(-1, path, &tv) == -1 && errno != ENOENT)
653                 error(1, "can't reset %s to Epoch", path);
654         }
655     }
656
657     efree(timestampdir);
658     efree(timestampfile);
659 }
660
661 /*
662  * Returns TRUE if tty lives on a devpts or /devices filesystem, else FALSE.
663  * Unlike most filesystems, the ctime of devpts nodes is not updated when
664  * the device node is written to, only when the inode's status changes,
665  * typically via the chmod, chown, link, rename, or utimes system calls.
666  * Since the ctime is "stable" in this case, we can stash it the tty ticket
667  * file and use it to determine whether the tty ticket file is stale.
668  */
669 static int
670 tty_is_devpts(tty)
671     const char *tty;
672 {
673     int retval = FALSE;
674 #ifdef __linux__
675     struct statfs sfs;
676
677 #ifndef DEVPTS_SUPER_MAGIC
678 # define DEVPTS_SUPER_MAGIC 0x1cd1
679 #endif
680
681     if (statfs(tty, &sfs) == 0) {
682         if (sfs.f_type == DEVPTS_SUPER_MAGIC)
683             retval = TRUE;
684     }
685 #elif defined(__sun) && defined(__SVR4)
686     struct statvfs sfs;
687
688     if (statvfs(tty, &sfs) == 0) {
689         if (strcmp(sfs.f_fstr, "devices") == 0)
690             retval = TRUE;
691     }
692 #endif /* __linux__ */
693     return retval;
694 }