Skip to content

Commit c37577b

Browse files
sirtimidclaude
andauthored
fix(ocap-kernel): enqueue async vat syscalls immediately when outside a crank (#848)
## Summary - 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. - Add `isInCrank()` to the kernel store's crank methods. In `VatSyscall`, check `isInCrank()` to determine the `immediate` flag: during a crank, buffer as before; outside a crank, enqueue immediately to wake the run queue. - This unblocks any vat method that chains multiple `E()` calls with real async I/O between vats (e.g. `coordinator → provider(fetch) → coordinator → keyring → provider(fetch)`). ## Test plan - [x] All 1983 ocap-kernel unit tests pass - [x] All 62 kernel-test integration tests pass (persistence, liveslots, remotes, IO) - [x] New unit tests for `isInCrank()` (true during crank, false outside) - [x] New unit tests for out-of-crank syscall behavior (`immediate=true`) - [x] Verified with eth-wallet's `redeemDelegation` pipeline (5-hop E() chain with real fetch I/O) — previously hung, now completes 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes core syscall-to-queue behavior for `send`/`resolve`/`notify`, which can affect scheduling and ordering across the kernel run loop, though the change is small and covered by new tests. > > **Overview** > Fixes a kernel hang by making vat syscalls that occur *outside* an active crank enqueue work immediately instead of buffering it for crank completion. > > Adds `isInCrank()` to crank methods / kernel store, and updates `VatSyscall` (`send`, `resolve`, and resolved-`subscribe` notify) to set the queue `immediate` flag based on `!kernelStore.isInCrank()`, with new unit tests covering both the new API and out-of-crank behavior. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9a8f97c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3064275 commit c37577b

5 files changed

Lines changed: 72 additions & 6 deletions

File tree

packages/ocap-kernel/src/store/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ describe('kernel store', () => {
121121
'initKernelObject',
122122
'initKernelPromise',
123123
'invertRRef',
124+
'isInCrank',
124125
'isObjectPinned',
125126
'isRevoked',
126127
'isRootObject',

packages/ocap-kernel/src/store/methods/crank.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,4 +215,17 @@ describe('crank methods', () => {
215215
crankMethods.endCrank();
216216
});
217217
});
218+
219+
describe('isInCrank', () => {
220+
it('returns false when not in a crank', () => {
221+
expect(crankMethods.isInCrank()).toBe(false);
222+
});
223+
224+
it('returns true during a crank', () => {
225+
crankMethods.startCrank();
226+
expect(crankMethods.isInCrank()).toBe(true);
227+
crankMethods.endCrank();
228+
expect(crankMethods.isInCrank()).toBe(false);
229+
});
230+
});
218231
});

packages/ocap-kernel/src/store/methods/crank.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,15 @@ export function getCrankMethods(ctx: StoreContext, kdb: KernelDatabase) {
106106
return items;
107107
}
108108

109+
/**
110+
* Check whether the kernel is currently inside a crank.
111+
*
112+
* @returns True if a crank is in progress.
113+
*/
114+
function isInCrank(): boolean {
115+
return ctx.inCrank;
116+
}
117+
109118
return {
110119
startCrank,
111120
createCrankSavepoint,
@@ -115,5 +124,6 @@ export function getCrankMethods(ctx: StoreContext, kdb: KernelDatabase) {
115124
waitForCrank,
116125
bufferCrankOutput,
117126
flushCrankBuffer,
127+
isInCrank,
118128
};
119129
}

