Skip to content

Commit 9501597

Browse files
authored
fix: Restore Kernel hardening (#510)
This PR re-enables hardening for the Kernel class, which was previously disabled to facilitate testing. With the recent architectural improvements that split out components like KernelQueue, we can now properly test the Kernel without compromising security. The PR adopts a dependency-based mocking approach (properly mocking KernelQueue via vi.hoisted, injecting mock databases, etc.) instead of attempting to modify hardened objects directly. This ensures tests respect object capability security principles while still providing comprehensive coverage. Several new tests have been added for previously untested methods.
1 parent c3fd01c commit 9501597

File tree

3 files changed

+318
-43
lines changed

3 files changed

+318
-43
lines changed

packages/ocap-kernel/src/Kernel.test.ts

Lines changed: 308 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,26 @@ import type {
1717
import { VatHandle } from './VatHandle.ts';
1818
import { makeMapKernelDatabase } from '../test/storage.ts';
1919

20+
const mocks = vi.hoisted(() => {
21+
class KernelQueue {
22+
static lastInstance: KernelQueue;
23+
24+
enqueueMessage = vi
25+
.fn()
26+
.mockResolvedValue({ body: '{"result":"ok"}', slots: [] });
27+
28+
run = vi.fn().mockResolvedValue(undefined);
29+
30+
constructor() {
31+
(this.constructor as typeof KernelQueue).lastInstance = this;
32+
}
33+
}
34+
return { KernelQueue };
35+
});
36+
vi.mock('./KernelQueue.ts', () => {
37+
return { KernelQueue: mocks.KernelQueue };
38+
});
39+
2040
const makeMockVatConfig = (): VatConfig => ({
2141
sourceSpec: 'not-really-there.js',
2242
});
@@ -67,15 +87,16 @@ describe('Kernel', () => {
6787
vatHandles = [];
6888
makeVatHandleMock = vi
6989
.spyOn(VatHandle, 'make')
70-
.mockImplementation(async () => {
90+
.mockImplementation(async ({ vatId, vatConfig }) => {
7191
const vatHandle = {
92+
vatId,
93+
config: vatConfig,
7294
init: vi.fn(),
7395
terminate: vi.fn(),
7496
handleMessage: vi.fn(),
7597
deliverMessage: vi.fn(),
7698
deliverNotify: vi.fn(),
7799
sendVatCommand: vi.fn(),
78-
config: makeMockVatConfig(),
79100
} as unknown as VatHandle;
80101
vatHandles.push(vatHandle as Mocked<VatHandle>);
81102
return vatHandle;
@@ -84,6 +105,245 @@ describe('Kernel', () => {
84105
mockKernelDatabase = makeMapKernelDatabase();
85106
});
86107

108+
describe('constructor()', () => {
109+
it('initializes the kernel without errors', async () => {
110+
expect(
111+
async () =>
112+
await Kernel.make(mockStream, mockWorkerService, mockKernelDatabase),
113+
).not.toThrow();
114+
});
115+
116+
it('honors resetStorage option and clears persistent state', async () => {
117+
const db = makeMapKernelDatabase();
118+
db.kernelKVStore.set('foo', 'bar');
119+
// Create with resetStorage should clear existing keys
120+
await Kernel.make(mockStream, mockWorkerService, db, {
121+
resetStorage: true,
122+
});
123+
expect(db.kernelKVStore.get('foo')).toBeUndefined();
124+
});
125+
});
126+
127+
describe('init()', () => {
128+
it('initializes the kernel store', async () => {
129+
const kernel = await Kernel.make(
130+
mockStream,
131+
mockWorkerService,
132+
mockKernelDatabase,
133+
);
134+
await kernel.launchVat(makeMockVatConfig());
135+
expect(kernel.getVatIds()).toStrictEqual(['v1']);
136+
});
137+
138+
it('starts receiving messages', async () => {
139+
let drainHandler: ((message: JsonRpcRequest) => Promise<void>) | null =
140+
null;
141+
const customMockStream = {
142+
drain: async (handler: (message: JsonRpcRequest) => Promise<void>) => {
143+
drainHandler = handler;
144+
return Promise.resolve();
145+
},
146+
write: vi.fn().mockResolvedValue(undefined),
147+
} as unknown as DuplexStream<JsonRpcRequest, JsonRpcResponse>;
148+
await Kernel.make(
149+
customMockStream,
150+
mockWorkerService,
151+
mockKernelDatabase,
152+
);
153+
expect(drainHandler).toBeInstanceOf(Function);
154+
});
155+
156+
it('initializes and starts the kernel queue', async () => {
157+
await Kernel.make(mockStream, mockWorkerService, mockKernelDatabase);
158+
const queueInstance = mocks.KernelQueue.lastInstance;
159+
expect(queueInstance.run).toHaveBeenCalledTimes(1);
160+
});
161+
162+
it('throws if the stream throws', async () => {
163+
const streamError = new Error('Stream error');
164+
const throwingMockStream = {
165+
drain: () => {
166+
throw streamError;
167+
},
168+
write: vi.fn().mockResolvedValue(undefined),
169+
} as unknown as DuplexStream<JsonRpcRequest, JsonRpcResponse>;
170+
await expect(
171+
Kernel.make(throwingMockStream, mockWorkerService, mockKernelDatabase),
172+
).rejects.toThrow('Stream error');
173+
});
174+
175+
it('recovers vats from persistent storage on startup', async () => {
176+
const db = makeMapKernelDatabase();
177+
// Launch initial kernel and vat
178+
const kernel1 = await Kernel.make(mockStream, mockWorkerService, db);
179+
await kernel1.launchVat(makeMockVatConfig());
180+
expect(kernel1.getVatIds()).toStrictEqual(['v1']);
181+
// Clear spies
182+
launchWorkerMock.mockClear();
183+
makeVatHandleMock.mockClear();
184+
// New kernel should recover existing vat
185+
const kernel2 = await Kernel.make(mockStream, mockWorkerService, db);
186+
expect(launchWorkerMock).toHaveBeenCalledTimes(1);
187+
expect(makeVatHandleMock).toHaveBeenCalledTimes(1);
188+
expect(kernel2.getVatIds()).toStrictEqual(['v1']);
189+
});
190+
});
191+
192+
describe('reload()', () => {
193+
it('should reload with current config when config exists', async () => {
194+
const kernel = await Kernel.make(
195+
mockStream,
196+
mockWorkerService,
197+
mockKernelDatabase,
198+
);
199+
kernel.clusterConfig = makeMockClusterConfig();
200+
await kernel.launchVat(makeMockVatConfig());
201+
expect(kernel.getVatIds()).toStrictEqual(['v1']);
202+
await kernel.reload();
203+
// Verify the old vat was terminated
204+
expect(vatHandles[0]?.terminate).toHaveBeenCalledTimes(1);
205+
// Initial + reload
206+
expect(launchWorkerMock).toHaveBeenCalledTimes(2);
207+
});
208+
209+
it('should throw if no config exists', async () => {
210+
const kernel = await Kernel.make(
211+
mockStream,
212+
mockWorkerService,
213+
mockKernelDatabase,
214+
);
215+
await expect(kernel.reload()).rejects.toThrow('no subcluster to reload');
216+
});
217+
218+
it('should propagate errors from terminateAllVats', async () => {
219+
const kernel = await Kernel.make(
220+
mockStream,
221+
mockWorkerService,
222+
mockKernelDatabase,
223+
);
224+
kernel.clusterConfig = makeMockClusterConfig();
225+
// Set up a vat that will throw during termination
226+
await kernel.launchVat(makeMockVatConfig());
227+
vatHandles[0]?.terminate.mockRejectedValueOnce(
228+
new Error('Termination failed'),
229+
);
230+
await expect(kernel.reload()).rejects.toThrow('Termination failed');
231+
});
232+
});
233+
234+
describe('queueMessage()', () => {
235+
it('enqueues a message and returns the result', async () => {
236+
const kernel = await Kernel.make(
237+
mockStream,
238+
mockWorkerService,
239+
mockKernelDatabase,
240+
);
241+
await kernel.launchVat(makeMockVatConfig());
242+
const result = await kernel.queueMessage('ko1', 'hello', []);
243+
expect(result).toStrictEqual({ body: '{"result":"ok"}', slots: [] });
244+
});
245+
});
246+
247+
describe('launchSubcluster()', () => {
248+
it('launches a subcluster according to config', async () => {
249+
const kernel = await Kernel.make(
250+
mockStream,
251+
mockWorkerService,
252+
mockKernelDatabase,
253+
);
254+
const config = makeMockClusterConfig();
255+
await kernel.launchSubcluster(config);
256+
expect(launchWorkerMock).toHaveBeenCalled();
257+
expect(makeVatHandleMock).toHaveBeenCalled();
258+
expect(kernel.clusterConfig).toStrictEqual(config);
259+
});
260+
261+
it('throws an error for invalid configs', async () => {
262+
const kernel = await Kernel.make(
263+
mockStream,
264+
mockWorkerService,
265+
mockKernelDatabase,
266+
);
267+
// @ts-expect-error Intentionally passing invalid config
268+
await expect(kernel.launchSubcluster({})).rejects.toThrow(
269+
'invalid cluster config',
270+
);
271+
});
272+
273+
it('throws an error when bootstrap vat name is invalid', async () => {
274+
const kernel = await Kernel.make(
275+
mockStream,
276+
mockWorkerService,
277+
mockKernelDatabase,
278+
);
279+
const invalidConfig = {
280+
bootstrap: 'nonexistent',
281+
vats: {
282+
alice: {
283+
sourceSpec: 'test.js',
284+
},
285+
},
286+
};
287+
await expect(kernel.launchSubcluster(invalidConfig)).rejects.toThrow(
288+
'invalid bootstrap vat name',
289+
);
290+
});
291+
292+
it('returns the bootstrap message result when bootstrap vat is specified', async () => {
293+
const kernel = await Kernel.make(
294+
mockStream,
295+
mockWorkerService,
296+
mockKernelDatabase,
297+
);
298+
const config = makeMockClusterConfig();
299+
const result = await kernel.launchSubcluster(config);
300+
expect(result).toStrictEqual({ body: '{"result":"ok"}', slots: [] });
301+
});
302+
});
303+
304+
describe('clearStorage()', () => {
305+
it('clears the kernel storage', async () => {
306+
const kernel = await Kernel.make(
307+
mockStream,
308+
mockWorkerService,
309+
mockKernelDatabase,
310+
);
311+
const clearSpy = vi.spyOn(mockKernelDatabase, 'clear');
312+
await kernel.clearStorage();
313+
expect(clearSpy).toHaveBeenCalledOnce();
314+
});
315+
});
316+
317+
describe('getVats()', () => {
318+
it('returns an empty array when no vats are added', async () => {
319+
const kernel = await Kernel.make(
320+
mockStream,
321+
mockWorkerService,
322+
mockKernelDatabase,
323+
);
324+
expect(kernel.getVats()).toStrictEqual([]);
325+
});
326+
327+
it('returns vat information after adding vats', async () => {
328+
const kernel = await Kernel.make(
329+
mockStream,
330+
mockWorkerService,
331+
mockKernelDatabase,
332+
);
333+
const config = makeMockVatConfig();
334+
await kernel.launchVat(config);
335+
const vats = kernel.getVats();
336+
expect(vats).toHaveLength(1);
337+
console.log(vats);
338+
expect(vats).toStrictEqual([
339+
{
340+
id: 'v1',
341+
config,
342+
},
343+
]);
344+
});
345+
});
346+
87347
describe('getVatIds()', () => {
88348
it('returns an empty array when no vats are added', async () => {
89349
const kernel = await Kernel.make(
@@ -289,59 +549,75 @@ describe('Kernel', () => {
289549
expect(vatHandles[0]?.terminate).toHaveBeenCalledOnce();
290550
expect(kernel.getVatIds()).toStrictEqual([]);
291551
});
292-
});
293552

294-
describe('constructor()', () => {
295-
it('initializes the kernel without errors', async () => {
296-
expect(
297-
async () =>
298-
await Kernel.make(mockStream, mockWorkerService, mockKernelDatabase),
299-
).not.toThrow();
553+
it('returns the existing VatHandle instance on restart', async () => {
554+
const kernel = await Kernel.make(
555+
mockStream,
556+
mockWorkerService,
557+
mockKernelDatabase,
558+
);
559+
await kernel.launchVat(makeMockVatConfig());
560+
const originalHandle = vatHandles[0];
561+
const returnedHandle = await kernel.restartVat('v1');
562+
expect(returnedHandle).toBe(originalHandle);
300563
});
301564
});
302565

303-
describe('init()', () => {
304-
it.todo('initializes the kernel store');
305-
it.todo('starts receiving messages');
306-
it.todo('throws if the stream throws');
307-
});
308-
309-
describe('reload()', () => {
310-
it('should reload with current config when config exists', async () => {
566+
describe('clusterConfig', () => {
567+
it('gets and sets cluster configuration', async () => {
311568
const kernel = await Kernel.make(
312569
mockStream,
313570
mockWorkerService,
314571
mockKernelDatabase,
315572
);
316-
kernel.clusterConfig = makeMockClusterConfig();
317-
await kernel.launchVat(makeMockVatConfig());
318-
const launchSubclusterMock = vi
319-
.spyOn(kernel, 'launchSubcluster')
320-
.mockResolvedValueOnce(undefined);
321-
await kernel.reload();
322-
expect(vatHandles[0]?.terminate).toHaveBeenCalledTimes(1);
323-
expect(launchSubclusterMock).toHaveBeenCalledOnce();
573+
expect(kernel.clusterConfig).toBeNull();
574+
const config = makeMockClusterConfig();
575+
kernel.clusterConfig = config;
576+
expect(kernel.clusterConfig).toStrictEqual(config);
324577
});
325578

326-
it('should throw if no config exists', async () => {
579+
it('throws an error when setting invalid config', async () => {
327580
const kernel = await Kernel.make(
328581
mockStream,
329582
mockWorkerService,
330583
mockKernelDatabase,
331584
);
332-
await expect(kernel.reload()).rejects.toThrow('no subcluster to reload');
585+
expect(() => {
586+
// @ts-expect-error Intentionally setting invalid config
587+
kernel.clusterConfig = { invalid: true };
588+
}).toThrow('invalid cluster config');
333589
});
590+
});
334591

335-
it('should propagate errors from terminateAllVats', async () => {
592+
describe('reset()', () => {
593+
it('terminates all vats and resets kernel state', async () => {
594+
const mockDb = makeMapKernelDatabase();
595+
const clearSpy = vi.spyOn(mockDb, 'clear');
596+
const kernel = await Kernel.make(mockStream, mockWorkerService, mockDb);
597+
await kernel.launchVat(makeMockVatConfig());
598+
await kernel.reset();
599+
expect(clearSpy).toHaveBeenCalled();
600+
expect(kernel.getVatIds()).toHaveLength(0);
601+
});
602+
});
603+
604+
describe('pinVatRoot and unpinVatRoot', () => {
605+
it('pins and unpins a vat root correctly', async () => {
336606
const kernel = await Kernel.make(
337607
mockStream,
338608
mockWorkerService,
339609
mockKernelDatabase,
340610
);
341-
kernel.clusterConfig = makeMockClusterConfig();
342-
const error = new Error('Termination failed');
343-
vi.spyOn(kernel, 'terminateAllVats').mockRejectedValueOnce(error);
344-
await expect(kernel.reload()).rejects.toThrow(error);
611+
const config = makeMockVatConfig();
612+
const rootRef = await kernel.launchVat(config);
613+
// Pinning existing vat root should return the kref
614+
expect(kernel.pinVatRoot('v1')).toBe(rootRef);
615+
// Pinning non-existent vat should throw
616+
expect(() => kernel.pinVatRoot('v2')).toThrow(VatNotFoundError);
617+
// Unpinning existing vat root should succeed
618+
expect(() => kernel.unpinVatRoot('v1')).not.toThrow();
619+
// Unpinning non-existent vat should throw
620+
expect(() => kernel.unpinVatRoot('v3')).toThrow(VatNotFoundError);
345621
});
346622
});
347623
});

0 commit comments

Comments
 (0)