CVE-2020-28008: Assorted attacks in Exim's spool directory
[exim.git] / src / src / dbfn.c
1 /*************************************************
2 *     Exim - an Internet mail transport agent    *
3 *************************************************/
4
5 /* Copyright (c) University of Cambridge 1995 - 2018 */
6 /* Copyright (c) The Exim Maintainers 2020 */
7 /* See the file NOTICE for conditions of use and distribution. */
8
9
10 #include "exim.h"
11
12
13 /* Functions for accessing Exim's hints database, which consists of a number of
14 different DBM files. This module does not contain code for reading DBM files
15 for (e.g.) alias expansion. That is all contained within the general search
16 functions. As Exim now has support for several DBM interfaces, all the relevant
17 functions are called as macros.
18
19 All the data in Exim's database is in the nature of *hints*. Therefore it
20 doesn't matter if it gets destroyed by accident. These functions are not
21 supposed to implement a "safe" database.
22
23 Keys are passed in as C strings, and the terminating zero *is* used when
24 building the dbm files. This just makes life easier when scanning the files
25 sequentially.
26
27 Synchronization is required on the database files, and this is achieved by
28 means of locking on independent lock files. (Earlier attempts to lock on the
29 DBM files themselves were never completely successful.) Since callers may in
30 general want to do more than one read or write while holding the lock, there
31 are separate open and close functions. However, the calling modules should
32 arrange to hold the locks for the bare minimum of time. */
33
34
35
36 /*************************************************
37 *         Berkeley DB error callback             *
38 *************************************************/
39
40 /* For Berkeley DB >= 2, we can define a function to be called in case of DB
41 errors. This should help with debugging strange DB problems, e.g. getting "File
42 exists" when you try to open a db file. The API for this function was changed
43 at DB release 4.3. */
44
45 #if defined(USE_DB) && defined(DB_VERSION_STRING)
46 void
47 #if DB_VERSION_MAJOR > 4 || (DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR >= 3)
48 dbfn_bdb_error_callback(const DB_ENV *dbenv, const char *pfx, const char *msg)
49 {
50 dbenv = dbenv;
51 #else
52 dbfn_bdb_error_callback(const char *pfx, char *msg)
53 {
54 #endif
55 pfx = pfx;
56 log_write(0, LOG_MAIN, "Berkeley DB error: %s", msg);
57 }
58 #endif
59
60
61
62
63 static enum {
64   PRIV_DROPPING, PRIV_DROPPED,
65   PRIV_RESTORING, PRIV_RESTORED
66 } priv_state = PRIV_RESTORED;
67
68 static uid_t priv_euid;
69 static gid_t priv_egid;
70 static gid_t priv_groups[EXIM_GROUPLIST_SIZE + 1];
71 static int priv_ngroups;
72
73 /* Inspired by OpenSSH's temporarily_use_uid(). Thanks! */
74
75 static void
76 priv_drop_temp(const uid_t temp_uid, const gid_t temp_gid)
77 {
78 if (priv_state != PRIV_RESTORED) _exit(EXIT_FAILURE);
79 priv_state = PRIV_DROPPING;
80
81 priv_euid = geteuid();
82 if (priv_euid == root_uid)
83   {
84   priv_egid = getegid();
85   priv_ngroups = getgroups(nelem(priv_groups), priv_groups);
86   if (priv_ngroups < 0) _exit(EXIT_FAILURE);
87
88   if (priv_ngroups > 0 && setgroups(1, &temp_gid) != 0) _exit(EXIT_FAILURE);
89   if (setegid(temp_gid) != 0) _exit(EXIT_FAILURE);
90   if (seteuid(temp_uid) != 0) _exit(EXIT_FAILURE);
91
92   if (geteuid() != temp_uid) _exit(EXIT_FAILURE);
93   if (getegid() != temp_gid) _exit(EXIT_FAILURE);
94   }
95
96 priv_state = PRIV_DROPPED;
97 }
98
99 /* Inspired by OpenSSH's restore_uid(). Thanks! */
100
101 static void
102 priv_restore(void)
103 {
104 if (priv_state != PRIV_DROPPED) _exit(EXIT_FAILURE);
105 priv_state = PRIV_RESTORING;
106
107 if (priv_euid == root_uid)
108   {
109   if (seteuid(priv_euid) != 0) _exit(EXIT_FAILURE);
110   if (setegid(priv_egid) != 0) _exit(EXIT_FAILURE);
111   if (priv_ngroups > 0 && setgroups(priv_ngroups, priv_groups) != 0) _exit(EXIT_FAILURE);
112
113   if (geteuid() != priv_euid) _exit(EXIT_FAILURE);
114   if (getegid() != priv_egid) _exit(EXIT_FAILURE);
115   }
116
117 priv_state = PRIV_RESTORED;
118 }
119
120
121
122
123 /*************************************************
124 *          Open and lock a database file         *
125 *************************************************/
126
127 /* Used for accessing Exim's hints databases.
128
129 Arguments:
130   name     The single-component name of one of Exim's database files.
131   flags    Either O_RDONLY or O_RDWR, indicating the type of open required;
132              O_RDWR implies "create if necessary"
133   dbblock  Points to an open_db block to be filled in.
134   lof      If TRUE, write to the log for actual open failures (locking failures
135            are always logged).
136   panic    If TRUE, panic on failure to create the db directory
137
138 Returns:   NULL if the open failed, or the locking failed. After locking
139            failures, errno is zero.
140
141            On success, dbblock is returned. This contains the dbm pointer and
142            the fd of the locked lock file.
143
144 There are some calls that use O_RDWR|O_CREAT for the flags. Having discovered
145 this in December 2005, I'm not sure if this is correct or not, but for the
146 moment I haven't changed them.
147 */
148
149 open_db *
150 dbfn_open(uschar *name, int flags, open_db *dbblock, BOOL lof, BOOL panic)
151 {
152 int rc, save_errno;
153 BOOL read_only = flags == O_RDONLY;
154 flock_t lock_data;
155 uschar dirname[256], filename[256];
156
157 DEBUG(D_hints_lookup) acl_level++;
158
159 /* The first thing to do is to open a separate file on which to lock. This
160 ensures that Exim has exclusive use of the database before it even tries to
161 open it. Early versions tried to lock on the open database itself, but that
162 gave rise to mysterious problems from time to time - it was suspected that some
163 DB libraries "do things" on their open() calls which break the interlocking.
164 The lock file is never written to, but we open it for writing so we can get a
165 write lock if required. If it does not exist, we create it. This is done
166 separately so we know when we have done it, because when running as root we
167 need to change the ownership - see the bottom of this function. We also try to
168 make the directory as well, just in case. We won't be doing this many times
169 unnecessarily, because usually the lock file will be there. If the directory
170 exists, there is no error. */
171
172 snprintf(CS dirname, sizeof(dirname), "%s/db", spool_directory);
173 snprintf(CS filename, sizeof(filename), "%s/%s.lockfile", dirname, name);
174
175 priv_drop_temp(exim_uid, exim_gid);
176 if ((dbblock->lockfd = Uopen(filename, O_RDWR, EXIMDB_LOCKFILE_MODE)) < 0)
177   {
178   (void)directory_make(spool_directory, US"db", EXIMDB_DIRECTORY_MODE, panic);
179   dbblock->lockfd = Uopen(filename, O_RDWR|O_CREAT, EXIMDB_LOCKFILE_MODE);
180   }
181 priv_restore();
182
183 if (dbblock->lockfd < 0)
184   {
185   log_write(0, LOG_MAIN, "%s",
186     string_open_failed(errno, "database lock file %s", filename));
187   errno = 0;      /* Indicates locking failure */
188   DEBUG(D_hints_lookup) acl_level--;
189   return NULL;
190   }
191
192 /* Now we must get a lock on the opened lock file; do this with a blocking
193 lock that times out. */
194
195 lock_data.l_type = read_only? F_RDLCK : F_WRLCK;
196 lock_data.l_whence = lock_data.l_start = lock_data.l_len = 0;
197
198 DEBUG(D_hints_lookup|D_retry|D_route|D_deliver)
199   debug_printf_indent("locking %s\n", filename);
200
201 sigalrm_seen = FALSE;
202 ALARM(EXIMDB_LOCK_TIMEOUT);
203 rc = fcntl(dbblock->lockfd, F_SETLKW, &lock_data);
204 ALARM_CLR(0);
205
206 if (sigalrm_seen) errno = ETIMEDOUT;
207 if (rc < 0)
208   {
209   log_write(0, LOG_MAIN|LOG_PANIC, "Failed to get %s lock for %s: %s",
210     read_only ? "read" : "write", filename,
211     errno == ETIMEDOUT ? "timed out" : strerror(errno));
212   (void)close(dbblock->lockfd);
213   errno = 0;       /* Indicates locking failure */
214   DEBUG(D_hints_lookup) acl_level--;
215   return NULL;
216   }
217
218 DEBUG(D_hints_lookup) debug_printf_indent("locked  %s\n", filename);
219
220 /* At this point we have an opened and locked separate lock file, that is,
221 exclusive access to the database, so we can go ahead and open it. If we are
222 expected to create it, don't do so at first, again so that we can detect
223 whether we need to change its ownership (see comments about the lock file
224 above.) There have been regular reports of crashes while opening hints
225 databases - often this is caused by non-matching db.h and the library. To make
226 it easy to pin this down, there are now debug statements on either side of the
227 open call. */
228
229 snprintf(CS filename, sizeof(filename), "%s/%s", dirname, name);
230
231 priv_drop_temp(exim_uid, exim_gid);
232 EXIM_DBOPEN(filename, dirname, flags, EXIMDB_MODE, &(dbblock->dbptr));
233 if (!dbblock->dbptr && errno == ENOENT && flags == O_RDWR)
234   {
235   DEBUG(D_hints_lookup)
236     debug_printf_indent("%s appears not to exist: trying to create\n", filename);
237   EXIM_DBOPEN(filename, dirname, flags|O_CREAT, EXIMDB_MODE, &(dbblock->dbptr));
238   }
239 save_errno = errno;
240 priv_restore();
241
242 /* If the open has failed, return NULL, leaving errno set. If lof is TRUE,
243 log the event - also for debugging - but debug only if the file just doesn't
244 exist. */
245
246 if (!dbblock->dbptr)
247   {
248   if (lof && save_errno != ENOENT)
249     log_write(0, LOG_MAIN, "%s", string_open_failed(save_errno, "DB file %s",
250         filename));
251   else
252     DEBUG(D_hints_lookup)
253       debug_printf_indent("%s\n", CS string_open_failed(save_errno, "DB file %s",
254           filename));
255   (void)close(dbblock->lockfd);
256   errno = save_errno;
257   DEBUG(D_hints_lookup) acl_level--;
258   return NULL;
259   }
260
261 DEBUG(D_hints_lookup)
262   debug_printf_indent("opened hints database %s: flags=%s\n", filename,
263     flags == O_RDONLY ? "O_RDONLY"
264     : flags == O_RDWR ? "O_RDWR"
265     : flags == (O_RDWR|O_CREAT) ? "O_RDWR|O_CREAT"
266     : "??");
267
268 /* Pass back the block containing the opened database handle and the open fd
269 for the lock. */
270
271 return dbblock;
272 }
273
274
275
276
277 /*************************************************
278 *         Unlock and close a database file       *
279 *************************************************/
280
281 /* Closing a file automatically unlocks it, so after closing the database, just
282 close the lock file.
283
284 Argument: a pointer to an open database block
285 Returns:  nothing
286 */
287
288 void
289 dbfn_close(open_db *dbblock)
290 {
291 EXIM_DBCLOSE(dbblock->dbptr);
292 (void)close(dbblock->lockfd);
293 DEBUG(D_hints_lookup)
294   { debug_printf_indent("closed hints database and lockfile\n"); acl_level--; }
295 }
296
297
298
299
300 /*************************************************
301 *             Read from database file            *
302 *************************************************/
303
304 /* Passing back the pointer unchanged is useless, because there is
305 no guarantee of alignment. Since all the records used by Exim need
306 to be properly aligned to pick out the timestamps, etc., we might as
307 well do the copying centrally here.
308
309 Most calls don't need the length, so there is a macro called dbfn_read which
310 has two arguments; it calls this function adding NULL as the third.
311
312 Arguments:
313   dbblock   a pointer to an open database block
314   key       the key of the record to be read
315   length    a pointer to an int into which to return the length, if not NULL
316
317 Returns: a pointer to the retrieved record, or
318          NULL if the record is not found
319 */
320
321 void *
322 dbfn_read_with_length(open_db *dbblock, const uschar *key, int *length)
323 {
324 void *yield;
325 EXIM_DATUM key_datum, result_datum;
326 int klen = Ustrlen(key) + 1;
327 uschar * key_copy = store_get(klen, is_tainted(key));
328
329 memcpy(key_copy, key, klen);
330
331 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_read: key=%s\n", key);
332
333 EXIM_DATUM_INIT(key_datum);         /* Some DBM libraries require the datum */
334 EXIM_DATUM_INIT(result_datum);      /* to be cleared before use. */
335 EXIM_DATUM_DATA(key_datum) = CS key_copy;
336 EXIM_DATUM_SIZE(key_datum) = klen;
337
338 if (!EXIM_DBGET(dbblock->dbptr, key_datum, result_datum)) return NULL;
339
340 /* Assume the data store could have been tainted.  Properly, we should
341 store the taint status with the data. */
342
343 yield = store_get(EXIM_DATUM_SIZE(result_datum), TRUE);
344 memcpy(yield, EXIM_DATUM_DATA(result_datum), EXIM_DATUM_SIZE(result_datum));
345 if (length != NULL) *length = EXIM_DATUM_SIZE(result_datum);
346
347 EXIM_DATUM_FREE(result_datum);    /* Some DBM libs require freeing */
348 return yield;
349 }
350
351
352
353 /*************************************************
354 *             Write to database file             *
355 *************************************************/
356
357 /*
358 Arguments:
359   dbblock   a pointer to an open database block
360   key       the key of the record to be written
361   ptr       a pointer to the record to be written
362   length    the length of the record to be written
363
364 Returns:    the yield of the underlying dbm or db "write" function. If this
365             is dbm, the value is zero for OK.
366 */
367
368 int
369 dbfn_write(open_db *dbblock, const uschar *key, void *ptr, int length)
370 {
371 EXIM_DATUM key_datum, value_datum;
372 dbdata_generic *gptr = (dbdata_generic *)ptr;
373 int klen = Ustrlen(key) + 1;
374 uschar * key_copy = store_get(klen, is_tainted(key));
375
376 memcpy(key_copy, key, klen);
377 gptr->time_stamp = time(NULL);
378
379 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_write: key=%s\n", key);
380
381 EXIM_DATUM_INIT(key_datum);         /* Some DBM libraries require the datum */
382 EXIM_DATUM_INIT(value_datum);       /* to be cleared before use. */
383 EXIM_DATUM_DATA(key_datum) = CS key_copy;
384 EXIM_DATUM_SIZE(key_datum) = klen;
385 EXIM_DATUM_DATA(value_datum) = CS ptr;
386 EXIM_DATUM_SIZE(value_datum) = length;
387 return EXIM_DBPUT(dbblock->dbptr, key_datum, value_datum);
388 }
389
390
391
392 /*************************************************
393 *           Delete record from database file     *
394 *************************************************/
395
396 /*
397 Arguments:
398   dbblock    a pointer to an open database block
399   key        the key of the record to be deleted
400
401 Returns: the yield of the underlying dbm or db "delete" function.
402 */
403
404 int
405 dbfn_delete(open_db *dbblock, const uschar *key)
406 {
407 int klen = Ustrlen(key) + 1;
408 uschar * key_copy = store_get(klen, is_tainted(key));
409
410 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_delete: key=%s\n", key);
411
412 memcpy(key_copy, key, klen);
413 EXIM_DATUM key_datum;
414 EXIM_DATUM_INIT(key_datum);         /* Some DBM libraries require clearing */
415 EXIM_DATUM_DATA(key_datum) = CS key_copy;
416 EXIM_DATUM_SIZE(key_datum) = klen;
417 return EXIM_DBDEL(dbblock->dbptr, key_datum);
418 }
419
420
421
422 /*************************************************
423 *         Scan the keys of a database file       *
424 *************************************************/
425
426 /*
427 Arguments:
428   dbblock  a pointer to an open database block
429   start    TRUE if starting a new scan
430            FALSE if continuing with the current scan
431   cursor   a pointer to a pointer to a cursor anchor, for those dbm libraries
432            that use the notion of a cursor
433
434 Returns:   the next record from the file, or
435            NULL if there are no more
436 */
437
438 uschar *
439 dbfn_scan(open_db *dbblock, BOOL start, EXIM_CURSOR **cursor)
440 {
441 EXIM_DATUM key_datum, value_datum;
442 uschar *yield;
443 value_datum = value_datum;    /* dummy; not all db libraries use this */
444
445 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_scan\n");
446
447 /* Some dbm require an initialization */
448
449 if (start) EXIM_DBCREATE_CURSOR(dbblock->dbptr, cursor);
450
451 EXIM_DATUM_INIT(key_datum);         /* Some DBM libraries require the datum */
452 EXIM_DATUM_INIT(value_datum);       /* to be cleared before use. */
453
454 yield = (EXIM_DBSCAN(dbblock->dbptr, key_datum, value_datum, start, *cursor))?
455   US EXIM_DATUM_DATA(key_datum) : NULL;
456
457 /* Some dbm require a termination */
458
459 if (!yield) EXIM_DBDELETE_CURSOR(*cursor);
460 return yield;
461 }
462
463
464
465 /*************************************************
466 **************************************************
467 *             Stand-alone test program           *
468 **************************************************
469 *************************************************/
470
471 #ifdef STAND_ALONE
472
473 int
474 main(int argc, char **cargv)
475 {
476 open_db dbblock[8];
477 int max_db = sizeof(dbblock)/sizeof(open_db);
478 int current = -1;
479 int showtime = 0;
480 int i;
481 dbdata_wait *dbwait = NULL;
482 uschar **argv = USS cargv;
483 uschar buffer[256];
484 uschar structbuffer[1024];
485
486 if (argc != 2)
487   {
488   printf("Usage: test_dbfn directory\n");
489   printf("The subdirectory called \"db\" in the given directory is used for\n");
490   printf("the files used in this test program.\n");
491   return 1;
492   }
493
494 /* Initialize */
495
496 spool_directory = argv[1];
497 debug_selector = D_all - D_memory;
498 debug_file = stderr;
499 big_buffer = malloc(big_buffer_size);
500
501 for (i = 0; i < max_db; i++) dbblock[i].dbptr = NULL;
502
503 printf("\nExim's db functions tester: interface type is %s\n", EXIM_DBTYPE);
504 printf("DBM library: ");
505
506 #ifdef DB_VERSION_STRING
507 printf("Berkeley DB: %s\n", DB_VERSION_STRING);
508 #elif defined(BTREEVERSION) && defined(HASHVERSION)
509   #ifdef USE_DB
510   printf("probably Berkeley DB version 1.8x (native mode)\n");
511   #else
512   printf("probably Berkeley DB version 1.8x (compatibility mode)\n");
513   #endif
514 #elif defined(_DBM_RDONLY) || defined(dbm_dirfno)
515 printf("probably ndbm\n");
516 #elif defined(USE_TDB)
517 printf("using tdb\n");
518 #else
519   #ifdef USE_GDBM
520   printf("probably GDBM (native mode)\n");
521   #else
522   printf("probably GDBM (compatibility mode)\n");
523   #endif
524 #endif
525
526 /* Test the functions */
527
528 printf("\nTest the functions\n> ");
529
530 while (Ufgets(buffer, 256, stdin) != NULL)
531   {
532   int len = Ustrlen(buffer);
533   int count = 1;
534   clock_t start = 1;
535   clock_t stop = 0;
536   uschar *cmd = buffer;
537   while (len > 0 && isspace((uschar)buffer[len-1])) len--;
538   buffer[len] = 0;
539
540   if (isdigit((uschar)*cmd))
541     {
542     count = Uatoi(cmd);
543     while (isdigit((uschar)*cmd)) cmd++;
544     while (isspace((uschar)*cmd)) cmd++;
545     }
546
547   if (Ustrncmp(cmd, "open", 4) == 0)
548     {
549     int i;
550     open_db *odb;
551     uschar *s = cmd + 4;
552     while (isspace((uschar)*s)) s++;
553
554     for (i = 0; i < max_db; i++)
555       if (dbblock[i].dbptr == NULL) break;
556
557     if (i >= max_db)
558       {
559       printf("Too many open databases\n> ");
560       continue;
561       }
562
563     start = clock();
564     odb = dbfn_open(s, O_RDWR, dbblock + i, TRUE, TRUE);
565     stop = clock();
566
567     if (odb)
568       {
569       current = i;
570       printf("opened %d\n", current);
571       }
572     /* Other error cases will have written messages */
573     else if (errno == ENOENT)
574       {
575       printf("open failed: %s%s\n", strerror(errno),
576         #ifdef USE_DB
577         " (or other Berkeley DB error)"
578         #else
579         ""
580         #endif
581         );
582       }
583     }
584
585   else if (Ustrncmp(cmd, "write", 5) == 0)
586     {
587     int rc = 0;
588     uschar *key = cmd + 5;
589     uschar *data;
590
591     if (current < 0)
592       {
593       printf("No current database\n");
594       continue;
595       }
596
597     while (isspace((uschar)*key)) key++;
598     data = key;
599     while (*data != 0 && !isspace((uschar)*data)) data++;
600     *data++ = 0;
601     while (isspace((uschar)*data)) data++;
602
603     dbwait = (dbdata_wait *)(&structbuffer);
604     Ustrcpy(dbwait->text, data);
605
606     start = clock();
607     while (count-- > 0)
608       rc = dbfn_write(dbblock + current, key, dbwait,
609         Ustrlen(data) + sizeof(dbdata_wait));
610     stop = clock();
611     if (rc != 0) printf("Failed: %s\n", strerror(errno));
612     }
613
614   else if (Ustrncmp(cmd, "read", 4) == 0)
615     {
616     uschar *key = cmd + 4;
617     if (current < 0)
618       {
619       printf("No current database\n");
620       continue;
621       }
622     while (isspace((uschar)*key)) key++;
623     start = clock();
624     while (count-- > 0)
625       dbwait = (dbdata_wait *)dbfn_read_with_length(dbblock+ current, key, NULL);
626     stop = clock();
627     printf("%s\n", (dbwait == NULL)? "<not found>" : CS dbwait->text);
628     }
629
630   else if (Ustrncmp(cmd, "delete", 6) == 0)
631     {
632     uschar *key = cmd + 6;
633     if (current < 0)
634       {
635       printf("No current database\n");
636       continue;
637       }
638     while (isspace((uschar)*key)) key++;
639     dbfn_delete(dbblock + current, key);
640     }
641
642   else if (Ustrncmp(cmd, "scan", 4) == 0)
643     {
644     EXIM_CURSOR *cursor;
645     BOOL startflag = TRUE;
646     uschar *key;
647     uschar keybuffer[256];
648     if (current < 0)
649       {
650       printf("No current database\n");
651       continue;
652       }
653     start = clock();
654     while ((key = dbfn_scan(dbblock + current, startflag, &cursor)) != NULL)
655       {
656       startflag = FALSE;
657       Ustrcpy(keybuffer, key);
658       dbwait = (dbdata_wait *)dbfn_read_with_length(dbblock + current,
659         keybuffer, NULL);
660       printf("%s: %s\n", keybuffer, dbwait->text);
661       }
662     stop = clock();
663     printf("End of scan\n");
664     }
665
666   else if (Ustrncmp(cmd, "close", 5) == 0)
667     {
668     uschar *s = cmd + 5;
669     while (isspace((uschar)*s)) s++;
670     i = Uatoi(s);
671     if (i >= max_db || dbblock[i].dbptr == NULL) printf("Not open\n"); else
672       {
673       start = clock();
674       dbfn_close(dbblock + i);
675       stop = clock();
676       dbblock[i].dbptr = NULL;
677       if (i == current) current = -1;
678       }
679     }
680
681   else if (Ustrncmp(cmd, "file", 4) == 0)
682     {
683     uschar *s = cmd + 4;
684     while (isspace((uschar)*s)) s++;
685     i = Uatoi(s);
686     if (i >= max_db || dbblock[i].dbptr == NULL) printf("Not open\n");
687       else current = i;
688     }
689
690   else if (Ustrncmp(cmd, "time", 4) == 0)
691     {
692     showtime = ~showtime;
693     printf("Timing %s\n", showtime? "on" : "off");
694     }
695
696   else if (Ustrcmp(cmd, "q") == 0 || Ustrncmp(cmd, "quit", 4) == 0) break;
697
698   else if (Ustrncmp(cmd, "help", 4) == 0)
699     {
700     printf("close  [<number>]              close file [<number>]\n");
701     printf("delete <key>                   remove record from current file\n");
702     printf("file   <number>                make file <number> current\n");
703     printf("open   <name>                  open db file\n");
704     printf("q[uit]                         exit program\n");
705     printf("read   <key>                   read record from current file\n");
706     printf("scan                           scan current file\n");
707     printf("time                           time display on/off\n");
708     printf("write  <key> <rest-of-line>    write record to current file\n");
709     }
710
711   else printf("Eh?\n");
712
713   if (showtime && stop >= start)
714     printf("start=%d stop=%d difference=%d\n", (int)start, (int)stop,
715      (int)(stop - start));
716
717   printf("> ");
718   }
719
720 for (i = 0; i < max_db; i++)
721   {
722   if (dbblock[i].dbptr != NULL)
723     {
724     printf("\nClosing %d", i);
725     dbfn_close(dbblock + i);
726     }
727   }
728
729 printf("\n");
730 return 0;
731 }
732
733 #endif
734
735 /* End of dbfn.c */