packages/ocap-kernel/src/vats/VatSyscall.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ describe('VatSyscall', () => {
3232
forgetKref: vi.fn(),
3333
getVatConfig: vi.fn(() => ({})),
3434
isVatActive: vi.fn(() => true),
35+
isInCrank: vi.fn(() => true),
3536
} as unknown as KernelStore;
3637
logger = {
3738
debug: vi.fn(),
@@ -66,6 +67,27 @@ describe('VatSyscall', () => {
6667
);
6768
});
6869

70+
it('enqueues send immediately when outside a crank', () => {
71+
(kernelStore.isInCrank as unknown as MockInstance).mockReturnValue(false);
72+
const target = 'o+1';
73+
const message = { methargs: { body: '', slots: [] } } as unknown as Message;
74+
const vso = ['send', target, message] as unknown as VatSyscallObject;
75+
vatSys.handleSyscall(vso);
76+
expect(kernelQueue.enqueueSend).toHaveBeenCalledWith(target, message, true);
77+
});
78+
79+
it('resolves promises immediately when outside a crank', () => {
80+
(kernelStore.isInCrank as unknown as MockInstance).mockReturnValue(false);
81+
const resolution = ['kp1', false, {}] as unknown as VatOneResolution;
82+
const vso = ['resolve', [resolution]] as unknown as VatSyscallObject;
83+
vatSys.handleSyscall(vso);
84+
expect(kernelQueue.resolvePromises).toHaveBeenCalledWith(
85+
'v1',
86+
[resolution],
87+
true,
88+
);
89+
});
90+
6991
describe('subscribe syscall', () => {
7092
it('subscribes to unresolved promise', async () => {
7193
(
@@ -95,6 +117,18 @@ describe('VatSyscall', () => {
95117
false,
96118
);
97119
});
120+
121+
it('notifies immediately for resolved promise when outside a crank', () => {
122+
(kernelStore.isInCrank as unknown as MockInstance).mockReturnValue(false);
123+
(
124+
kernelStore.getKernelPromise as unknown as MockInstance
125+
).mockReturnValueOnce({
126+
state: 'fulfilled',
127+
});
128+
const vso = ['subscribe', 'kp1'] as unknown as VatSyscallObject;
129+
vatSys.handleSyscall(vso);
130+
expect(kernelQueue.enqueueNotify).toHaveBeenCalledWith('v1', 'kp1', true);
131+
});
98132
});
99133

100134
describe('dropImports syscall', () => {

packages/ocap-kernel/src/vats/VatSyscall.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,23 +71,30 @@ export class VatSyscall {
7171
}
7272

7373
/**
74-
* Handle a 'send' syscall from the vat. The send is buffered and will be
75-
* flushed to the run queue on successful crank completion.
74+
* Handle a 'send' syscall from the vat. During a crank, the send is
75+
* buffered and flushed on crank completion. Outside a crank (async vat
76+
* operations like fetch), the send is enqueued immediately to wake the
77+
* run queue.
7678
*
7779
* @param target - The target of the message send.
7880
* @param message - The message that was sent.
7981
*/
8082
#handleSyscallSend(target: KRef, message: Message): void {
81-
this.#kernelQueue.enqueueSend(target, message, false);
83+
const immediate = !this.#kernelStore.isInCrank();
84+
this.#kernelQueue.enqueueSend(target, message, immediate);
8285
}
8386

8487
/**
85-
* Handle a 'resolve' syscall from the vat.
88+
* Handle a 'resolve' syscall from the vat. During a crank, notifications
89+
* are buffered and flushed on crank completion. Outside a crank (async vat
90+
* operations like fetch), notifications are enqueued immediately to wake
91+
* the run queue.
8692
*
8793
* @param resolutions - One or more promise resolutions.
8894
*/
8995
#handleSyscallResolve(resolutions: VatOneResolution[]): void {
90-
this.#kernelQueue.resolvePromises(this.vatId, resolutions, false);
96+
const immediate = !this.#kernelStore.isInCrank();
97+
this.#kernelQueue.resolvePromises(this.vatId, resolutions, immediate);
9198
}
9299

93100
/**
@@ -100,7 +107,8 @@ export class VatSyscall {
100107
if (kp.state === 'unresolved') {
101108
this.#kernelStore.addPromiseSubscriber(this.vatId, kpid);
102109
} else {
103-
this.#kernelQueue.enqueueNotify(this.vatId, kpid, false);
110+
const immediate = !this.#kernelStore.isInCrank();
111+
this.#kernelQueue.enqueueNotify(this.vatId, kpid, immediate);
104112
}
105113
}
106114

0 commit comments

Comments
 (0)