From 9a8f97c909b73b2b00baf2faf2492a0966fe7fb2 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 23 Feb 2026 19:18:37 +0100 Subject: [PATCH] fix(ocap-kernel): enqueue async vat syscalls immediately when outside a crank MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a vat's async operation (e.g. fetch) completes between cranks, the resulting syscall.resolve was buffered with immediate=false. Since no crank was active to flush the buffer, notifications were never delivered and the crank loop never restarted — causing the kernel to hang indefinitely. Fix: add isInCrank() to the kernel store's crank methods. In VatSyscall, check isInCrank() to determine the immediate flag: - During a crank (immediate=false): buffer as before for atomic flush - Outside a crank (immediate=true): enqueue directly to wake the run queue via the existing wakeUpTheRunQueue mechanism This unblocks any vat method that chains multiple E() calls with real async I/O between vats (e.g. coordinator → provider with fetch → coordinator → keyring → provider). Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/ocap-kernel/src/store/index.test.ts | 1 + .../src/store/methods/crank.test.ts | 13 +++++++ .../ocap-kernel/src/store/methods/crank.ts | 10 ++++++ .../ocap-kernel/src/vats/VatSyscall.test.ts | 34 +++++++++++++++++++ packages/ocap-kernel/src/vats/VatSyscall.ts | 20 +++++++---- 5 files changed, 72 insertions(+), 6 deletions(-) diff --git a/packages/ocap-kernel/src/store/index.test.ts b/packages/ocap-kernel/src/store/index.test.ts index 1749f4f28..247e6513a 100644 --- a/packages/ocap-kernel/src/store/index.test.ts +++ b/packages/ocap-kernel/src/store/index.test.ts @@ -121,6 +121,7 @@ describe('kernel store', () => { 'initKernelObject', 'initKernelPromise', 'invertRRef', + 'isInCrank', 'isObjectPinned', 'isRevoked', 'isRootObject', diff --git a/packages/ocap-kernel/src/store/methods/crank.test.ts b/packages/ocap-kernel/src/store/methods/crank.test.ts index eba5bca01..6e6310937 100644 --- a/packages/ocap-kernel/src/store/methods/crank.test.ts +++ b/packages/ocap-kernel/src/store/methods/crank.test.ts @@ -215,4 +215,17 @@ describe('crank methods', () => { crankMethods.endCrank(); }); }); + + describe('isInCrank', () => { + it('returns false when not in a crank', () => { + expect(crankMethods.isInCrank()).toBe(false); + }); + + it('returns true during a crank', () => { + crankMethods.startCrank(); + expect(crankMethods.isInCrank()).toBe(true); + crankMethods.endCrank(); + expect(crankMethods.isInCrank()).toBe(false); + }); + }); }); diff --git a/packages/ocap-kernel/src/store/methods/crank.ts b/packages/ocap-kernel/src/store/methods/crank.ts index 8fbb688c3..f4b5aa9cc 100644 --- a/packages/ocap-kernel/src/store/methods/crank.ts +++ b/packages/ocap-kernel/src/store/methods/crank.ts @@ -106,6 +106,15 @@ export function getCrankMethods(ctx: StoreContext, kdb: KernelDatabase) { return items; } + /** + * Check whether the kernel is currently inside a crank. + * + * @returns True if a crank is in progress. + */ + function isInCrank(): boolean { + return ctx.inCrank; + } + return { startCrank, createCrankSavepoint, @@ -115,5 +124,6 @@ export function getCrankMethods(ctx: StoreContext, kdb: KernelDatabase) { waitForCrank, bufferCrankOutput, flushCrankBuffer, + isInCrank, }; } diff --git a/packages/ocap-kernel/src/vats/VatSyscall.test.ts b/packages/ocap-kernel/src/vats/VatSyscall.test.ts index a93a361de..1fe6e5a07 100644 --- a/packages/ocap-kernel/src/vats/VatSyscall.test.ts +++ b/packages/ocap-kernel/src/vats/VatSyscall.test.ts @@ -32,6 +32,7 @@ describe('VatSyscall', () => { forgetKref: vi.fn(), getVatConfig: vi.fn(() => ({})), isVatActive: vi.fn(() => true), + isInCrank: vi.fn(() => true), } as unknown as KernelStore; logger = { debug: vi.fn(), @@ -66,6 +67,27 @@ describe('VatSyscall', () => { ); }); + it('enqueues send immediately when outside a crank', () => { + (kernelStore.isInCrank as unknown as MockInstance).mockReturnValue(false); + const target = 'o+1'; + const message = { methargs: { body: '', slots: [] } } as unknown as Message; + const vso = ['send', target, message] as unknown as VatSyscallObject; + vatSys.handleSyscall(vso); + expect(kernelQueue.enqueueSend).toHaveBeenCalledWith(target, message, true); + }); + + it('resolves promises immediately when outside a crank', () => { + (kernelStore.isInCrank as unknown as MockInstance).mockReturnValue(false); + const resolution = ['kp1', false, {}] as unknown as VatOneResolution; + const vso = ['resolve', [resolution]] as unknown as VatSyscallObject; + vatSys.handleSyscall(vso); + expect(kernelQueue.resolvePromises).toHaveBeenCalledWith( + 'v1', + [resolution], + true, + ); + }); + describe('subscribe syscall', () => { it('subscribes to unresolved promise', async () => { ( @@ -95,6 +117,18 @@ describe('VatSyscall', () => { false, ); }); + + it('notifies immediately for resolved promise when outside a crank', () => { + (kernelStore.isInCrank as unknown as MockInstance).mockReturnValue(false); + ( + kernelStore.getKernelPromise as unknown as MockInstance + ).mockReturnValueOnce({ + state: 'fulfilled', + }); + const vso = ['subscribe', 'kp1'] as unknown as VatSyscallObject; + vatSys.handleSyscall(vso); + expect(kernelQueue.enqueueNotify).toHaveBeenCalledWith('v1', 'kp1', true); + }); }); describe('dropImports syscall', () => { diff --git a/packages/ocap-kernel/src/vats/VatSyscall.ts b/packages/ocap-kernel/src/vats/VatSyscall.ts index 014018c85..9453429c6 100644 --- a/packages/ocap-kernel/src/vats/VatSyscall.ts +++ b/packages/ocap-kernel/src/vats/VatSyscall.ts @@ -71,23 +71,30 @@ export class VatSyscall { } /** - * Handle a 'send' syscall from the vat. The send is buffered and will be - * flushed to the run queue on successful crank completion. + * Handle a 'send' syscall from the vat. During a crank, the send is + * buffered and flushed on crank completion. Outside a crank (async vat + * operations like fetch), the send is enqueued immediately to wake the + * run queue. * * @param target - The target of the message send. * @param message - The message that was sent. */ #handleSyscallSend(target: KRef, message: Message): void { - this.#kernelQueue.enqueueSend(target, message, false); + const immediate = !this.#kernelStore.isInCrank(); + this.#kernelQueue.enqueueSend(target, message, immediate); } /** - * Handle a 'resolve' syscall from the vat. + * Handle a 'resolve' syscall from the vat. During a crank, notifications + * are buffered and flushed on crank completion. Outside a crank (async vat + * operations like fetch), notifications are enqueued immediately to wake + * the run queue. * * @param resolutions - One or more promise resolutions. */ #handleSyscallResolve(resolutions: VatOneResolution[]): void { - this.#kernelQueue.resolvePromises(this.vatId, resolutions, false); + const immediate = !this.#kernelStore.isInCrank(); + this.#kernelQueue.resolvePromises(this.vatId, resolutions, immediate); } /** @@ -100,7 +107,8 @@ export class VatSyscall { if (kp.state === 'unresolved') { this.#kernelStore.addPromiseSubscriber(this.vatId, kpid); } else { - this.#kernelQueue.enqueueNotify(this.vatId, kpid, false); + const immediate = !this.#kernelStore.isInCrank(); + this.#kernelQueue.enqueueNotify(this.vatId, kpid, immediate); } }