Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/cli/src/vite/vat-bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export async function bundleVat(sourcePath: string): Promise<VatBundle> {
const result = await build({
configFile: false,
logLevel: 'silent',
// Replace process.env references since they don't exist in SES vat environment
define: {
'process.env.NODE_ENV': JSON.stringify('production'),
},
build: {
write: false,
lib: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ export function makeKernelFacade(kernel: Kernel): KernelFacade {
// TODO: Enable custom CapTP marshalling tables to convert this to a presence
return { kref: krefString };
},

getSystemVatRoot: async (name: string) => {
const rootKref = kernel.getSystemVatRoot(name);
if (!rootKref) {
throw new Error(`System vat "${name}" not found`);
}
return { kref: rootKref };
},

reset: async () => {
return kernel.reset();
},
});
}
harden(makeKernelFacade);
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,21 @@ async function main(): Promise<void> {
makeSQLKernelDatabase({ dbFilename: DB_FILENAME }),
]);

// Set up console forwarding - messages flow through offscreen to background
setupConsoleForwarding({
source: 'kernel-worker',
onMessage: (message) => {
messageStream.write(message).catch(() => undefined);
},
});

const resetStorage =
new URLSearchParams(globalThis.location.search).get('reset-storage') ===
'true';
const urlParams = new URLSearchParams(globalThis.location.search);
const resetStorage = urlParams.get('reset-storage') === 'true';
const systemVatsParam = urlParams.get('system-vats');
const systemVats = systemVatsParam ? JSON.parse(systemVatsParam) : undefined;

const kernelP = Kernel.make(platformServicesClient, kernelDatabase, {
resetStorage,
systemVats,
});

