Skip to content

9P over devssl survives its readers being killed mid-read - #1153

Merged
pdfinn merged 2 commits into
masterfrom
test/disconnect-fuzz
Nov 10, 2026
Merged

pdfinn merged 2 commits into
masterfrom
test/disconnect-fuzz

Conversation

@pdfinn

@pdfinn pdfinn commented Nov 10, 2026

Copy link
Copy Markdown
Member

Readers killed in the middle of their reads could break an encrypted 9P mount for everyone else on it. This is the robustness half of the 9P/video work: a board's feed went out of step after many viewers left (too many QIDs returned by walk, fid already in use), unexplained until now.

The test

tests/ninep_disconnect_test.b sets up an encrypted mount the way a feed is served: export(2) over loopback TCP with devssl, as listen ... {export dir} does. The exported tree has a 1 MB file and a file2chan file whose reads answer late, much later, or never. Each round it:

  • starts readers in their own process group, each with several reads in flight;
  • kills the group at a random moment;
  • then requires the same mount to walk, stat and read the big file byte for byte.

Every few rounds it also drops the connection and makes another. At the end no Prog may be left over. It runs 60 rounds (-n, -s seed) in about 5 seconds and exits on its own, so the runner picks it up.

On the unfixed emulator it failed in round 0, every time.

Four causes, each fixed

  1. devssl hung up the connection on any write error, interrupts included. A 9P client killed while sending its request took the mount down for every other client (inappropriate use of fd).
    • In emu, records now go through a queue to a writer kproc, so a record is never half sent. Queueing is all or nothing, and an interrupt after everything is queued no longer causes a hangup.
    • Native TCP and pipes are qio, which queues before it waits, so native needs only the second half.
  2. emu's osleave threw away a host call's work when an interrupt arrived during it. Bytes read off a socket were lost, and devssl's next record failed its digest (bad digest).
    • New osleavekeep drops the interrupt after a socket read or write that moved bytes.
    • A killed Prog is still found killed at its next sleep and on its way back to the VM.
    • Kept rather than dropped, it interrupted an exportfs worker's next request; that's why it is dropped.
  3. A tag was reused while a flush naming it was still out. An orphaned call answered before its flush was freed at once. The server then flushed the new call that got the tag, and the client took the old flush's answer for the new call. Replies reached the wrong calls (mismatch ... T110 R121, an Rclunk to a Twalk; then an Rwalk to a Tclunk), as on the Pi.
    • Such a call is now held, with its tag, on m->held until its flush is answered. Emu and native.
  4. A clunk, or a walk's new fid, given up on unanswered left its fid number to be reused while the server might still hold it (fid in use).
    • chanfidretire gives the Chan a fid never used before. Emu and native.

Verification

  • macOS emu: the test passes all 60 rounds on every seed tried (1–9), under -c1 and -c0. Before the fixes it failed in round 0. The full suite: 273 passed, 0 failed, 33 skipped.
  • sysfuzz on emu: 6 seeds, all pass.
  • Native objects (devmnt, devssl, chan, qio) compile with the kernels' flags.
  • Native virt suite on the Orin: 200 passed, 5 failed, all five in USB-Ethernet networking.
    • The adapter's DHCP got ether0's address after another DHCP client has port 68; waiting, and pings over the adapter failed.
    • Master's CI virt job passes these, so I'm running master on the same Orin (QEMU 6.2) to compare and will post the result here. This PR's own virt CI run is the other check.

tests/ninep_disconnect_test.b kills readers of an encrypted 9P mount
(export(2) over loopback TCP with devssl, as listen ... {export dir}
serves a feed) at random moments, round after round, and checks the
same mount still walks, stats and reads byte for byte. It failed at
its first round; four causes:

- devssl hung up the connection on any write error, interrupted
  included: a 9P client killed while sending its request took the
  mount down for every other client ("inappropriate use of fd"). In
  the emulator a record went to the socket from the writer's own
  thread, and an interrupt could cut it part way. Records now go
  through a queue to a writer kproc, queueing being whole or nothing,
  and an interrupt after every record is queued is no reason to hang
  up. Native's TCP and pipes are qio, which queues before it waits,
  so native needs only the second half.

- emu's osleave raised an interrupt that arrived during a host call
  even when the call had done its work: bytes read off a socket were
  thrown away and the next devssl record failed its digest ("bad
  digest"). osleavekeep drops the interrupt after a socket read or
  write that moved bytes; a killed Prog is still found killed at its
  next sleep and on its way back to the VM.

- An orphaned call answered before its flush was freed at once and
  its tag reused while the flush naming it was still out: the server
  flushed the new call and the client took the old flush's answer for
  it, so replies reached the wrong calls (an Rclunk to a Twalk, an
  Rwalk to a Tclunk: "too many QIDs returned by walk", "unexpected
  reply tag"). Such a call is now held, with its tag, on m->held until
  its flush is answered. Emu and native.

- A clunk or a walk's new fid given up on unanswered was used again
  while the server might still hold it ("fid in use"):
  chanfidretire gives the Chan a fid never used before. Emu and
  native.

macOS emu: the test passes 60 rounds on every seed tried, under -c1
and -c0; the whole suite 273 passed, 0 failed.
@pdfinn

pdfinn commented Nov 10, 2026

Copy link
Copy Markdown
Member Author

Native comparison on the same Orin (QEMU 6.2), virt suite: master 036dd48 gets 200 passed, 5 failed, and this branch gets 200 passed, 5 failed. The five failures are the same in both: USB Ethernet DHCP, its default route, and pings over the usb, xhci and msi adapters. They come from that machine's QEMU, not from this change; master's CI virt job passes them.

@pdfinn

pdfinn commented Nov 10, 2026

Copy link
Copy Markdown
Member Author

On the Raspberry Pi 5, native, ninep_disconnect_test -n 40: the master kernel fails in round 1 (mount rpc error), and this branch's kernel passes all 40 rounds on seeds 1, 2 and 3.

pdfinn added a commit that referenced this pull request Nov 10, 2026
…1152

#1133 (merged) and this branch fix the same failures of an encrypted
mount whose readers are killed, differently.  Where they meet, this
branch's design is kept:

- osleavekeep: this branch's, which drops an interrupt that came while
  a host call moved bytes.  #1133's left it pending for the next Sleep,
  which interrupted an exportfs worker's next request ("clone failed:
  interrupted").  The two definitions had both merged into proc.c.
- sslbwrite: this branch's, records queued whole to a writer of their
  own.  #1133's begun-record tracking and the interrupt it raised before
  each record are left out: merged with them, ninep_disconnect_test
  crashed in mntfree.
- devmnt: #1133's mntkilledsend, which cleared a pending interrupt
  before a killed call's flush, clunk or remove, is left out: with it
  ninep_disconnect_test failed 1 run in 8 ("fid in use").
- ipif (FreeBSD, Nt too): this branch's comment, the interrupt dropped.

Without them, under -c0: ninep_disconnect_test 10 of 10 (it had failed
3 of 6 merged as git merged it, and #1153 alone passed 5 of 5), and
#1133's own ssl_killedcall_test 5 of 5.  kill_deadserver_test,
kill_uncalled_test, 9p_export_test and tcp_test pass.  The native
kernel's devssl merged without conflict: #1133 did not change its write
path.
@pdfinn
pdfinn merged commit f04bfc0 into master Nov 10, 2026
58 of 59 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant