#include "stdinc.h" #include "9h.h" enum { OMODE = 0x7, /* Topen/Tcreate mode */ }; enum { PermX = 1, PermW = 2, PermR = 4, }; static char EPermission[] = "permission denied"; static int permFile(File* file, Fid* fid, int perm) { char *u; DirEntry de; if(!fileGetDir(file, &de)) return -1; /* * User none only gets other permissions. */ if(strcmp(fid->uname, unamenone) != 0){ /* * There is only one uid<->uname mapping * and it's already cached in the Fid, but * it might have changed during the lifetime * if this Fid. */ if((u = unameByUid(de.uid)) != nil){ if(strcmp(fid->uname, u) == 0 && ((perm<<6) & de.mode)){ vtMemFree(u); deCleanup(&de); return 1; } vtMemFree(u); } if(groupMember(de.gid, fid->uname) && ((perm<<3) & de.mode)){ deCleanup(&de); return 1; } } if(perm & de.mode){ if(perm == PermX && (de.mode & ModeDir)){ deCleanup(&de); return 1; } if(!groupMember(uidnoworld, fid->uname)){ deCleanup(&de); return 1; } } if(fsysNoPermCheck(fid->fsys) || (fid->con->flags&ConNoPermCheck)){ deCleanup(&de); return 1; } vtSetError(EPermission); deCleanup(&de); return 0; } static int permFid(Fid* fid, int p) { return permFile(fid->file, fid, p); } static int permParent(Fid* fid, int p) { int r; File *parent; parent = fileGetParent(fid->file); r = permFile(parent, fid, p); fileDecRef(parent); return r; } int validFileName(char* name) { char *p; if(name == nil || name[0] == '\0'){ vtSetError("no file name"); return 0; } if(name[0] == '.'){ if(name[1] == '\0' || (name[1] == '.' && name[2] == '\0')){ vtSetError(". and .. illegal as file name"); return 0; } } for(p = name; *p != '\0'; p++){ if((*p & 0xFF) <= 040){ vtSetError("bad character in file name"); return 0; } } return 1; } static int rTwstat(Msg* m) { Dir dir; Fid *fid; ulong mode, oldmode; DirEntry de; char *gid, *strs, *uid; int gl, op, retval, tsync, wstatallow; if((fid = fidGet(m->con, m->t.fid, FidFWlock)) == nil) return 0; gid = uid = nil; retval = 0; if(strcmp(fid->uname, unamenone) == 0 || (fid->qid.type & QTAUTH)){ vtSetError(EPermission); goto error0; } if(fileIsRoFs(fid->file) || !groupWriteMember(fid->uname)){ vtSetError("read-only filesystem"); goto error0; } if(!fileGetDir(fid->file, &de)) goto error0; strs = vtMemAlloc(m->t.nstat); if(convM2D(m->t.stat, m->t.nstat, &dir, strs) == 0){ vtSetError("wstat -- protocol botch"); goto error; } /* * Run through each of the (sub-)fields in the provided Dir * checking for validity and whether it's a default: * .type, .dev and .atime are completely ignored and not checked; * .qid.path, .qid.vers and .muid are checked for validity but * any attempt to change them is an error. * .qid.type/.mode, .mtime, .name, .length, .uid and .gid can * possibly be changed. * * 'Op' flags there are changed fields, i.e. it's not a no-op. * 'Tsync' flags all fields are defaulted. */ tsync = 1; if(dir.qid.path != ~0){ if(dir.qid.path != de.qid){ vtSetError("wstat -- attempt to change qid.path"); goto error; } tsync = 0; } if(dir.qid.vers != ~0){ if(dir.qid.vers != de.mcount){ vtSetError("wstat -- attempt to change qid.vers"); goto error; } tsync = 0; } if(dir.muid != nil && *dir.muid != '\0'){ if((uid = uidByUname(dir.muid)) == nil){ vtSetError("wstat -- unknown muid"); goto error; } if(strcmp(uid, de.mid) != 0){ vtSetError("wstat -- attempt to change muid"); goto error; } vtMemFree(uid); uid = nil; tsync = 0; } /* * Check .qid.type and .mode agree if neither is defaulted. */ if(dir.qid.type != (uchar)~0 && dir.mode != ~0){ if(dir.qid.type != ((dir.mode>>24) & 0xFF)){ vtSetError("wstat -- qid.type/mode mismatch"); goto error; } } op = 0; oldmode = de.mode; if(dir.qid.type != (uchar)~0 || dir.mode != ~0){ /* * .qid.type or .mode isn't defaulted, check for unknown bits. */ if(dir.mode == ~0) dir.mode = (dir.qid.type<<24)|(de.mode & 0777); if(dir.mode & ~(DMDIR|DMAPPEND|DMEXCL|DMTMP|0777)){ vtSetError("wstat -- unknown bits in qid.type/mode"); goto error; } /* * Synthesise a mode to check against the current settings. */ mode = dir.mode & 0777; if(dir.mode & DMEXCL) mode |= ModeExclusive; if(dir.mode & DMAPPEND) mode |= ModeAppend; if(dir.mode & DMDIR) mode |= ModeDir; if(dir.mode & DMTMP) mode |= ModeTemporary; if((de.mode^mode) & ModeDir){ vtSetError("wstat -- attempt to change directory bit"); goto error; } if((de.mode & (ModeAppend|ModeExclusive|ModeTemporary|0777)) != mode){ de.mode &= ~(ModeAppend|ModeExclusive|ModeTemporary|0777); de.mode |= mode; op = 1; } tsync = 0; } if(dir.mtime != ~0){ if(dir.mtime != de.mtime){ de.mtime = dir.mtime; op = 1; } tsync = 0; } if(dir.length != ~0){ if(dir.length != de.size){ /* * Cannot change length on append-only files. * If we're changing the append bit, it's okay. */ if(de.mode & oldmode & ModeAppend){ vtSetError("wstat -- attempt to change length of append-only file"); goto error; } if(de.mode & ModeDir){ vtSetError("wstat -- attempt to change length of directory"); goto error; } de.size = dir.length; op = 1; } tsync = 0; } /* * Check for permission to change .mode, .mtime or .length, * must be owner or leader of either group, for which test gid * is needed; permission checks on gid will be done later. */ if(dir.gid != nil && *dir.gid != '\0'){ if((gid = uidByUname(dir.gid)) == nil){ vtSetError("wstat -- unknown gid"); goto error; } tsync = 0; } else gid = vtStrDup(de.gid); wstatallow = (fsysWstatAllow(fid->fsys) || (m->con->flags&ConWstatAllow)); /* * 'Gl' counts whether neither, one or both groups are led. */ gl = groupLeader(gid, fid->uname) != 0; gl += groupLeader(de.gid, fid->uname) != 0; if(op && !wstatallow){ if(strcmp(fid->uid, de.uid) != 0 && !gl){ vtSetError("wstat -- not owner or group leader"); goto error; } } /* * Check for permission to change group, must be * either owner and in new group or leader of both groups. * If gid is nil here then */ if(strcmp(gid, de.gid) != 0){ if(!wstatallow && !(strcmp(fid->uid, de.uid) == 0 && groupMember(gid, fid->uname)) && !(gl == 2)){ vtSetError("wstat -- not owner and not group leaders"); goto error; } vtMemFree(de.gid); de.gid = gid; gid = nil; op = 1; tsync = 0; } /* * Rename. * Check .name is valid and different to the current. * If so, check write permission in parent. */ if(dir.name != nil && *dir.name != '\0'){ if(!validFileName(dir.name)) goto error; if(strcmp(dir.name, de.elem) != 0){ if(permParent(fid, PermW) <= 0) goto error; vtMemFree(de.elem); de.elem = vtStrDup(dir.name); op = 1; } tsync = 0; } /* * Check for permission to change owner - must be god. */ if(dir.uid != nil && *dir.uid != '\0'){ if((uid = uidByUname(dir.uid)) == nil){ vtSetError("wstat -- unknown uid"); goto error; } if(strcmp(uid, de.uid) != 0){ if(!wstatallow){ vtSetError("wstat -- not owner"); goto error; } if(strcmp(uid, uidnoworld) == 0){ vtSetError(EPermission); goto error; } vtMemFree(de.uid); de.uid = uid; uid = nil; op = 1; } tsync = 0; } if(op) retval = fileSetDir(fid->file, &de, fid->uid); else retval = 1; if(tsync){ /* * All values were defaulted, * make the state of the file exactly what it * claims to be before returning... */ USED(tsync); } error: deCleanup(&de); vtMemFree(strs); if(gid != nil) vtMemFree(gid); if(uid != nil) vtMemFree(uid); error0: fidPut(fid); return retval; }; static int rTstat(Msg* m) { Dir dir; Fid *fid; DirEntry de; if((fid = fidGet(m->con, m->t.fid, 0)) == nil) return 0; if(fid->qid.type & QTAUTH){ memset(&dir, 0, sizeof(Dir)); dir.qid = fid->qid; dir.mode = DMAUTH; dir.atime = time(0L); dir.mtime = dir.atime; dir.length = 0; dir.name = "#¿"; dir.uid = fid->uname; dir.gid = fid->uname; dir.muid = fid->uname; if((m->r.nstat = convD2M(&dir, m->data, m->con->msize)) == 0){ vtSetError("stat QTAUTH botch"); fidPut(fid); return 0; } m->r.stat = m->data; fidPut(fid); return 1; } if(!fileGetDir(fid->file, &de)){ fidPut(fid); return 0; } fidPut(fid); /* * TODO: optimise this copy (in convS2M) away somehow. * This pettifoggery with m->data will do for the moment. */ m->r.nstat = dirDe2M(&de, m->data, m->con->msize); m->r.stat = m->data; deCleanup(&de); return 1; } static int _rTclunk(Fid* fid, int remove) { int rok; if(fid->excl) exclFree(fid); rok = 1; if(remove && !(fid->qid.type & QTAUTH)){ if((rok = permParent(fid, PermW)) > 0) rok = fileRemove(fid->file, fid->uid); } fidClunk(fid); return rok; } static int rTremove(Msg* m) { Fid *fid; if((fid = fidGet(m->con, m->t.fid, FidFWlock)) == nil) return 0; return _rTclunk(fid, 1); } static int rTclunk(Msg* m) { Fid *fid; if((fid = fidGet(m->con, m->t.fid, FidFWlock)) == nil) return 0; _rTclunk(fid, (fid->open & FidORclose)); return 1; } static int rTwrite(Msg* m) { Fid *fid; int count, n; if((fid = fidGet(m->con, m->t.fid, 0)) == nil) return 0; if(!(fid->open & FidOWrite)){ vtSetError("fid not open for write"); goto error; } count = m->t.count; if(count < 0 || count > m->con->msize-IOHDRSZ){ vtSetError("write count too big"); goto error; } if(m->t.offset < 0){ vtSetError("write offset negative"); goto error; } if(fid->excl != nil && !exclUpdate(fid)) goto error; if(fid->qid.type & QTDIR){ vtSetError("is a directory"); goto error; } else if(fid->qid.type & QTAUTH) n = authWrite(fid, m->t.data, count); else n = fileWrite(fid->file, m->t.data, count, m->t.offset, fid->uid); if(n < 0) goto error; m->r.count = n; fidPut(fid); return 1; error: fidPut(fid); return 0; } static int rTread(Msg* m) { Fid *fid; uchar *data; int count, n; if((fid = fidGet(m->con, m->t.fid, 0)) == nil) return 0; if(!(fid->open & FidORead)){ vtSetError("fid not open for read"); goto error; } count = m->t.count; if(count < 0 || count > m->con->msize-IOHDRSZ){ vtSetError("read count too big"); goto error; } if(m->t.offset < 0){ vtSetError("read offset negative"); goto error; } if(fid->excl != nil && !exclUpdate(fid)) goto error; /* * TODO: optimise this copy (in convS2M) away somehow. * This pettifoggery with m->data will do for the moment. */ data = m->data+IOHDRSZ; if(fid->qid.type & QTDIR) n = dirRead(fid, data, count, m->t.offset); else if(fid->qid.type & QTAUTH) n = authRead(fid, data, count); else n = fileRead(fid->file, data, count, m->t.offset); if(n < 0) goto error; m->r.count = n; m->r.data = (char*)data; fidPut(fid); return 1; error: fidPut(fid); return 0; } static int rTcreate(Msg* m) { Fid *fid; File *file; ulong mode; int omode, open, perm; if((fid = fidGet(m->con, m->t.fid, FidFWlock)) == nil) return 0; if(fid->open){ vtSetError("fid open for I/O"); goto error; } if(fileIsRoFs(fid->file) || !groupWriteMember(fid->uname)){ vtSetError("read-only filesystem"); goto error; } if(!fileIsDir(fid->file)){ vtSetError("not a directory"); goto error; } if(permFid(fid, PermW) <= 0) goto error; if(!validFileName(m->t.name)) goto error; if(strcmp(fid->uid, uidnoworld) == 0){ vtSetError(EPermission); goto error; } omode = m->t.mode & OMODE; open = 0; if(omode == OREAD || omode == ORDWR || omode == OEXEC) open |= FidORead; if(omode == OWRITE || omode == ORDWR) open |= FidOWrite; if((open & (FidOWrite|FidORead)) == 0){ vtSetError("unknown mode"); goto error; } if(m->t.perm & DMDIR){ if((m->t.mode & (ORCLOSE|OTRUNC)) || (open & FidOWrite)){ vtSetError("illegal mode"); goto error; } if(m->t.perm & DMAPPEND){ vtSetError("illegal perm"); goto error; } } mode = fileGetMode(fid->file); perm = m->t.perm; if(m->t.perm & DMDIR) perm &= ~0777|(mode & 0777); else perm &= ~0666|(mode & 0666); mode = perm & 0777; if(m->t.perm & DMDIR) mode |= ModeDir; if(m->t.perm & DMAPPEND) mode |= ModeAppend; if(m->t.perm & DMEXCL) mode |= ModeExclusive; if(m->t.perm & DMTMP) mode |= ModeTemporary; if((file = fileCreate(fid->file, m->t.name, mode, fid->uid)) == nil){ fidPut(fid); return 0; } fileDecRef(fid->file); fid->qid.vers = fileGetMcount(file); fid->qid.path = fileGetId(file); fid->file = file; mode = fileGetMode(fid->file); if(mode & ModeDir) fid->qid.type = QTDIR; else fid->qid.type = QTFILE; if(mode & ModeAppend) fid->qid.type |= QTAPPEND; if(mode & ModeExclusive){ fid->qid.type |= QTEXCL; assert(exclAlloc(fid) != 0); } if(m->t.mode & ORCLOSE) open |= FidORclose; fid->open = open; m->r.qid = fid->qid; m->r.iounit = m->con->msize-IOHDRSZ; fidPut(fid); return 1; error: fidPut(fid); return 0; } static int rTopen(Msg* m) { Fid *fid; int isdir, mode, omode, open, rofs; if((fid = fidGet(m->con, m->t.fid, FidFWlock)) == nil) return 0; if(fid->open){ vtSetError("fid open for I/O"); goto error; } isdir = fileIsDir(fid->file); open = 0; rofs = fileIsRoFs(fid->file) || !groupWriteMember(fid->uname); if(m->t.mode & ORCLOSE){ if(isdir){ vtSetError("is a directory"); goto error; } if(rofs){ vtSetError("read-only filesystem"); goto error; } if(permParent(fid, PermW) <= 0) goto error; open |= FidORclose; } omode = m->t.mode & OMODE; if(omode == OREAD || omode == ORDWR){ if(permFid(fid, PermR) <= 0) goto error; open |= FidORead; } if(omode == OWRITE || omode == ORDWR || (m->t.mode & OTRUNC)){ if(isdir){ vtSetError("is a directory"); goto error; } if(rofs){ vtSetError("read-only filesystem"); goto error; } if(permFid(fid, PermW) <= 0) goto error; open |= FidOWrite; } if(omode == OEXEC){ if(isdir){ vtSetError("is a directory"); goto error; } if(permFid(fid, PermX) <= 0) goto error; open |= FidORead; } if((open & (FidOWrite|FidORead)) == 0){ vtSetError("unknown mode"); goto error; } mode = fileGetMode(fid->file); if((mode & ModeExclusive) && exclAlloc(fid) == 0) goto error; /* * Everything checks out, try to commit any changes. */ if((m->t.mode & OTRUNC) && !(mode & ModeAppend)) if(!fileTruncate(fid->file, fid->uid)) goto error; if(isdir && fid->db != nil){ dirBufFree(fid->db); fid->db = nil; } fid->qid.vers = fileGetMcount(fid->file); m->r.qid = fid->qid; m->r.iounit = m->con->msize-IOHDRSZ; fid->open = open; fidPut(fid); return 1; error: if(fid->excl != nil) exclFree(fid); fidPut(fid); return 0; } static int rTwalk(Msg* m) { Qid qid; Fcall *r, *t; int nwname, wlock; File *file, *nfile; Fid *fid, *ofid, *nfid; t = &m->t; if(t->fid == t->newfid) wlock = FidFWlock; else wlock = 0; /* * The file identified by t->fid must be valid in the * current session and must not have been opened for I/O * by an open or create message. */ if((ofid = fidGet(m->con, t->fid, wlock)) == nil) return 0; if(ofid->open){ vtSetError("file open for I/O"); fidPut(ofid); return 0; } /* * If newfid is not the same as fid, allocate a new file; * a side effect is checking newfid is not already in use (error); * if there are no names to walk this will be equivalent to a * simple 'clone' operation. * It's a no-op if newfid is the same as fid and t->nwname is 0. */ nfid = nil; if(t->fid != t->newfid){ nfid = fidGet(m->con, t->newfid, FidFWlock|FidFCreate); if(nfid == nil){ vtSetError("%s: walk: newfid 0x%ud in use", argv0, t->newfid); fidPut(ofid); return 0; } nfid->open = ofid->open & ~FidORclose; nfid->file = fileIncRef(ofid->file); nfid->qid = ofid->qid; nfid->uid = vtStrDup(ofid->uid); nfid->uname = vtStrDup(ofid->uname); nfid->fsys = fsysIncRef(ofid->fsys); fid = nfid; } else fid = ofid; r = &m->r; r->nwqid = 0; if(t->nwname == 0){ if(nfid != nil) fidPut(nfid); fidPut(ofid); return 1; } file = fid->file; fileIncRef(file); qid = fid->qid; for(nwname = 0; nwname < t->nwname; nwname++){ /* * Walked elements must represent a directory and * the implied user must have permission to search * the directory. Walking .. is always allowed, so that * you can't walk into a directory and then not be able * to walk out of it. */ if(!(qid.type & QTDIR)){ vtSetError("not a directory"); break; } switch(permFile(file, fid, PermX)){ case 1: break; case 0: if(strcmp(t->wname[nwname], "..") == 0) break; case -1: goto Out; } if((nfile = fileWalk(file, t->wname[nwname])) == nil) break; fileDecRef(file); file = nfile; qid.type = QTFILE; if(fileIsDir(file)) qid.type = QTDIR; qid.vers = fileGetMcount(file); qid.path = fileGetId(file); r->wqid[r->nwqid++] = qid; } if(nwname == t->nwname){ /* * Walked all elements. Update the target fid * from the temporary qid used during the walk, * and tidy up. */ fid->qid = r->wqid[r->nwqid-1]; fileDecRef(fid->file); fid->file = file; if(nfid != nil) fidPut(nfid); fidPut(ofid); return 1; } Out: /* * Didn't walk all elements, 'clunk' nfid if it exists * and leave fid untouched. * It's not an error if some of the elements were walked OK. */ fileDecRef(file); if(nfid != nil) fidClunk(nfid); fidPut(ofid); if(nwname == 0) return 0; return 1; } static int rTflush(Msg* m) { if(m->t.oldtag != NOTAG) msgFlush(m); return 1; } static void parseAname(char *aname, char **fsname, char **path) { char *s; if(aname && aname[0]) s = vtStrDup(aname); else s = vtStrDup("main/active"); *fsname = s; if((*path = strchr(s, '/')) != nil) *(*path)++ = '\0'; else *path = ""; } /* * Check remote IP address against /mnt/ipok. * Sources.cs.bell-labs.com uses this to disallow * network connections from Sudan, Libya, etc., * following U.S. cryptography export regulations. */ static int conIPCheck(Con* con) { char ok[256], *p; int fd; if(con->flags&ConIPCheck){ if(con->remote[0] == 0){ vtSetError("cannot verify unknown remote address"); return 0; } if(access("/mnt/ipok/ok", AEXIST) < 0){ /* mount closes the fd on success */ if((fd = open("/srv/ipok", ORDWR)) >= 0 && mount(fd, -1, "/mnt/ipok", MREPL, "") < 0) close(fd); if(access("/mnt/ipok/ok", AEXIST) < 0){ vtSetError("cannot verify remote address"); return 0; } } snprint(ok, sizeof ok, "/mnt/ipok/ok/%s", con->remote); if((p = strchr(ok, '!')) != nil) *p = 0; if(access(ok, AEXIST) < 0){ vtSetError("restricted remote address"); return 0; } } return 1; } static int rTattach(Msg* m) { Fid *fid; Fsys *fsys; char *fsname, *path; if((fid = fidGet(m->con, m->t.fid, FidFWlock|FidFCreate)) == nil) return 0; parseAname(m->t.aname, &fsname, &path); if((fsys = fsysGet(fsname)) == nil){ fidClunk(fid); vtMemFree(fsname); return 0; } fid->fsys = fsys; if(m->t.uname[0] != '\0') fid->uname = vtStrDup(m->t.uname); else fid->uname = vtStrDup(unamenone); if((fid->con->flags&ConIPCheck) && !conIPCheck(fid->con)){ consPrint("reject %s from %s: %R\n", fid->uname, fid->con->remote); fidClunk(fid); vtMemFree(fsname); return 0; } if(fsysNoAuthCheck(fsys) || (m->con->flags&ConNoAuthCheck)){ if((fid->uid = uidByUname(fid->uname)) == nil) fid->uid = vtStrDup(unamenone); } else if(!authCheck(&m->t, fid, fsys)){ fidClunk(fid); vtMemFree(fsname); return 0; } fsysFsRlock(fsys); if((fid->file = fsysGetRoot(fsys, path)) == nil){ fsysFsRUnlock(fsys); fidClunk(fid); vtMemFree(fsname); return 0; } fsysFsRUnlock(fsys); vtMemFree(fsname); fid->qid = (Qid){fileGetId(fid->file), 0, QTDIR}; m->r.qid = fid->qid; fidPut(fid); return 1; } static int rTauth(Msg* m) { int afd; Con *con; Fid *afid; Fsys *fsys; char *fsname, *path; parseAname(m->t.aname, &fsname, &path); if((fsys = fsysGet(fsname)) == nil){ vtMemFree(fsname); return 0; } vtMemFree(fsname); if(fsysNoAuthCheck(fsys) || (m->con->flags&ConNoAuthCheck)){ m->con->aok = 1; vtSetError("authentication disabled"); fsysPut(fsys); return 0; } if(strcmp(m->t.uname, unamenone) == 0){ vtSetError("user 'none' requires no authentication"); fsysPut(fsys); return 0; } con = m->con; if((afid = fidGet(con, m->t.afid, FidFWlock|FidFCreate)) == nil){ fsysPut(fsys); return 0; } afid->fsys = fsys; if((afd = open("/mnt/factotum/rpc", ORDWR)) < 0){ vtSetError("can't open \"/mnt/factotum/rpc\""); fidClunk(afid); return 0; } if((afid->rpc = auth_allocrpc(afd)) == nil){ close(afd); vtSetError("can't auth_allocrpc"); fidClunk(afid); return 0; } if(auth_rpc(afid->rpc, "start", "proto=p9any role=server", 23) != ARok){ vtSetError("can't auth_rpc"); fidClunk(afid); return 0; } afid->open = FidOWrite|FidORead; afid->qid.type = QTAUTH; afid->qid.path = m->t.afid; afid->uname = vtStrDup(m->t.uname); m->r.qid = afid->qid; fidPut(afid); return 1; } static int rTversion(Msg* m) { int v; Con *con; Fcall *r, *t; t = &m->t; r = &m->r; con = m->con; vtLock(con->lock); if(con->state != ConInit){ vtUnlock(con->lock); vtSetError("Tversion: down"); return 0; } con->state = ConNew; /* * Release the karma of past lives and suffering. * Should this be done before or after checking the * validity of the Tversion? */ fidClunkAll(con); if(t->tag != NOTAG){ vtUnlock(con->lock); vtSetError("Tversion: invalid tag"); return 0; } if(t->msize < 256){ vtUnlock(con->lock); vtSetError("Tversion: message size too small"); return 0; } if(t->msize < con->msize) r->msize = t->msize; else r->msize = con->msize; r->version = "unknown"; if(t->version[0] == '9' && t->version[1] == 'P'){ /* * Currently, the only defined version * is "9P2000"; ignore any later versions. */ v = strtol(&t->version[2], 0, 10); if(v >= 2000){ r->version = VERSION9P; con->msize = r->msize; con->state = ConUp; } else if(strcmp(t->version, "9PEoF") == 0){ r->version = "9PEoF"; con->msize = r->msize; con->state = ConMoribund; /* * Don't want to attempt to write this * message as the connection may be already * closed. */ m->state = MsgF; } } vtUnlock(con->lock); return 1; } int (*rFcall[Tmax])(Msg*) = { [Tversion] = rTversion, [Tauth] = rTauth, [Tattach] = rTattach, [Tflush] = rTflush, [Twalk] = rTwalk, [Topen] = rTopen, [Tcreate] = rTcreate, [Tread] = rTread, [Twrite] = rTwrite, [Tclunk] = rTclunk, [Tremove] = rTremove, [Tstat] = rTstat, [Twstat] = rTwstat, };