const handlerP = kernelP.then((kernel) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/kernel-browser-runtime/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ export type KernelFacade = {
getStatus: Kernel['getStatus'];
pingVat: Kernel['pingVat'];
getVatRoot: (krefString: string) => Promise<unknown>;
getSystemVatRoot: (name: string) => Promise<{ kref: string }>;
reset: Kernel['reset'];
};
20 changes: 20 additions & 0 deletions packages/ocap-kernel/src/Kernel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,26 @@ describe('Kernel', () => {
expect(kernel.getVatIds()).toHaveLength(0);
});

it('clears system vat roots', async () => {
const mockDb = makeMapKernelDatabase();
const kernel = await Kernel.make(mockPlatformServices, mockDb, {
systemVats: [
{
name: 'testSystemVat',
sourceSpec: 'system-vat.js',
},
],
});
// Verify system vat root was stored
expect(kernel.getSystemVatRoot('testSystemVat')).toBeDefined();
expect(kernel.getSystemVatRoot('testSystemVat')).toMatch(/^ko\d+$/u);

await kernel.reset();

// Verify system vat roots are cleared
expect(kernel.getSystemVatRoot('testSystemVat')).toBeUndefined();
});

it('logs an error if resetting the kernel state fails', async () => {
const mockDb = makeMapKernelDatabase();
const logger = new Logger('test');
Expand Down
84 changes: 82 additions & 2 deletions packages/ocap-kernel/src/Kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { CapData } from '@endo/marshal';
import type { KernelDatabase } from '@metamask/kernel-store';
import { Logger } from '@metamask/logger';

import { makeKernelFacet } from './kernel-facet.ts';
import { KernelQueue } from './KernelQueue.ts';
import { KernelRouter } from './KernelRouter.ts';
import { KernelServiceManager } from './KernelServiceManager.ts';
Expand All @@ -23,6 +24,7 @@ import type {
Subcluster,
SubclusterLaunchResult,
EndpointHandle,
SystemVatConfig,
} from './types.ts';
import { isVatId, isRemoteId } from './types.ts';
import { SubclusterManager } from './vats/SubclusterManager.ts';
Expand All @@ -49,6 +51,9 @@ export class Kernel {
/** Manages subcluster operations */
readonly #subclusterManager: SubclusterManager;

/** Stores root krefs of launched system vats */
readonly #systemVatRoots: Map<string, KRef> = new Map();

/** Manages remote kernel connections */
readonly #remoteManager: RemoteManager;

Expand Down Expand Up @@ -190,6 +195,7 @@ export class Kernel {
* @param options.logger - Optional logger for error and diagnostic output.
* @param options.keySeed - Optional seed for libp2p key generation.
* @param options.mnemonic - Optional BIP39 mnemonic for deriving the kernel identity.
* @param options.systemVats - Optional array of system vat configurations to launch at init.
* @returns A promise for the new kernel instance.
*/
static async make(
Expand All @@ -200,17 +206,20 @@ export class Kernel {
logger?: Logger;
keySeed?: string | undefined;
mnemonic?: string | undefined;
systemVats?: SystemVatConfig[];
} = {},
): Promise<Kernel> {
const kernel = new Kernel(platformServices, kernelDatabase, options);
await kernel.#init();
await kernel.#init(options.systemVats);
return kernel;
}

/**
* Start the kernel running.
*
* @param systemVatConfigs - Optional array of system vat configurations to launch.
*/
async #init(): Promise<void> {
async #init(systemVatConfigs?: SystemVatConfig[]): Promise<void> {
// Set up the remote message handler
this.#remoteManager.setMessageHandler(
async (from: string, message: string) =>
Expand All @@ -223,6 +232,7 @@ export class Kernel {

// Start the kernel queue processing (non-blocking)
// This runs for the entire lifetime of the kernel
// Must start before launching system vats since launchSubcluster awaits bootstrap results
this.#kernelQueue
.run(this.#kernelRouter.deliver.bind(this.#kernelRouter))
.catch((error) => {
Expand All @@ -232,6 +242,65 @@ export class Kernel {
);
// Don't re-throw to avoid unhandled rejection in this long-running task
});

// Launch system vats after queue is running
if (systemVatConfigs && systemVatConfigs.length > 0) {
// Ensure kernel facet is registered before launching system vats
this.#registerKernelFacet();
await this.#launchSystemVats(systemVatConfigs);
}
}

/**
* Launch system vats from their configurations.
* If a system vat subcluster already exists (from a previous session),
* it will be terminated and relaunched to ensure a clean state.
*
* @param configs - Array of system vat configurations.
*/
async #launchSystemVats(configs: SystemVatConfig[]): Promise<void> {
for (const config of configs) {
const { name, services, ...vatConfig } = config;

// Terminate any existing subcluster with the same bootstrap name
const existingSubcluster = this.getSubclusters().find(
(sc) => sc.config.bootstrap === name,
);
if (existingSubcluster) {
this.#logger.info(
`Terminating existing system vat "${name}" for relaunch`,
);
await this.terminateSubcluster(existingSubcluster.id);
}

// Launch system vat
const clusterConfig: ClusterConfig = {
bootstrap: name,
vats: { [name]: vatConfig },
...(services && { services }),
};
const result = await this.launchSubcluster(clusterConfig);
this.#systemVatRoots.set(name, result.bootstrapRootKref);
this.#logger.info(`System vat "${name}" launched`);
}
}

/**
* Register the kernel facet as a kernel service for system vats.
*/
#registerKernelFacet(): void {
const kernelFacet = makeKernelFacet({
launchSubcluster: this.launchSubcluster.bind(this),
terminateSubcluster: this.terminateSubcluster.bind(this),
reloadSubcluster: this.reloadSubcluster.bind(this),
getSubcluster: this.getSubcluster.bind(this),
getSubclusters: this.getSubclusters.bind(this),
getStatus: this.getStatus.bind(this),
});
this.#kernelServiceManager.registerKernelServiceObject(
'kernelFacet',
kernelFacet,
);
}

/**
Expand Down Expand Up @@ -346,6 +415,16 @@ export class Kernel {
return this.#subclusterManager.getSubclusters();
}

/**
* Get the root kref of a system vat by name.
*
* @param name - The name of the system vat.
* @returns The root kref or undefined if not found.
*/
getSystemVatRoot(name: string): KRef | undefined {
return this.#systemVatRoots.get(name);
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

System vat roots not cleared on kernel reset

Medium Severity

The #systemVatRoots map is never cleared when reset() is called. After reset() terminates all vats (including system vats) and resets the kernel store, the #systemVatRoots map still contains stale krefs referencing terminated vats. Subsequent calls to getSystemVatRoot() return these invalid references, which would cause failures when attempting to use them.

Additional Locations (1)

Fix in Cursor Fix in Web

/**
* Checks if a vat belongs to a specific subcluster.
*
Expand Down Expand Up @@ -532,6 +611,7 @@ export class Kernel {
await this.#kernelQueue.waitForCrank();
try {
await this.terminateAllVats();
this.#systemVatRoots.clear();
this.#resetKernelState();
} catch (error) {
this.#logger.error('Error resetting kernel:', error);
Expand Down
18 changes: 6 additions & 12 deletions packages/ocap-kernel/src/KernelRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,7 @@ export class KernelRouter {
readonly #getEndpoint: (endpointId: EndpointId) => EndpointHandle;

/** A function that invokes a method on a kernel service. */
readonly #invokeKernelService: (
target: KRef,
message: Message,
) => Promise<void>;
readonly #invokeKernelService: (target: KRef, message: Message) => void;

/** The logger, if any. */
readonly #logger: Logger | undefined;
Expand All @@ -66,7 +63,7 @@ export class KernelRouter {
kernelStore: KernelStore,
kernelQueue: KernelQueue,
getEndpoint: (endpointId: EndpointId) => EndpointHandle,
invokeKernelService: (target: KRef, message: Message) => Promise<void>,
invokeKernelService: (target: KRef, message: Message) => void,
logger?: Logger,
) {
this.#kernelStore = kernelStore;
Expand Down Expand Up @@ -266,7 +263,7 @@ export class KernelRouter {
// Continue processing other messages - don't let one failure crash the queue
}
} else if (isKernelServiceMessage) {
crankResults = await this.#deliverKernelServiceMessage(target, message);
crankResults = this.#deliverKernelServiceMessage(target, message);
} else {
Fail`no owner for kernel object ${target}`;
}
Expand All @@ -286,13 +283,10 @@ export class KernelRouter {
*
* @param target - The kernel reference of the target service object.
* @param message - The message to deliver to the service.
* @returns A promise that resolves to the crank results indicating the delivery was to the kernel.
* @returns The crank results indicating the delivery was to the kernel.
*/
async #deliverKernelServiceMessage(
target: KRef,
message: Message,
): Promise<CrankResults> {
await this.#invokeKernelService(target, message);
#deliverKernelServiceMessage(target: KRef, message: Message): CrankResults {
this.#invokeKernelService(target, message);
return { didDelivery: 'kernel' };
}

Expand Down
Loading
Loading