ADR-0002: synthetic-fileserver stat semantics
Status: Proposed Date: 2026-04-24 Tags: #kernel #fileservers #container #9p #unix-compat
Context
Synthetic fileservers (containerFS, consFS, motdFS, factotumFS, composeFS, deployFS, exportsFS, agentFS) compose a Stat from a mix of 9P-native fields (qid.path, qid.version, qid.type) and Unix-compat fields (ino, dev, nlink, mode). Two recurring questions surface as new synthetic servers are added:
- What does
qid.path(and the correspondingino) identify when a path is recyclable? E.g. a container namedfoois destroyed and a new one created with the same name. Real Unix recycles inodes, NFS solves it with(fh, generation), 9P’s contract is unambiguous: the new file MUST have a differentqid.pathor a bumpedqid.version. TodaycontainerFSkeysinoFor('child:${name}')which violates this. - What is the correct value for
nlinkon a synthetic directory? Hardcoded values (root=4,self=2,children=2) drift from the Unix2 + subdir_countconvention, breakingfind -type dheuristics that rely onnlink == 2to skip subdir descent.
Both decisions affect every synthetic fileserver and every future one. Documenting them once here prevents drift across servers.
Decision
Two coordinated rules for synthetic fileserver stat composition:
-
qid identity = lifetime-stable id, not path. When the entity behind a synthetic path has its own lifetime-stable identifier (e.g.
Container.containerId), the fileserver’sinoFor()(and qid derivation) keys on that identifier — not on the path/name. Re-creating an entity with the same name produces a different qid.path, correctly signaling to clients (“this is a different file”) that the prior cached qid must be discarded. Path/name remains the human-visible label in dirent output; only the identity derivation changes. -
nlinkfor synthetic directories follows Unix convention:2 + count(subdirs). Only directory children count toward nlink (files don’t). Implementation: a single shared helperdirNlink(subdirCount: number): numberreturning2 + subdirCount. Applied at stat time for dynamic dirs; precomputed at construction for static dirs. We follow Unix convention here even though 9P itself has nonlinkconcept — we serve Unix tools (find,ls,fts-walkers) and breaking their heuristics is a real-world bug.
Alternatives Considered
#1 — Generation-counter key ('child:${name}:${gen}')
Embeds destruction history in a path string and forces the manager to track a counter purely for ino derivation. Reinvents identity that already exists at the entity layer (Container.containerId). Rejected.
#1 — Bump qid.version instead of qid.path
9P-conformant alternative — same path, new version means “file changed.” But version is for content changes within a file’s lifetime, not for “this is a different file.” Wrong semantic. Rejected.
#2 — Hardcoded nlink per dir
Current state. Drifts as subdir counts change; consFS root is wrong-by-accident (4 over zero subdirs). Rejected.
#2 — Skip nlink (return 0 or 1)
Plan 9-faithful (Plan 9 doesn’t have nlink) but breaks Unix client heuristics. Rejected on pragmatic grounds.
Consequences
Positive
- 9P qid contract honored across synthetic fileservers — cached qids correctly invalidate on entity recreate.
find -type d -maxdepth Nand other nlink-heuristic Unix tools work correctly against synthetic mounts.- Single
dirNlinkhelper eliminates per-server bugs from copy-paste. - New synthetic fileservers inherit the rules by following the helper + identity-keying pattern.
Negative / Trade-offs
ls -i /dev/container/children/fooreturns different inodes across destroy/recreate cycles — observable behavior change, but the correct one (a destroyed container is a different file). Document in user-facing notes.dirNlinkrequires synthetic servers to know their subdir count at stat time; for highly dynamic dirs this means computing per-stat. Acceptable: synthetic dirs have small, bounded child sets.- This ADR couples two decisions; if they diverge later (e.g. nlink rule changes), one ADR-supersession touches both. Tradeoff for a single discoverable record.
Open follow-ups
bug-containerfs-qid-recreate-stability.md— implementation of the qid identity rule for containerFS.gap-synthetic-dir-nlink-accuracy.md— implementation of the nlink rule across all synthetic fileservers.- Future synthetic fileservers (e.g. layerFS, imgFS) inherit both rules without separate decision records.