Repository navigation
A delivery to a remote that fails after its crank commits is lost with nobody told #1125
Description
#1064 closed the case where a remote refuses a GC delivery: #1099 aborts the crank, which restores the action and the c-list entries together, and holds that remote's actions back until the run loop next goes idle. This is the same damage reached by the other route — the delivery succeeds, the crank commits, and the send fails afterwards — where that mechanism cannot apply because there is no crank left to abort.
The sequence
processGCActionSetselects an action and callsstorage.setGCActions(allActionsSet)before returning it, so the durable set has already dropped it.RemoteHandle.#deliverToPeerwrites the message toremotePending.<remoteId>.<seq>and returnsafterCommit. Nothing has failed yet.- The crank commits. The GC action is now gone for good.
afterCommithands the message to the transport andsendRemoteMessagerejects with a terminal error —PeerRestartedError,IntentionalCloseErrororNetworkStoppedError(three transient failures have already been retried).#rejectAllPendingadvancesstartSeqpast the message, so it is never retransmitted, and#onGiveUpfires.
Why nobody is told
RemoteManager.#handleRemoteGiveUp rejects the promises it finds via getPromisesByDecider(remoteId), which filters on kp.state === 'unresolved' && kp.decider === decider — promises the remote owes us. Two kinds of outbound delivery are invisible to it:
- A GC action (
dropExports,retireExports,retireImports) has no promise at all. - A
notifycarries a resolution for a promise the peer imported. We decided it, so it isresolved, and its decider is not the remote. Neither test matches.
So both are dropped silently.
The damage
For a GC action, exactly what #1064 describes. The kernel releases its own side before telling the endpoint, so after this the peer still holds the eref while the kernel has forgotten it. The peer's next message naming that object goes through exportFromEndpoint and mints a second kref for the same object. An incarnation change reconciles a dropped or retired export, but not a retireImports: forgetEndpointImports keeps only entries whose direction is export.
For a notify, arguably worse: the peer's imported promise never settles and nothing times it out, so whatever is waiting on it hangs for the life of that incarnation.
Reachability
Narrow but real. It needs a peer dead enough to exhaust three retries over ~30s, and to come back without restarting — a network drop that recovers, rather than a peer restart. A peer that restarts is cleaned up by the incarnation-change path; a peer that never returns makes the drift moot. The window in between is where this bites, and giveUp does not remove the handle, so the kernel will talk to that peer again.
Directions
Neither of these is the unbounded retry the original comment in #deliverGCAction rightly rejects:
- Make the failure restartable.
#rejectAllPendingcurrently abandons the payload. It could instead put the GC action back in the durable set, which would need the action to be recoverable from the pending message — and would have to happen inside a crank, which is the same "only the run loop writes the store" problem the transport's failure handling already has (see the Not fixed here section of feat(ocap-kernel): send a remote delivery once its crank has committed #1101). - Record the orphan for the next incarnation change to sweep, which is option 2 from A remote's retireImports that fails is never reconciled #1064: either
forgetEndpointImportsstops filtering on direction, or a separate record covers the entries it does not.
The notify half may want a different answer from the GC half, since there is no action to restore — only a peer-side promise that will never settle.
Related
- A remote's retireImports that fails is never reconciled #1064 — the refuse-during-the-crank route, closed by feat(ocap-kernel): keep a GC action a remote could not be told about #1099.
- GC deliveries to remotes carry rrefs in the sender's frame, so the receiver mints a phantom object #1017 — GC deliveries carry rrefs in the sender's frame.
- retireKernelObjects never notifies remote importers, leaving a dangling c-list entry #1015 —
retireKernelObjectsand remote importers. - feat(ocap-kernel): send a remote delivery once its crank has committed #1101 — introduced
afterCommit, which is what creates the commit-then-send gap this exploits. Its Not fixed here section names the related problem that the transport's failure handling writes the kernel store from outside any crank.
Activity
sirtimid commentedon Oct 21, 2026
Blocked by #1152, which moves the give-up's writes (
#rejectAllPending) into a crank of its own.