Skip to content

Commit 5e3be43

Browse files
sirtimidclaude
andcommitted
fix(ocap-kernel): don't audit an importer entry the collector is retiring
`retireKernelObjects` deletes an object and queues a `retireImport` for each importer in the same breath, so until that action is delivered an importer's c-list entry names a kref the kernel has already dropped. The audit counted those entries as holders and reported a violation against the collector's own output — and since `assertRefCountsIfAuditing` throws from inside the crank, that killed the run loop for good. Reachable from an ordinary `terminateVat` while a surviving vat holds the dying vat's export in liveslots' dropped-but-recognizable state. No current test produced it; found by Cursor Bugbot on #1020 and reproduced against the real store. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 263411c commit 5e3be43

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

packages/ocap-kernel/src/store/methods/clist-accounting.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,26 @@ describe('c-list reference accounting', () => {
216216
expect(kernelStore.auditRefCounts()).toStrictEqual([]);
217217
});
218218

219+
it('tolerates an importer entry that outlives the object it names', () => {
220+
const kref = kernelStore.exportFromEndpoint('v1', 'o+1');
221+
kernelStore.translateRefKtoE('v3', kref, true);
222+
// v3 has let go of the object but can still recognize the name
223+
kernelStore.clearReachableFlag('v3', kref);
224+
kernelStore.markVatAsTerminated('v1');
225+
kernelStore.cleanupTerminatedVat('v1');
226+
227+
kernelStore.collectGarbage();
228+
229+
// The collector deletes the object and queues the retirement together, so
230+
// v3's entry names a kref the kernel has already dropped until that action
231+
// is delivered. Counting it as a holder fails the end-of-crank audit on a
232+
// state the collector itself just created, which kills the run loop.
233+
expect([...kernelStore.getGCActions()]).toStrictEqual([
234+
`v3 retireImport ${kref}`,
235+
]);
236+
expect(kernelStore.auditRefCounts()).toStrictEqual([]);
237+
});
238+
219239
it('leaves a live vat that shares the object untouched', () => {
220240
const kref = kernelStore.exportFromEndpoint('v2', 'o+1');
221241
kernelStore.translateRefKtoE('v1', kref, true);

packages/ocap-kernel/src/store/methods/refcount-audit.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ export function getRefCountAuditMethods(ctx: StoreContext) {
9898
*/
9999
function computeExpectedRefCounts(): Map<KRef, Tally> {
100100
const tallies = new Map<KRef, Tally>();
101+
// `retireKernelObjects` deletes an object and queues a `retireImport` for
102+
// each importer in the same breath, so between then and the delivery an
103+
// importer's c-list entry legitimately names a kref the kernel has already
104+
// dropped. Those entries are scheduled for teardown and are not holders.
105+
const retiring = new Set(
106+
(JSON.parse(ctx.gcActions.get() ?? '[]') as string[]).filter((action) =>
107+
action.includes(' retireImport '),
108+
),
109+
);
101110

102111
const credit = (
103112
kref: KRef,
@@ -152,7 +161,10 @@ export function getRefCountAuditMethods(ctx: StoreContext) {
152161
if (isPromiseRef(kref)) {
153162
// Both directions count for a promise.
154163
credit(kref, holder);
155-
} else if (direction === 'import') {
164+
} else if (
165+
direction === 'import' &&
166+
!retiring.has(`${endpointId} retireImport ${kref}`)
167+
) {
156168
// An object export is the owner's own entry and carries no count;
157169
// an object import always recognizes and, while flagged, reaches.
158170
credit(kref, holder, { onlyRecognizable: !isReachable });

0 commit comments

Comments
 (0)