From 05e0aba1557c607724378e2e70acffd0e54947e5 Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Sun, 2 Aug 2026 16:08:23 +0200 Subject: [PATCH] Server owns the fid map: validate newfids, GC stale fids MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - attach: reject if fc.Fid already in use (was silently overwriting) - walk: reject if fc.Newfid already in use (both clone and full walk) - open: validate fid path exists; delete stale fid if path is gone - stat: validate fid path exists; delete stale fid if path is gone - wstat: after rename, GC all fids on this connection that reference the old path tree (was previously updating paths — now just deletes) The server fully owns the fid map. The client proposes fid numbers, the server validates and accepts or rejects. Stale fids (pointing to renamed/deleted paths) are garbage collected when detected. --- cmd/olliesrv/server.go | 47 ++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/cmd/olliesrv/server.go b/cmd/olliesrv/server.go index bdb88a4..39634aa 100644 --- a/cmd/olliesrv/server.go +++ b/cmd/olliesrv/server.go @@ -131,7 +131,7 @@ func New(cfg Config) *Server { plan9.Tread: func(cs *connState, fc *plan9.Fcall, ctx context.Context) *plan9.Fcall { return read(cfg.RootTree, cs, fc, ctx) }, plan9.Twrite: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return write(cfg.RootTree, cs, fc) }, plan9.Tstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return stat(cfg.RootTree, cs, fc) }, - plan9.Twstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return s.wstat(cs, fc) }, + plan9.Twstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return wstat(cfg.RootTree, cs, fc) }, plan9.Tclunk: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return clunk(cfg.RootTree, cs, fc) }, plan9.Tremove: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return remove(cfg.RootTree, cs, fc) }, } @@ -335,6 +335,9 @@ func pathBase(path string) string { func (s *Server) attach(cs *connState, fc *plan9.Fcall) *plan9.Fcall { cs.mu.Lock() defer cs.mu.Unlock() + if _, exists := cs.fids[fc.Fid]; exists { + return errFcall(fc, "fid already in use") + } cs.uname = fc.Uname qid := plan9.Qid{Type: QTDir, Path: 0} cs.fids[fc.Fid] = &fid{path: "/", qid: qid} @@ -355,6 +358,9 @@ func walk(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall { } newf := &fid{path: f.path, qid: f.qid} if len(fc.Wname) == 0 { + if _, exists := cs.fids[fc.Newfid]; exists { + return errFcall(fc, "fid already in use") + } cs.fids[fc.Newfid] = newf return &plan9.Fcall{Type: plan9.Rwalk, Tag: fc.Tag, Wqid: []plan9.Qid{}} } @@ -383,6 +389,9 @@ func walk(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall { cur = next } if len(wqids) == len(fc.Wname) { + if _, exists := cs.fids[fc.Newfid]; exists { + return errFcall(fc, "fid already in use") + } newf.path = cur newf.qid = wqids[len(wqids)-1] cs.fids[fc.Newfid] = newf @@ -397,6 +406,10 @@ func (s *Server) open(cs *connState, fc *plan9.Fcall) *plan9.Fcall { if !ok { return errFcall(fc, "bad fid") } + if _, err := s.rootTree.Stat(f.path[1:]); err != nil { + delete(cs.fids, fc.Fid) + return errFcall(fc, "file not found") + } uid, gid := fileOwnerGroup(s.rootTree, f.path) perm := uint32(makeStat(s.rootTree, f.path).Mode) & 0777 @@ -581,6 +594,12 @@ func stat(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall { if !ok { return errFcall(fc, "bad fid") } + if _, err := root.Stat(f.path[1:]); err != nil { + cs.mu.Lock() + delete(cs.fids, fc.Fid) + cs.mu.Unlock() + return errFcall(fc, "file not found") + } dir := makeStat(root, f.path) stat, err := dir.Bytes() if err != nil { @@ -589,7 +608,7 @@ func stat(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall { return &plan9.Fcall{Type: plan9.Rstat, Tag: fc.Tag, Stat: stat} } -func (s *Server) wstat(cs *connState, fc *plan9.Fcall) *plan9.Fcall { +func wstat(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall { cs.mu.Lock() f, ok := cs.fids[fc.Fid] cs.mu.Unlock() @@ -604,26 +623,18 @@ func (s *Server) wstat(cs *connState, fc *plan9.Fcall) *plan9.Fcall { if newDir.Name == "" || newDir.Name == oldName { return &plan9.Fcall{Type: plan9.Rwstat, Tag: fc.Tag} } - if err := s.rootTree.Rename(f.path[1:], newDir.Name); err != nil { + if err := root.Rename(f.path[1:], newDir.Name); err != nil { return errFcall(fc, err.Error()) } - - relPath := f.path[1:] - oldPath := f.path - newPath := "/" + relPath[:len(relPath)-len(oldName)] + newDir.Name - s.mu.RLock() - conns := s.conns - s.mu.RUnlock() - for _, c := range conns { - c.mu.Lock() - for _, fid := range c.fids { - if fid.path == oldPath || strings.HasPrefix(fid.path, oldPath+"/") { - fid.path = newPath + fid.path[len(oldPath):] - fid.qid.Path = qidPath(fid.path) - } + // GC stale fids on this connection referencing the old path + oldPrefix := f.path + cs.mu.Lock() + for id, fid := range cs.fids { + if fid.path == oldPrefix || strings.HasPrefix(fid.path, oldPrefix+"/") { + delete(cs.fids, id) } - c.mu.Unlock() } + cs.mu.Unlock() return &plan9.Fcall{Type: plan9.Rwstat, Tag: fc.Tag} }