<feed xmlns='http://www.w3.org/2005/Atom'>
<title>cloud9.git/src/post.zig, branch main</title>
<subtitle>9p for zig</subtitle>
<id>https://git.0x4200.cafe/cloud9.git/atom?h=main</id>
<link rel='self' href='https://git.0x4200.cafe/cloud9.git/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/cloud9.git/'/>
<updated>2026-09-21T18:20:27Z</updated>
<entry>
<title>9ns --mntgen: registry subdirectories are mount points too</title>
<updated>2026-09-21T18:20:27Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-09-21T17:23:27Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/cloud9.git/commit/?id=0d7e295efee1fca0935cf4a8bee9629c007dd2b6'/>
<id>urn:sha1:0d7e295efee1fca0935cf4a8bee9629c007dd2b6</id>
<content type='text'>
A registry entry that is a directory is now served the way the root is:
a synthetic directory listing the real one, dialing the sockets inside
it on walk and recursing into further directories, to max_synth_depth
(8) levels across max_synth_dirs (64) synthetic nodes. That is the
plan9port mntgen shape and the layout zmx now posts under, so a live
session reads at /mnt/9p/zmx/&lt;name&gt;. Before this a directory in the
registry was dialed like a socket and answered EIO for good.

post gains the two entry points the traversal needs: postedDir (the
registry scan, against any directory) and dialPath (a dial by composed
path, no name validation).

Hardening, each from an attack that broke the code:

- BATCH_FORGET carries entries for many owners and puts 0 in the header
  nodeid, so routing it by the header dropped all of them: 32 of 64
  synthetic slots leaked in one close burst and the subdirectories that
  held them answered EIO forever. distributeForgets unpacks the body and
  hands each entry to its owner.
- probe() and connectBlocking() copied a caller's path into the kernel
  address with no bound: a path past sun_path overran the 110-byte stack
  sockaddr (a panic in Debug, silent corruption in ReleaseFast). Both
  refuse it now, probe as `.live` so a claim never deletes what it could
  not inspect.
- That bound then caught 9proc's own listener, which handed probe() the
  whole 108-byte sun_path array instead of the path inside it. The probe
  reads `.live` for anything it cannot ask about, so every stale socket
  became AlreadyListening and no server could ever take a dead
  predecessor's name back. It passes the path now.

Suites: 87/87 root (+7 post/serve attack regressions), 48/48 9ns,
51+88 9ns integration (+4 traversal and slot-recycling checks), 213/0
9ns adversarial, 60/60 9proc plus its adversarial suites with a new
stale-socket takeover check, freestanding green.
</content>
</entry>
<entry>
<title>post registry + 9ns --mntgen: the /srv translation</title>
<updated>2026-09-21T17:13:43Z</updated>
<author>
<name>Gabriel Schneider</name>
<email>gbrls@0x4200.cafe</email>
</author>
<published>2026-09-21T17:13:43Z</published>
<link rel='alternate' type='text/html' href='https://git.0x4200.cafe/cloud9.git/commit/?id=3a23f6a29e47ace901bd4d82b9db4055fcc12bb9'/>
<id>urn:sha1:3a23f6a29e47ace901bd4d82b9db4055fcc12bb9</id>
<content type='text'>
cloud9.post: servers post their socket under a name in
$XDG_RUNTIME_DIR/9p (post/unpost, posted, dial, Watch) and
serve.Runner.listenPosted posts a server by name, unposting on stop.
Names are budget-checked against the 108-byte socket path; a claim
binds+listens at a private temp path and takes the name with atomic
renames under flock (RENAME_NOREPLACE for free names, RENAME_EXCHANGE
grab-verify-commit for stale ones): the registry path is never unlinked
by a claim, live names refuse with AlreadyPosted, foreign files with
NotSocket, and unpost removes only the caller's inode-matched entry.
Watch surfaces inotify overflow and a replaced registry dir.

9ns --mntgen [--mount DIR] -- PROGRAM: one FUSE mount at /mnt/9p whose
synthetic root lists the posted registry (no connection made); a walk
into an unmounted name dials it and runs the existing bridge dispatch
in a per-server worker thread, routed by mount index in the node id's
top bits (ordinals never reused, cap 4096); a dead server answers EIO
on its subtree and is re-dialed on the next walk. The dial watches
stop_fd through Tversion (connectWatched). All existing 9ns forms are
unchanged.

9proc's unix listener no longer blind-unlinks its path: a foreign
non-socket is refused (Occupied), a live server is refused
(AlreadyListening), only a refused socket is cleared, and stop()
unlinks only the listener's own inode-matched socket.

Hardened by adversarial review (GLM 5.3 x2 + DeepSeek V4.1 Flash, all
high-thinking): double-bind races on one name (0 in 180k rounds),
foreign-file TOCTOU deletions (0 in 4M flips), a 255-byte-name listing
panic, inotify queue overflow silently dropped, listenPosted silently
overwriting, dial-time Tversion hangs wedging the dispatcher, --debug
silently ignored in mntgen, and xattr/statx probes answering EPERM on
the synthetic root (broke `ls -l /mnt/9p`).

Tests: root 80/80, 9ns 47/47, 9proc 60/60, integration 88/88 +
mntgen 37/37, adversarial 213/0, freestanding riscv32 gate green.
</content>
</entry>
</feed>
