OSDN Git Service

* utils.sgml (kill): Add SIGIO, SIGCLD, and SIGPWR.
[pf3gnuchains/pf3gnuchains4x.git] / winsup / utils / passwd.c
1 /* passwd.c: Changing passwords and managing account information
2
3    Copyright 1999, 2000, 2001, 2002, 2003, 2008, 2009 Red Hat, Inc.
4
5    Written by Corinna Vinschen <corinna.vinschen@cityweb.de>
6
7 This file is part of Cygwin.
8
9 This software is a copyrighted work licensed under the terms of the
10 Cygwin license.  Please consult the file "CYGWIN_LICENSE" for
11 details. */
12
13 #include <windows.h>
14 #include <wininet.h>
15 #include <lmaccess.h>
16 #include <lmerr.h>
17 #include <lmcons.h>
18 #include <lmapibuf.h>
19
20 #include <stdio.h>
21 #include <stdlib.h>
22 #include <string.h>
23 #include <unistd.h>
24 #include <getopt.h>
25 #include <pwd.h>
26 #include <sys/cygwin.h>
27 #include <sys/types.h>
28 #include <time.h>
29 #include <errno.h>
30 #include <locale.h>
31 #include <wchar.h>
32
33 #define USER_PRIV_ADMIN          2
34
35 static const char version[] = "$Revision$";
36 static char *prog_name;
37
38 static struct option longopts[] =
39 {
40   {"cannot-change", no_argument, NULL, 'c'},
41   {"can-change", no_argument, NULL, 'C'},
42   {"logonserver", required_argument, NULL, 'd'},
43   {"never-expires", no_argument, NULL, 'e'},
44   {"expires", no_argument, NULL, 'E'},
45   {"help", no_argument, NULL, 'h' },
46   {"inactive", required_argument, NULL, 'i'},
47   {"lock", no_argument, NULL, 'l'},
48   {"minage", required_argument, NULL, 'n'},
49   {"pwd-not-required", no_argument, NULL, 'p'},
50   {"pwd-required", no_argument, NULL, 'P'},
51   {"unlock", no_argument, NULL, 'u'},
52   {"version", no_argument, NULL, 'v'},
53   {"maxage", required_argument, NULL, 'x'},
54   {"length", required_argument, NULL, 'L'},
55   {"status", no_argument, NULL, 'S'},
56   { "reg-store-pwd", no_argument, NULL, 'R'},
57   {NULL, 0, NULL, 0}
58 };
59
60 static char opts[] = "cCd:eEhi:ln:pPuvx:L:SR";
61
62 int
63 eprint (int with_name, const char *fmt, ...)
64 {
65   va_list ap;
66
67   if (with_name)
68     fprintf(stderr, "%s: ", prog_name);
69   va_start (ap, fmt);
70   vfprintf (stderr, fmt, ap);
71   va_end (ap);
72   fprintf(stderr, "\n");
73   return 1;
74 }
75
76 int
77 EvalRet (int ret, const char *user)
78 {
79   switch (ret)
80     {
81     case NERR_Success:
82       return 0;
83
84     case ERROR_ACCESS_DENIED:
85       if (! user)
86         eprint (0, "You may not change password expiry information.");
87       else
88         eprint (0, "You may not change the password for %s.", user);
89       break;
90
91       eprint (0, "Bad password: Invalid.");
92       break;
93
94     case NERR_PasswordTooShort:
95       eprint (0, "Bad password: Too short.");
96       break;
97
98     case NERR_UserNotFound:
99       eprint (1, "unknown user %s", user);
100       break;
101
102     case ERROR_INVALID_PASSWORD:
103     case NERR_BadPassword:
104       eprint (0, "Incorrect password for %s.", user);
105       eprint (0, "The password for %s is unchanged.", user);
106       break;
107
108     default:
109       eprint (1, "unrecoverable error %d", ret);
110       break;
111     }
112   return 1;
113 }
114
115 PUSER_INFO_3
116 GetPW (char *user, int print_win_name, LPCWSTR server)
117 {
118   char usr_buf[UNLEN + 1];
119   WCHAR name[UNLEN + 1];
120   DWORD ret;
121   PUSER_INFO_3 ui;
122   struct passwd *pw;
123   char *domain = (char *) alloca (INTERNET_MAX_HOST_NAME_LENGTH + 1);
124      
125   /* Try getting a Win32 username in case the user edited /etc/passwd */
126   if ((pw = getpwnam (user)))
127     {
128       cygwin_internal (CW_EXTRACT_DOMAIN_AND_USER, pw, domain, usr_buf);
129       if (strcasecmp (pw->pw_name, usr_buf))
130         {
131           /* Hack to avoid problem with LookupAccountSid after impersonation */
132           if (strcasecmp (usr_buf, "SYSTEM"))
133             {
134               user = usr_buf;
135               if (print_win_name)
136                 printf ("Windows username : %s\n", user);
137             }
138         }
139     }
140   mbstowcs (name, user, UNLEN + 1);
141   ret = NetUserGetInfo (server, name, 3, (void *) &ui);
142   return EvalRet (ret, user) ? NULL : ui;
143 }
144
145 int
146 ChangePW (const char *user, const char *oldpwd, const char *pwd, int justcheck,
147           LPCWSTR server)
148 {
149   WCHAR name[UNLEN + 1], oldpass[512], pass[512];
150   DWORD ret;
151
152   mbstowcs (name, user, UNLEN + 1);
153   mbstowcs (pass, pwd, 512);
154   if (! oldpwd)
155     {
156       USER_INFO_1003 ui;
157
158       ui.usri1003_password = pass;
159       ret = NetUserSetInfo (server, name, 1003, (LPBYTE) &ui, NULL);
160     }
161   else
162     {
163       mbstowcs (oldpass, oldpwd, 512);
164       ret = NetUserChangePassword (server, name, oldpass, pass);
165     }
166   if (justcheck && ret != ERROR_INVALID_PASSWORD)
167     return 0;
168   if (! EvalRet (ret, user) && ! justcheck)
169     {
170       eprint (0, "Password changed.");
171     }
172   return ret;
173 }
174
175 void
176 PrintPW (PUSER_INFO_3 ui, LPCWSTR server)
177 {
178   time_t t = time (NULL) - ui->usri3_password_age;
179   int ret;
180   PUSER_MODALS_INFO_0 mi;
181
182   printf ("Account disabled           : %s",
183         (ui->usri3_flags & UF_ACCOUNTDISABLE) ? "yes\n" : "no\n");
184   printf ("Password not required      : %s",
185         (ui->usri3_flags & UF_PASSWD_NOTREQD) ? "yes\n" : "no\n");
186   printf ("User can't change password : %s",
187         (ui->usri3_flags & UF_PASSWD_CANT_CHANGE) ? "yes\n" : "no\n");
188   printf ("Password never expires     : %s",
189         (ui->usri3_flags & UF_DONT_EXPIRE_PASSWD) ? "yes\n" : "no\n");
190   printf ("Password expired           : %s",
191         (ui->usri3_password_expired) ? "yes\n" : "no\n");
192   printf ("Latest password change     : %s", ctime(&t));
193   ret = NetUserModalsGet (server, 0, (void *) &mi);
194   if (! ret)
195     {
196       if (mi->usrmod0_max_passwd_age == TIMEQ_FOREVER)
197         mi->usrmod0_max_passwd_age = 0;
198       if (mi->usrmod0_min_passwd_age == TIMEQ_FOREVER)
199         mi->usrmod0_min_passwd_age = 0;
200       if (mi->usrmod0_force_logoff == TIMEQ_FOREVER)
201         mi->usrmod0_force_logoff = 0;
202       if (ui->usri3_priv == USER_PRIV_ADMIN)
203         mi->usrmod0_min_passwd_len = 0;
204       printf ("\nSystem password settings:\n");
205       printf ("Max. password age %ld days\n",
206               mi->usrmod0_max_passwd_age / ONE_DAY);
207       printf ("Min. password age %ld days\n",
208               mi->usrmod0_min_passwd_age / ONE_DAY);
209       printf ("Force logout after %ld days\n",
210               mi->usrmod0_force_logoff / ONE_DAY);
211       printf ("Min. password length: %ld\n",
212               mi->usrmod0_min_passwd_len);
213     }
214 }
215
216 int
217 SetModals (int xarg, int narg, int iarg, int Larg, LPCWSTR server)
218 {
219   int ret;
220   PUSER_MODALS_INFO_0 mi;
221
222   ret = NetUserModalsGet (server, 0, (void *) &mi);
223   if (! ret)
224     {
225       if (xarg == 0)
226         mi->usrmod0_max_passwd_age = TIMEQ_FOREVER;
227       else if (xarg > 0)
228         mi->usrmod0_max_passwd_age = xarg * ONE_DAY;
229
230       if (narg == 0)
231         {
232           mi->usrmod0_min_passwd_age = TIMEQ_FOREVER;
233           mi->usrmod0_password_hist_len = 0;
234         }
235       else if (narg > 0)
236         mi->usrmod0_min_passwd_age = narg * ONE_DAY;
237
238       if (iarg == 0)
239         mi->usrmod0_force_logoff = TIMEQ_FOREVER;
240       else if (iarg > 0)
241         mi->usrmod0_force_logoff = iarg * ONE_DAY;
242
243       if (Larg >= 0)
244         mi->usrmod0_min_passwd_len = Larg;
245
246       ret = NetUserModalsSet (server, 0, (LPBYTE) mi, NULL);
247       NetApiBufferFree (mi);
248     }
249   return EvalRet (ret, NULL);
250 }
251
252 static void usage (FILE * stream, int status) __attribute__ ((noreturn));
253 static void
254 usage (FILE * stream, int status)
255 {
256   fprintf (stream, ""
257   "Usage: %s [OPTION] [USER]\n"
258   "Change USER's password or password attributes.\n"
259   "\n"
260   "User operations:\n"
261   "  -l, --lock               lock USER's account.\n"
262   "  -u, --unlock             unlock USER's account.\n"
263   "  -c, --cannot-change      USER can't change password.\n"
264   "  -C, --can-change         USER can change password.\n"
265   "  -e, --never-expires      USER's password never expires.\n"
266   "  -E, --expires            USER's password expires according to system's\n"
267   "                           password aging rule.\n"
268   "  -p, --pwd-not-required   no password required for USER.\n"
269   "  -P, --pwd-required       password is required for USER.\n"
270   "  -R, --reg-store-pwd      enter password to store it in the registry for\n"
271   "                           later usage by services to be able to switch\n"
272   "                           to this user context with network credentials.\n"
273   "\n"
274   "System operations:\n"
275   "  -i, --inactive NUM       set NUM of days before inactive accounts are disabled\n"
276   "                           (inactive accounts are those with expired passwords).\n"
277   "  -n, --minage DAYS        set system minimum password age to DAYS days.\n"
278   "  -x, --maxage DAYS        set system maximum password age to DAYS days.\n"
279   "  -L, --length LEN         set system minimum password length to LEN.\n"
280   "\n"
281   "Other options:\n"
282   "  -d, --logonserver SERVER connect to SERVER (e.g. domain controller).\n"
283   "                           Default server is the local system, unless\n"
284   "                           changing the current user, in which case the\n"
285   "                           default is the content of $LOGONSERVER.\n"
286   "  -S, --status             display password status for USER (locked, expired,\n"
287   "                           etc.) plus global system password settings.\n"
288   "  -h, --help               output usage information and exit.\n"
289   "  -v, --version            output version information and exit.\n"
290   "\n"
291   "If no option is given, change USER's password.  If no user name is given,\n"
292   "operate on current user.  System operations must not be mixed with user\n"
293   "operations.  Don't specify a USER when triggering a system operation.\n"
294   "\n"
295   "Don't specify a user or any other option together with the -R option.\n"
296   "Non-Admin users can only store their password if cygserver is running\n"
297   "as service under the SYSTEM account.\n"
298   "Note that storing even obfuscated passwords in the registry is not overly\n"
299   "secure.  Use this feature only if the machine is adequately locked down.\n"
300   "Don't use this feature if you don't need network access within a remote\n"
301   "session.  You can delete your stored password by using `passwd -R' and\n"
302   "specifying an empty password.\n"
303   "\n"
304   "Report bugs to <cygwin@cygwin.com>\n", prog_name);
305   exit (status);
306 }
307
308 static int
309 caller_is_admin ()
310 {
311   static int is_admin = -1;
312   HANDLE token;
313   DWORD size;
314   PTOKEN_GROUPS grps;
315   SID_IDENTIFIER_AUTHORITY nt_auth = {SECURITY_NT_AUTHORITY};
316   PSID admin_grp; 
317   DWORD i;
318
319   if (is_admin == -1)
320     {
321       is_admin = 0;
322       if (OpenProcessToken (GetCurrentProcess (), TOKEN_READ, &token))
323         {
324           GetTokenInformation (token, TokenGroups, NULL, 0, &size);
325           grps = (PTOKEN_GROUPS) alloca (size);
326           if (!GetTokenInformation(token, TokenGroups, grps, size, &size)
327               || !AllocateAndInitializeSid (&nt_auth, 2,
328                                             SECURITY_BUILTIN_DOMAIN_RID,
329                                             DOMAIN_ALIAS_RID_ADMINS,
330                                             0, 0, 0, 0, 0, 0, &admin_grp))
331             is_admin = 0;
332           else
333             {
334               for (i = 0; i < grps->GroupCount; ++i)
335                 if (EqualSid (admin_grp, grps->Groups[i].Sid)
336                     && (grps->Groups[i].Attributes
337                         & (SE_GROUP_ENABLED | SE_GROUP_USE_FOR_DENY_ONLY))
338                        == SE_GROUP_ENABLED)
339                   {
340                     is_admin = 1;
341                     break;
342                   }
343               FreeSid (admin_grp);
344             }
345           CloseHandle (token);
346         }
347     }
348   return is_admin;
349 }
350
351 static void
352 print_version ()
353 {
354   const char *v = strchr (version, ':');
355   int len;
356   if (!v)
357     {
358       v = "?";
359       len = 1;
360     }
361   else
362     {
363       v += 2;
364       len = strchr (v, ' ') - v;
365     }
366   printf ("\
367 %s (cygwin) %.*s\n\
368 Password Utility\n\
369 Copyright 1999, 2000, 2001, 2002, 2003 Red Hat, Inc.\n\
370 Compiled on %s\n\
371 ", prog_name, len, v, __DATE__);
372 }
373
374 int
375 main (int argc, char **argv)
376 {
377   char *c, *logonserver;
378   char user[UNLEN + 1], oldpwd[_PASSWORD_LEN + 1], newpwd[_PASSWORD_LEN + 1];
379   int ret = 0;
380   int cnt = 0;
381   int opt;
382   int Larg = -1;
383   int xarg = -1;
384   int narg = -1;
385   int iarg = -1;
386   int lopt = 0;
387   int uopt = 0;
388   int copt = 0;
389   int Copt = 0;
390   int eopt = 0;
391   int Eopt = 0;
392   int popt = 0;
393   int Popt = 0;
394   int Sopt = 0;
395   int Ropt = 0;
396   PUSER_INFO_3 ui;
397   int myself = 0;
398   LPWSTR server = NULL;
399
400   prog_name = strrchr (argv[0], '/');
401   if (prog_name == NULL)
402     prog_name = strrchr (argv[0], '\\');
403   if (prog_name == NULL)
404     prog_name = argv[0];
405   else
406     prog_name++;
407   c = strrchr (prog_name, '.');
408   if (c)
409     *c = '\0';
410
411   /* Use locale from environment.  If not set or set to "C", use UTF-8. */
412   setlocale (LC_CTYPE, "");
413   if (!strcmp (setlocale (LC_CTYPE, NULL), "C"))
414     setlocale (LC_CTYPE, "en_US.UTF-8");
415   while ((opt = getopt_long (argc, argv, opts, longopts, NULL)) != EOF)
416     switch (opt)
417       {
418       case 'h':
419         usage (stdout, 0);
420         break;
421
422       case 'i':
423         if (lopt || uopt || copt || Copt || eopt || Eopt || popt || Popt || Sopt || Ropt)
424           usage (stderr, 1);
425         if ((iarg = atoi (optarg)) < 0 || iarg > 999)
426           return eprint (1, "Force logout time must be between 0 and 999.");
427         break;
428
429       case 'l':
430         if (xarg >= 0 || narg >= 0 || iarg >= 0 || Larg >= 0 || uopt || Sopt || Ropt)
431           usage (stderr, 1);
432         lopt = 1;
433         break;
434
435       case 'n':
436         if (lopt || uopt || copt || Copt || eopt || Eopt || popt || Popt || Sopt || Ropt)
437           usage (stderr, 1);
438         if ((narg = atoi (optarg)) < 0 || narg > 999)
439           return eprint (1, "Minimum password age must be between 0 and 999.");
440         if (xarg >= 0 && narg > xarg)
441           return eprint (1, "Minimum password age must be less than "
442                             "maximum password age.");
443         break;
444
445       case 'u':
446         if (xarg >= 0 || narg >= 0 || iarg >= 0 || Larg >= 0 || lopt || Sopt || Ropt)
447           usage (stderr, 1);
448         uopt = 1;
449         break;
450
451       case 'c':
452         if (xarg >= 0 || narg >= 0 || iarg >= 0 || Larg >= 0 || Sopt || Ropt)
453           usage (stderr, 1);
454         copt = 1;
455         break;
456
457       case 'C':
458         if (xarg >= 0 || narg >= 0 || iarg >= 0 || Larg >= 0 || Sopt || Ropt)
459           usage (stderr, 1);
460         Copt = 1;
461         break;
462
463       case 'd':
464         {
465           if (Ropt)
466             usage (stderr, 1);
467           char *tmpbuf = alloca (strlen (optarg) + 3);
468           tmpbuf[0] = '\0';
469           if (*optarg != '\\')
470             strcpy (tmpbuf, "\\\\");
471           strcat (tmpbuf, optarg);
472           size_t len = mbstowcs (NULL, tmpbuf, 0);
473           if (len > 0 && len != (size_t) -1)
474             mbstowcs (server = alloca ((len + 1) * sizeof (wchar_t)),
475                       tmpbuf, len + 1);
476         }
477         break;
478
479       case 'e':
480         if (xarg >= 0 || narg >= 0 || iarg >= 0 || Larg >= 0 || Sopt || Ropt)
481           usage (stderr, 1);
482         eopt = 1;
483         break;
484
485       case 'E':
486         if (xarg >= 0 || narg >= 0 || iarg >= 0 || Larg >= 0 || Sopt || Ropt)
487           usage (stderr, 1);
488         Eopt = 1;
489         break;
490
491       case 'p':
492         if (xarg >= 0 || narg >= 0 || iarg >= 0 || Larg >= 0 || Sopt || Ropt)
493           usage (stderr, 1);
494         popt = 1;
495         break;
496
497       case 'P':
498         if (xarg >= 0 || narg >= 0 || iarg >= 0 || Larg >= 0 || Sopt || Ropt)
499           usage (stderr, 1);
500         Popt = 1;
501         break;
502
503       case 'v':
504         print_version ();
505         exit (0);
506         break;
507
508       case 'x':
509         if (lopt || uopt || copt || Copt || eopt || Eopt || popt || Popt || Sopt || Ropt)
510           usage (stderr, 1);
511         if ((xarg = atoi (optarg)) < 0 || xarg > 999)
512           return eprint (1, "Maximum password age must be between 0 and 999.");
513         if (narg >= 0 && xarg < narg)
514           return eprint (1, "Maximum password age must be greater than "
515                             "minimum password age.");
516         break;
517
518       case 'L':
519         if (lopt || uopt || copt || Copt || eopt || Eopt || popt || Popt || Sopt || Ropt)
520           usage (stderr, 1);
521         if ((Larg = atoi (optarg)) < 0 || Larg > LM20_PWLEN)
522           return eprint (1, "Minimum password length must be between "
523                             "0 and %d.", LM20_PWLEN);
524         break;
525
526       case 'S':
527         if (xarg >= 0 || narg >= 0 || iarg >= 0 || Larg >= 0 || lopt || uopt
528             || copt || Copt || eopt || Eopt || popt || Popt || Ropt)
529           usage (stderr, 1);
530         Sopt = 1;
531         break;
532
533       case 'R':
534         if (xarg >= 0 || narg >= 0 || iarg >= 0 || Larg >= 0 || lopt || uopt
535             || copt || Copt || eopt || Eopt || popt || Popt || Sopt
536             || server)
537           usage (stderr, 1);
538         Ropt = 1;
539         break;
540
541       default:
542         usage (stderr, 1);
543       }
544
545   if (Ropt)
546     {
547       if (optind < argc)
548         usage (stderr, 1);
549       printf (
550 "This functionality stores a password in the registry for usage by services\n"
551 "which need to change the user context and require network access.  Typical\n"
552 "applications are interactive remote logons using sshd, cron task, etc.\n"
553 "This password will always tried first when any privileged application is\n"
554 "about to switch the user context.\n\n"
555 "Note that storing even obfuscated passwords in the registry is not overly\n"
556 "secure.  Use this feature only if the machine is adequately locked down.\n"
557 "Don't use this feature if you don't need network access within a remote\n"
558 "session.\n\n"
559 "You can delete your stored password by specifying an empty password.\n\n");
560       strcpy (newpwd, getpass ("Enter your current password: "));
561       if (strcmp (newpwd, getpass ("Re-enter your current password: ")))
562         eprint (0, "Password is not identical.");
563       else if (cygwin_internal (CW_SET_PRIV_KEY, newpwd))
564         return eprint (0, "Storing password failed: %s", strerror (errno));
565       return 0;
566     }
567
568   if (Larg >= 0 || xarg >= 0 || narg >= 0 || iarg >= 0)
569     {
570       if (optind < argc)
571         usage (stderr, 1);
572       return SetModals (xarg, narg, iarg, Larg, server);
573     }
574
575   strcpy (user, optind >= argc ? getlogin () : argv[optind]);
576
577   /* Changing password for calling user?  Use logonserver for user as well. */
578   if (!server && optind >= argc)
579     {
580       myself = 1;
581       if ((logonserver = getenv ("LOGONSERVER")))
582         {
583           size_t len = mbstowcs (NULL, logonserver, 0);
584           if (len > 0 && len != (size_t) -1)
585             mbstowcs (server = alloca ((len + 1) * sizeof (wchar_t)),
586                       logonserver, len + 1);
587         }
588     }
589
590   ui = GetPW (user, 1, server);
591   if (! ui)
592     return 1;
593
594   if (lopt || uopt || copt || Copt || eopt || Eopt || popt || Popt || Sopt)
595     {
596       USER_INFO_1008 uif;
597
598       uif.usri1008_flags = ui->usri3_flags;
599       if (lopt)
600         {
601           if (ui->usri3_priv == USER_PRIV_ADMIN)
602             return eprint (0, "Locking an admin account is disallowed.");
603           uif.usri1008_flags |= UF_ACCOUNTDISABLE;
604         }
605       if (uopt)
606         uif.usri1008_flags &= ~UF_ACCOUNTDISABLE;
607       if (copt)
608         uif.usri1008_flags |= UF_PASSWD_CANT_CHANGE;
609       if (Copt)
610         uif.usri1008_flags &= ~UF_PASSWD_CANT_CHANGE;
611       if (eopt)
612         uif.usri1008_flags |= UF_DONT_EXPIRE_PASSWD;
613       if (Eopt)
614         uif.usri1008_flags &= ~UF_DONT_EXPIRE_PASSWD;
615       if (popt)
616         uif.usri1008_flags |= UF_PASSWD_NOTREQD;
617       if (Popt)
618         uif.usri1008_flags &= ~UF_PASSWD_NOTREQD;
619
620       if (lopt || uopt || copt || Copt || eopt || Eopt || popt || Popt)
621         {
622           ret = NetUserSetInfo (server, ui->usri3_name, 1008, (LPBYTE) &uif,
623                                 NULL);
624           return EvalRet (ret, NULL);
625         }
626       // Sopt
627       PrintPW (ui, server);
628       return 0;
629     }
630
631   if (!caller_is_admin () && !myself)
632     return eprint (0, "You may not change the password for %s.", user);
633
634   eprint (0, "Enter the new password (minimum of 5, maximum of 8 characters).");
635   eprint (0, "Please use a combination of upper and lower case letters and numbers.");
636
637   oldpwd[0] = '\0';
638   if (!caller_is_admin ())
639     {
640       strcpy (oldpwd, getpass ("Old password: "));
641       if (ChangePW (user, oldpwd, oldpwd, 1, server))
642         return 1;
643     }
644
645   do
646     {
647       strcpy (newpwd, getpass ("New password: "));
648       if (strcmp (newpwd, getpass ("Re-enter new password: ")))
649         eprint (0, "Password is not identical.");
650       else if (! ChangePW (user, *oldpwd ? oldpwd : NULL, newpwd, 0, server))
651         ret = 1;
652       if (! ret && cnt < 2)
653         eprint (0, "Try again.");
654     }
655   while (! ret && ++cnt < 3);
656   return ! ret;
657 }