Server owns the fid map: validate newfids, GC stale fids

- 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.
This commit is contained in:
Ollie Agent 2026-08-02 16:08:23 +02:00
parent 8601562962
commit 05e0aba155
1 changed files with 29 additions and 18 deletions

View File

@ -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}
}