1 /* 2 * exportfs - Export a plan 9 name space across a network 3 */ 4 #include <u.h> 5 #include <libc.h> 6 #include <auth.h> 7 #include <fcall.h> 8 #include <libsec.h> 9 #define Extern 10 #include "exportfs.h" 11 12 #define QIDPATH ((1LL<<48)-1) 13 vlong newqid = 0; 14 15 enum { 16 Encnone, 17 Encssl, 18 Enctls, 19 }; 20 21 void (*fcalls[])(Fsrpc*) = 22 { 23 [Tversion] Xversion, 24 [Tauth] Xauth, 25 [Tflush] Xflush, 26 [Tattach] Xattach, 27 [Twalk] Xwalk, 28 [Topen] slave, 29 [Tcreate] Xcreate, 30 [Tclunk] Xclunk, 31 [Tread] slave, 32 [Twrite] slave, 33 [Tremove] Xremove, 34 [Tstat] Xstat, 35 [Twstat] Xwstat, 36 }; 37 38 /* accounting and debugging counters */ 39 int filecnt; 40 int freecnt; 41 int qidcnt; 42 int qfreecnt; 43 int ncollision; 44 45 int netfd; 46 int srvfd = -1; 47 int nonone = 1; 48 char *filterp; 49 char *ealgs = "rc4_256 sha1"; 50 char *aanfilter = "/bin/aan"; 51 int encproto = Encnone; 52 int readonly; 53 54 static void mksecret(char *, uchar *); 55 static int localread9pmsg(int, void *, uint, ulong *); 56 static char *anstring = "tcp!*!0"; 57 int filter(int, char *); 58 59 void 60 usage(void) 61 { 62 fprint(2, "usage: %s [-ads] [-f dbgfile] [-m msize] [-r root] [-S srvfile] [-e 'crypt hash'] [-A announce-string]\n", argv0); 63 fprint(2, " %s -B address\n", argv0); 64 fatal("usage"); 65 } 66 67 void 68 main(int argc, char **argv) 69 { 70 char buf[ERRMAX], ebuf[ERRMAX]; 71 Fsrpc *r; 72 int n, fd; 73 char *dbfile, *srv, *file, *na; 74 AuthInfo *ai; 75 ulong initial; 76 77 dbfile = "/tmp/exportdb"; 78 srv = nil; 79 srvfd = -1; 80 na = 0; 81 82 ai = nil; 83 ARGBEGIN{ 84 case 'a': 85 /* 86 * We use p9any so we don't have to visit this code again, with the 87 * cost that this code is incompatible with the old world, which 88 * requires p9sk2. (The two differ in who talks first, so compatibility 89 * is awkward.) 90 */ 91 ai = auth_proxy(0, auth_getkey, "proto=p9any role=server"); 92 if(ai == nil) 93 fatal("auth_proxy: %r"); 94 if(nonone && strcmp(ai->cuid, "none") == 0) 95 fatal("exportfs by none disallowed"); 96 if(auth_chuid(ai, nil) < 0) 97 fatal("auth_chuid: %r"); 98 if(newns(ai->cuid, 0) < 0) 99 fatal("newns"); 100 putenv("service", "exportfs"); 101 break; 102 103 case 'e': 104 ealgs = ARGF(); 105 if(ealgs == nil) 106 usage(); 107 if(*ealgs == 0 || strcmp(ealgs, "clear") == 0) 108 ealgs = nil; 109 break; 110 111 case 'S': 112 if(srvfd != -1) 113 usage(); 114 file = EARGF(usage()); 115 if((srvfd = open(file, ORDWR)) < 0) 116 sysfatal("open '%s': %r", file); 117 break; 118 119 case 'd': 120 dbg++; 121 break; 122 123 case 'f': 124 dbfile = EARGF(usage()); 125 break; 126 127 case 'F': 128 /* accepted but ignored, for backwards compatibility */ 129 break; 130 131 case 'm': 132 messagesize = strtoul(EARGF(usage()), nil, 0); 133 break; 134 135 case 'n': 136 nonone = 0; 137 break; 138 139 case 'r': 140 srv = EARGF(usage()); 141 break; 142 143 case 's': 144 srv = "/"; 145 break; 146 147 case 'P': 148 patternfile = EARGF(usage()); 149 break; 150 151 case 'A': 152 anstring = EARGF(usage()); 153 break; 154 155 case 'R': 156 readonly = 1; 157 break; 158 159 case 'B': 160 na = EARGF(usage()); 161 break; 162 163 default: 164 usage(); 165 }ARGEND 166 USED(argc, argv); 167 168 if(na){ 169 if(srv == nil) 170 sysfatal("-B requires -s"); 171 172 if((fd = dial(netmkaddr(na, 0, "importfs"), 0, 0, 0)) < 0) 173 sysfatal("can't dial %s: %r", na); 174 175 ai = auth_proxy(fd, auth_getkey, "proto=p9any role=client"); 176 if(ai == nil) 177 sysfatal("%r: %s", na); 178 179 dup(fd, 0); 180 dup(fd, 1); 181 close(fd); 182 } 183 184 exclusions(); 185 186 if(dbg) { 187 n = create(dbfile, OWRITE|OTRUNC, 0666); 188 dup(n, DFD); 189 close(n); 190 } 191 192 if(srvfd >= 0 && srv){ 193 fprint(2, "exportfs: -S cannot be used with -r or -s\n"); 194 usage(); 195 } 196 197 DEBUG(DFD, "exportfs: started\n"); 198 199 rfork(RFNOTEG); 200 201 if(messagesize == 0){ 202 messagesize = iounit(netfd); 203 if(messagesize == 0) 204 messagesize = 8192+IOHDRSZ; 205 } 206 207 Workq = emallocz(sizeof(Fsrpc)*Nr_workbufs); 208 // for(i=0; i<Nr_workbufs; i++) 209 // Workq[i].buf = emallocz(messagesize); 210 fhash = emallocz(sizeof(Fid*)*FHASHSIZE); 211 212 fmtinstall('F', fcallfmt); 213 214 /* 215 * Get tree to serve from network connection, 216 * check we can get there and ack the connection 217 */ 218 if(srvfd != -1) { 219 /* do nothing */ 220 } 221 else if(srv) { 222 chdir(srv); 223 DEBUG(DFD, "invoked as server for %s", srv); 224 strncpy(buf, srv, sizeof buf); 225 } 226 else { 227 buf[0] = 0; 228 n = read(0, buf, sizeof(buf)-1); 229 if(n < 0) { 230 errstr(buf, sizeof buf); 231 fprint(0, "read(0): %s", buf); 232 DEBUG(DFD, "read(0): %s", buf); 233 exits(buf); 234 } 235 buf[n] = 0; 236 if(chdir(buf) < 0) { 237 errstr(ebuf, sizeof ebuf); 238 fprint(0, "chdir(%d:\"%s\"): %s", n, buf, ebuf); 239 DEBUG(DFD, "chdir(%d:\"%s\"): %s", n, buf, ebuf); 240 exits(ebuf); 241 } 242 } 243 244 DEBUG(DFD, "\niniting root\n"); 245 initroot(); 246 247 DEBUG(DFD, "exportfs: %s\n", buf); 248 249 if(srv == nil && srvfd == -1 && write(0, "OK", 2) != 2) 250 fatal("open ack write"); 251 252 if (readn(netfd, &initial, sizeof(ulong)) < sizeof(ulong)) 253 fatal("can't read initial string: %r\n"); 254 255 if (!strncmp((char *)&initial, "impo", sizeof(ulong))) { 256 char buf[128], *p, *args[3]; 257 258 // New import. Read import's parameters... 259 initial = 0; 260 261 p = buf; 262 while (p - buf < sizeof buf) { 263 if ((n = read(netfd, p, 1)) < 0) 264 fatal("can't read impo arguments: %r\n"); 265 266 if (n == 0) 267 fatal("connection closed while reading arguments\n"); 268 269 if (*p == '\n') 270 *p = '\0'; 271 if (*p++ == '\0') 272 break; 273 } 274 275 if (tokenize(buf, args, nelem(args)) != 2) 276 fatal("impo arguments invalid: impo%s...\n", buf); 277 278 if (!strcmp(args[0], "aan")) 279 filterp = aanfilter; 280 else if (strcmp(args[0], "nofilter")) 281 fatal("import filter argument unsupported: %s\n", args[0]); 282 283 if (!strcmp(args[1], "ssl")) 284 encproto = Encssl; 285 else if (!strcmp(args[1], "tls")) 286 encproto = Enctls; 287 else if (strcmp(args[1], "clear")) 288 fatal("import encryption proto unsupported: %s\n", args[1]); 289 290 if (encproto == Enctls) 291 sysfatal("%s: tls has not yet been implemented\n", argv[0]); 292 } 293 294 if (encproto != Encnone && ealgs && ai) { 295 uchar key[16]; 296 uchar digest[SHA1dlen]; 297 char fromclientsecret[21]; 298 char fromserversecret[21]; 299 int i; 300 301 memmove(key+4, ai->secret, ai->nsecret); 302 303 /* exchange random numbers */ 304 srand(truerand()); 305 for(i = 0; i < 4; i++) 306 key[i+12] = rand(); 307 308 if (initial) 309 fatal("Protocol botch: old import\n"); 310 if(readn(netfd, key, 4) != 4) 311 fatal("can't read key part; %r\n"); 312 313 if(write(netfd, key+12, 4) != 4) 314 fatal("can't write key part; %r\n"); 315 316 /* scramble into two secrets */ 317 sha1(key, sizeof(key), digest, nil); 318 mksecret(fromclientsecret, digest); 319 mksecret(fromserversecret, digest+10); 320 321 if (filterp) 322 netfd = filter(netfd, filterp); 323 324 switch (encproto) { 325 case Encssl: 326 netfd = pushssl(netfd, ealgs, fromserversecret, 327 fromclientsecret, nil); 328 break; 329 case Enctls: 330 default: 331 fatal("Unsupported encryption protocol\n"); 332 } 333 334 if(netfd < 0) 335 fatal("can't establish ssl connection: %r"); 336 } 337 else if (filterp) { 338 if (initial) 339 fatal("Protocol botch: don't know how to deal with this\n"); 340 netfd = filter(netfd, filterp); 341 } 342 343 /* 344 * Start serving file requests from the network 345 */ 346 for(;;) { 347 r = getsbuf(); 348 if(r == 0) 349 fatal("Out of service buffers"); 350 351 n = localread9pmsg(netfd, r->buf, messagesize, &initial); 352 if(n <= 0) 353 fatal(nil); 354 355 if(convM2S(r->buf, n, &r->work) == 0) 356 fatal("convM2S format error"); 357 358 DEBUG(DFD, "%F\n", &r->work); 359 (fcalls[r->work.type])(r); 360 } 361 } 362 363 // WARNING: Replace this with the original version as soon as all 364 // _old_ imports have been replaced with negotiating imports. Also 365 // cpu relies on this (which needs to be fixed!) -- pb. 366 static int 367 localread9pmsg(int fd, void *abuf, uint n, ulong *initial) 368 { 369 int m, len; 370 uchar *buf; 371 372 buf = abuf; 373 374 /* read count */ 375 assert(BIT32SZ == sizeof(ulong)); 376 if (*initial) { 377 memcpy(buf, initial, BIT32SZ); 378 *initial = 0; 379 } 380 else { 381 m = readn(fd, buf, BIT32SZ); 382 if(m != BIT32SZ){ 383 if(m < 0) 384 return -1; 385 return 0; 386 } 387 } 388 389 len = GBIT32(buf); 390 if(len <= BIT32SZ || len > n){ 391 werrstr("bad length in 9P2000 message header"); 392 return -1; 393 } 394 len -= BIT32SZ; 395 m = readn(fd, buf+BIT32SZ, len); 396 if(m < len) 397 return 0; 398 return BIT32SZ+m; 399 } 400 void 401 reply(Fcall *r, Fcall *t, char *err) 402 { 403 uchar *data; 404 int n; 405 406 t->tag = r->tag; 407 t->fid = r->fid; 408 if(err) { 409 t->type = Rerror; 410 t->ename = err; 411 } 412 else 413 t->type = r->type + 1; 414 415 DEBUG(DFD, "\t%F\n", t); 416 417 data = malloc(messagesize); /* not mallocz; no need to clear */ 418 if(data == nil) 419 fatal(Enomem); 420 n = convS2M(t, data, messagesize); 421 if(write(netfd, data, n)!=n) 422 {syslog(0, "exportfs", "short write: %r"); 423 fatal("mount write"); 424 } 425 free(data); 426 } 427 428 Fid * 429 getfid(int nr) 430 { 431 Fid *f; 432 433 for(f = fidhash(nr); f; f = f->next) 434 if(f->nr == nr) 435 return f; 436 437 return 0; 438 } 439 440 int 441 freefid(int nr) 442 { 443 Fid *f, **l; 444 char buf[128]; 445 446 l = &fidhash(nr); 447 for(f = *l; f; f = f->next) { 448 if(f->nr == nr) { 449 if(f->mid) { 450 sprint(buf, "/mnt/exportfs/%d", f->mid); 451 unmount(0, buf); 452 psmap[f->mid] = 0; 453 } 454 if(f->f) { 455 freefile(f->f); 456 f->f = nil; 457 } 458 if(f->dir){ 459 free(f->dir); 460 f->dir = nil; 461 } 462 *l = f->next; 463 f->next = fidfree; 464 fidfree = f; 465 return 1; 466 } 467 l = &f->next; 468 } 469 470 return 0; 471 } 472 473 Fid * 474 newfid(int nr) 475 { 476 Fid *new, **l; 477 int i; 478 479 l = &fidhash(nr); 480 for(new = *l; new; new = new->next) 481 if(new->nr == nr) 482 return 0; 483 484 if(fidfree == 0) { 485 fidfree = emallocz(sizeof(Fid) * Fidchunk); 486 487 for(i = 0; i < Fidchunk-1; i++) 488 fidfree[i].next = &fidfree[i+1]; 489 490 fidfree[Fidchunk-1].next = 0; 491 } 492 493 new = fidfree; 494 fidfree = new->next; 495 496 memset(new, 0, sizeof(Fid)); 497 new->next = *l; 498 *l = new; 499 new->nr = nr; 500 new->fid = -1; 501 new->mid = 0; 502 503 return new; 504 } 505 506 Fsrpc * 507 getsbuf(void) 508 { 509 static int ap; 510 int look, rounds; 511 Fsrpc *wb; 512 int small_instead_of_fast = 1; 513 514 if(small_instead_of_fast) 515 ap = 0; /* so we always start looking at the beginning and reuse buffers */ 516 517 for(rounds = 0; rounds < 10; rounds++) { 518 for(look = 0; look < Nr_workbufs; look++) { 519 if(++ap == Nr_workbufs) 520 ap = 0; 521 if(Workq[ap].busy == 0) 522 break; 523 } 524 525 if(look == Nr_workbufs){ 526 sleep(10 * rounds); 527 continue; 528 } 529 530 wb = &Workq[ap]; 531 wb->pid = 0; 532 wb->canint = 0; 533 wb->flushtag = NOTAG; 534 wb->busy = 1; 535 if(wb->buf == nil) /* allocate buffers dynamically to keep size down */ 536 wb->buf = emallocz(messagesize); 537 return wb; 538 } 539 fatal("No more work buffers"); 540 return nil; 541 } 542 543 void 544 freefile(File *f) 545 { 546 File *parent, *child; 547 548 Loop: 549 f->ref--; 550 if(f->ref > 0) 551 return; 552 freecnt++; 553 if(f->ref < 0) abort(); 554 DEBUG(DFD, "free %s\n", f->name); 555 /* delete from parent */ 556 parent = f->parent; 557 if(parent->child == f) 558 parent->child = f->childlist; 559 else{ 560 for(child=parent->child; child->childlist!=f; child=child->childlist) 561 if(child->childlist == nil) 562 fatal("bad child list"); 563 child->childlist = f->childlist; 564 } 565 freeqid(f->qidt); 566 free(f->name); 567 f->name = nil; 568 free(f); 569 f = parent; 570 if(f != nil) 571 goto Loop; 572 } 573 574 File * 575 file(File *parent, char *name) 576 { 577 Dir *dir; 578 char *path; 579 File *f; 580 581 DEBUG(DFD, "\tfile: 0x%p %s name %s\n", parent, parent->name, name); 582 583 path = makepath(parent, name); 584 if(patternfile != nil && excludefile(path)){ 585 free(path); 586 return nil; 587 } 588 dir = dirstat(path); 589 free(path); 590 if(dir == nil) 591 return nil; 592 593 for(f = parent->child; f; f = f->childlist) 594 if(strcmp(name, f->name) == 0) 595 break; 596 597 if(f == nil){ 598 f = emallocz(sizeof(File)); 599 f->name = estrdup(name); 600 601 f->parent = parent; 602 f->childlist = parent->child; 603 parent->child = f; 604 parent->ref++; 605 f->ref = 0; 606 filecnt++; 607 } 608 f->ref++; 609 f->qid.type = dir->qid.type; 610 f->qid.vers = dir->qid.vers; 611 f->qidt = uniqueqid(dir); 612 f->qid.path = f->qidt->uniqpath; 613 614 f->inval = 0; 615 616 free(dir); 617 618 return f; 619 } 620 621 void 622 initroot(void) 623 { 624 Dir *dir; 625 626 root = emallocz(sizeof(File)); 627 root->name = estrdup("."); 628 629 dir = dirstat(root->name); 630 if(dir == nil) 631 fatal("root stat"); 632 633 root->ref = 1; 634 root->qid.vers = dir->qid.vers; 635 root->qidt = uniqueqid(dir); 636 root->qid.path = root->qidt->uniqpath; 637 root->qid.type = QTDIR; 638 free(dir); 639 640 psmpt = emallocz(sizeof(File)); 641 psmpt->name = estrdup("/"); 642 643 dir = dirstat(psmpt->name); 644 if(dir == nil) 645 return; 646 647 psmpt->ref = 1; 648 psmpt->qid.vers = dir->qid.vers; 649 psmpt->qidt = uniqueqid(dir); 650 psmpt->qid.path = psmpt->qidt->uniqpath; 651 free(dir); 652 653 psmpt = file(psmpt, "mnt"); 654 if(psmpt == 0) 655 return; 656 psmpt = file(psmpt, "exportfs"); 657 } 658 659 char* 660 makepath(File *p, char *name) 661 { 662 int i, n; 663 char *c, *s, *path, *seg[256]; 664 665 seg[0] = name; 666 n = strlen(name)+2; 667 for(i = 1; i < 256 && p; i++, p = p->parent){ 668 seg[i] = p->name; 669 n += strlen(p->name)+1; 670 } 671 path = malloc(n); 672 if(path == nil) 673 fatal("out of memory"); 674 s = path; 675 676 while(i--) { 677 for(c = seg[i]; *c; c++) 678 *s++ = *c; 679 *s++ = '/'; 680 } 681 while(s[-1] == '/') 682 s--; 683 *s = '\0'; 684 685 return path; 686 } 687 688 int 689 qidhash(vlong path) 690 { 691 int h, n; 692 693 h = 0; 694 for(n=0; n<64; n+=Nqidbits){ 695 h ^= path; 696 path >>= Nqidbits; 697 } 698 return h & (Nqidtab-1); 699 } 700 701 void 702 freeqid(Qidtab *q) 703 { 704 ulong h; 705 Qidtab *l; 706 707 q->ref--; 708 if(q->ref > 0) 709 return; 710 qfreecnt++; 711 h = qidhash(q->path); 712 if(qidtab[h] == q) 713 qidtab[h] = q->next; 714 else{ 715 for(l=qidtab[h]; l->next!=q; l=l->next) 716 if(l->next == nil) 717 fatal("bad qid list"); 718 l->next = q->next; 719 } 720 free(q); 721 } 722 723 Qidtab* 724 qidlookup(Dir *d) 725 { 726 ulong h; 727 Qidtab *q; 728 729 h = qidhash(d->qid.path); 730 for(q=qidtab[h]; q!=nil; q=q->next) 731 if(q->type==d->type && q->dev==d->dev && q->path==d->qid.path) 732 return q; 733 return nil; 734 } 735 736 int 737 qidexists(vlong path) 738 { 739 int h; 740 Qidtab *q; 741 742 for(h=0; h<Nqidtab; h++) 743 for(q=qidtab[h]; q!=nil; q=q->next) 744 if(q->uniqpath == path) 745 return 1; 746 return 0; 747 } 748 749 Qidtab* 750 uniqueqid(Dir *d) 751 { 752 ulong h; 753 vlong path; 754 Qidtab *q; 755 756 q = qidlookup(d); 757 if(q != nil){ 758 q->ref++; 759 return q; 760 } 761 path = d->qid.path; 762 while(qidexists(path)){ 763 DEBUG(DFD, "collision on %s\n", d->name); 764 /* collision: find a new one */ 765 ncollision++; 766 path &= QIDPATH; 767 ++newqid; 768 if(newqid >= (1<<16)){ 769 DEBUG(DFD, "collision wraparound\n"); 770 newqid = 1; 771 } 772 path |= newqid<<48; 773 DEBUG(DFD, "assign qid %.16llux\n", path); 774 } 775 q = mallocz(sizeof(Qidtab), 1); 776 if(q == nil) 777 fatal("no memory for qid table"); 778 qidcnt++; 779 q->ref = 1; 780 q->type = d->type; 781 q->dev = d->dev; 782 q->path = d->qid.path; 783 q->uniqpath = path; 784 h = qidhash(d->qid.path); 785 q->next = qidtab[h]; 786 qidtab[h] = q; 787 return q; 788 } 789 790 void 791 fatal(char *s, ...) 792 { 793 char buf[ERRMAX]; 794 va_list arg; 795 Proc *m; 796 797 if (s) { 798 va_start(arg, s); 799 vsnprint(buf, ERRMAX, s, arg); 800 va_end(arg); 801 } 802 803 /* Clear away the slave children */ 804 for(m = Proclist; m; m = m->next) 805 postnote(PNPROC, m->pid, "kill"); 806 807 DEBUG(DFD, "%s\n", buf); 808 if (s) 809 sysfatal(buf); 810 else 811 exits(nil); 812 } 813 814 void* 815 emallocz(uint n) 816 { 817 void *p; 818 819 p = mallocz(n, 1); 820 if(p == nil) 821 fatal(Enomem); 822 return p; 823 } 824 825 char* 826 estrdup(char *s) 827 { 828 char *t; 829 830 t = strdup(s); 831 if(t == nil) 832 fatal(Enomem); 833 return t; 834 } 835 836 /* Network on fd1, mount driver on fd0 */ 837 int 838 filter(int fd, char *cmd) 839 { 840 int p[2], lfd, len, nb, argc; 841 char newport[128], buf[128], devdir[40], *s, *file, *argv[16]; 842 843 // Get a free port and post it to the client. 844 if (announce(anstring, devdir) < 0) 845 sysfatal("filter: Cannot announce %s: %r\n", anstring); 846 847 snprint(buf, sizeof(buf), "%s/local", devdir); 848 buf[sizeof buf - 1] = '\0'; 849 if ((lfd = open(buf, OREAD)) < 0) 850 sysfatal("filter: Cannot open %s: %r\n", buf); 851 if ((len = read(lfd, newport, sizeof newport - 1)) < 0) 852 sysfatal("filter: Cannot read %s: %r\n", buf); 853 close(lfd); 854 newport[len] = '\0'; 855 856 if ((s = strchr(newport, '\n')) != nil) 857 *s = '\0'; 858 859 if ((nb = write(fd, newport, len)) < 0) 860 sysfatal("getport; cannot write port; %r"); 861 assert(nb == len); 862 863 argc = tokenize(cmd, argv, nelem(argv)-2); 864 if (argc == 0) 865 sysfatal("filter: empty command"); 866 argv[argc++] = buf; 867 argv[argc] = nil; 868 file = argv[0]; 869 if (s = strrchr(argv[0], '/')) 870 argv[0] = s+1; 871 872 if(pipe(p) < 0) 873 fatal("pipe"); 874 875 switch(rfork(RFNOWAIT|RFPROC|RFFDG)) { 876 case -1: 877 fatal("rfork record module"); 878 case 0: 879 if (dup(p[0], 1) < 0) 880 fatal("filter: Cannot dup to 1; %r\n"); 881 if (dup(p[0], 0) < 0) 882 fatal("filter: Cannot dup to 0; %r\n"); 883 close(p[0]); 884 close(p[1]); 885 exec(file, argv); 886 fatal("exec record module"); 887 default: 888 close(fd); 889 close(p[0]); 890 } 891 return p[1]; 892 } 893 894 static void 895 mksecret(char *t, uchar *f) 896 { 897 sprint(t, "%2.2ux%2.2ux%2.2ux%2.2ux%2.2ux%2.2ux%2.2ux%2.2ux%2.2ux%2.2ux", 898 f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9]); 899 } 900