Skip to content

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:

  1. What does qid.path (and the corresponding ino) identify when a path is recyclable? E.g. a container named foo is 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 different qid.path or a bumped qid.version. Today containerFS keys inoFor('child:${name}') which violates this.
  2. What is the correct value for nlink on a synthetic directory? Hardcoded values (root=4, self=2, children=2) drift from the Unix 2 + subdir_count convention, breaking find -type d heuristics that rely on nlink == 2 to 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:

  1. 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’s inoFor() (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.

  2. nlink for synthetic directories follows Unix convention: 2 + count(subdirs). Only directory children count toward nlink (files don’t). Implementation: a single shared helper dirNlink(subdirCount: number): number returning 2 + subdirCount. Applied at stat time for dynamic dirs; precomputed at construction for static dirs. We follow Unix convention here even though 9P itself has no nlink concept — 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.

Current state. Drifts as subdir counts change; consFS root is wrong-by-accident (4 over zero subdirs). Rejected.

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 N and other nlink-heuristic Unix tools work correctly against synthetic mounts.
  • Single dirNlink helper 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/foo returns 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.
  • dirNlink requires 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.