File protocol version 2: per-write append, client-chosen handle IDs and bind-ordered stream succession - #359
Merged
Conversation
…nd bind-ordered stream succession - sandboxfs.Version is 2, so Link refuses a File stream between builds of different layouts with VersionMismatch. - WriteRequest.Append replaces OpenAppend. The Linux service opens files without O_APPEND, runs a handle's writes one at a time and sets or clears the descriptor's O_APPEND to match each write; overlayfs on some kernels, including 5.15, drops pwritev2's RWF_APPEND and writes at the offset. - Open, Create and OpenDir carry a client-chosen HandleID that the service reserves before any filesystem effect; a duplicate is InvalidArgument with EffectNone, and a Release of a pending ID waits for its acquisition. - sandboxfs.Server admits one stream per (ServerInstanceID, AttachmentID) in Link bind order: it drains the predecessor before a successor dispatches, and refuses a stream bound before one already admitted with ErrSuperseded, also after the newer stream ended, until the attachment's lease ends. - ErrorCode.Retryable marks ResourceExhausted as the only code a client resends after EffectNone. - The world frontend passes O_APPEND from each WRITE's flags as Append, so fcntl(F_SETFL) toggling works, and takes handle IDs from sandboxfs.HandleIDs. An Open, Create or OpenDir that may have taken effect without a response is never resent; its ID is released, on a successor stream after a redial, and StaleHandle settles it. A failed Serve detaches on a new stream fenced behind every earlier request, so ErrAttachmentDirty remains only when that Detach cannot be sent or answered. - fileservicetest serves through sandboxfs.Server with a Dial-order bind sequence.
SaladDay
force-pushed
the
aos/file-protocol
branch
from
October 1, 2026 03:49
3609bac to
8b666e2
Compare
The world frontend queues the release of an acquisition in doubt for the drainer, so the kernel request returns EIO without waiting for an unreachable service. Abort, Stop and shutdown close streams without waiting for the transport and send Detach on its own goroutine, so their bounds hold when outgoing traffic blocks; no stream opens after shutdown. The File server keeps its succession fence in shared per-attachment state: a successor's requests wait before they run, and a successor that has run nothing ends at once when it is superseded or its stream or context ends, passing its wait on to the next one. The File access protocol states the node-reference residual without a numeric bound and names the client usage that avoids abandoned replies.
…ransport The File server forgets an attachment's stream entry after its lease ends only once that stream has drained and so has every earlier stream that ran a request, so a later stream can no longer bypass a handler still running. The File client no longer waits for a transport that holds a write or a close: a call whose context ends while its request is being written fails the stream and returns at once, and the client closes the transport in the background after it fails the calls in flight. The world frontend relies on this for its Detach and stream drops instead of its own goroutine wrapper, and its abort test completes a request after Attach before it jams the transport, so the cancellation finds Attach in doubt.
Serve refuses a stream whose attachment lease ended before admission with sandboxfs.ErrLeaseEnded, checked under the lock that forgets settled streams, so a stream bound before a forgotten one is always refused. The fence test now renews the lease for the later stream, as Link does for an attachment ID bound again after its attachment closed. The File access protocol states that a call cancelled while its request is being written returns the transport failure, Unknown with EffectPossible.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
File protocol version 2 (plan row 6a, lane L1c). It fixes three defects that the blind review of the world frontend (#349) found, and the frontend can't fix them within version 1.
It uses the Link bind sequence from #358 (merged).
Protocol (
internal/sandboxfs/protocol.go,docs/file-access-protocol.md)The wire version changes outright to 2, with no fallback.
Per-write append
OpenAppendis removed and the remainingOpenFlagsare renumbered.WriteRequest.Appendreplaces it. An append write ignoresOffsetand writes atomically at the end of the file.fcntltogglesO_APPEND.AtomicAppenddeclares support for this.Client-chosen handle IDs (the 9P fid model)
Open,CreateandOpenDircarry aHandleIDthat the client chooses, and their responses no longer return one.sandboxfs.HandleIDsallocates IDs per attachment.MaxOpenHandles, and a duplicate ID is refused withInvalidArgumentandEffectNone.ReleaseorReleaseDirof a reserved ID runs only after that ID's acquisition finishes. That settles a lost or cancelled acquisition: success orStaleHandleproves that no handle with that ID remains.MaxInFlightare leaked per transport loss. A Detach to recover them would break every open handle of the world.Stream succession fence
(ServerInstanceID, AttachmentID), the server admits one stream at a time, in Link bind order.sandboxfs.ErrSuperseded.Detachcleans up an uncertainAttach, and aReleasecleans up an uncertain acquisition.ErrorCode.Retryable()ResourceExhaustedis the retryable code. A failure with a retryable code andEffectNonemay be resent unchanged.Linux service (
apps/sandboxio/internal/fileservice)O_APPEND, and a handle's writes run one at a time. Each write first sets or clears the descriptor'sO_APPENDto matchAppend; an append is then onewritecall.pwritev2(RWF_APPEND)is not used. On this host's 5.15 kernel, overlayfs ignores it and writes at offset 0, and the container suites on overlayfs caught this.World frontend (
apps/daemon/internal/worldfs)Appendfrom the FUSE write'sO_APPEND.HandleIDs. An uncertain Open, Create or OpenDir is never resent; it is released, on the new stream after a redial.ErrAttachmentDirty: it now remains only when that Detach can't be sent or answered.doc.golists those cases.ErrorCode.Retryable().Tests
Golden frames: regenerated, with an append
Writeframe added.Server:
AttachthenDetach, and forOpenthenRelease;Releasethat races a cancelledOpenthat is still running;Service:
Openreply released on the next stream.worldfs:
TestAppendFollowsFcntl(privileged);TestLostOpenIsReleased;TestUncertainAttachIsDetached.Checks:
go build ./...;go vet, andgo test -race -count=1on sandboxfs, sandboxlink, sandboxio, worldfs, sessionview and gateway;FuzzDecodefor 30 seconds.Privileged suites in the container:
Review fixes (8b666e2)
From the blind review:
TestSupersededWaiterEnds,TestAbortBoundedWhenTransportBlocks, and a rewrittenTestLostOpenIsReleased.sandboxfs.ErrLeaseEnded.