Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
33 changes: 19 additions & 14 deletions packages/kernel-test/src/endowment-globals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '@metamask/logger';
import type { LogEntry } from '@metamask/logger';
import { Kernel } from '@metamask/ocap-kernel';
import type { KRef, VatId } from '@metamask/ocap-kernel';
import type { AllowedGlobalName, KRef, VatId } from '@metamask/ocap-kernel';
import { getWorkerFile } from '@ocap/nodejs-test-workers';
import { describe, expect, it } from 'vitest';

Expand All @@ -22,8 +22,8 @@ describe('global endowments', () => {
globals,
allowedGlobalNames,
}: {
globals: string[];
allowedGlobalNames?: string[];
globals: AllowedGlobalName[];
allowedGlobalNames?: AllowedGlobalName[];
}) => {
const entries: LogEntry[] = [];
const logger = new Logger({
Expand Down Expand Up @@ -248,17 +248,22 @@ describe('global endowments', () => {
).rejects.toThrow('unknown global "TextEncoder"');
});

it('silently drops unknown names in allowedGlobalNames without affecting valid ones', async () => {
const { kernel, entries } = await setup({
globals: ['TextEncoder', 'TextDecoder'],
allowedGlobalNames: ['TextEncoder', 'TextDecoder', 'NotARealGlobal'],
});

await kernel.queueMessage(v1Root, 'testTextCodec', []);
await waitUntilQuiescent();

const logs = extractTestLogs(entries, vatId);
expect(logs).toContain('textCodec: hello');
it('rejects unknown names in allowedGlobalNames at the RPC boundary', async () => {
// Callers on the typed API get a compile-time error. This test covers
// the runtime check: the `initVat` RPC struct (`AllowedGlobalNameStruct`)
// rejects any name outside the literal union, so a caller that bypasses
// the type system (e.g., JS client, cast) still cannot smuggle bad names
// through.
await expect(
setup({
globals: ['TextEncoder', 'TextDecoder'],
allowedGlobalNames: [
'TextEncoder',
'TextDecoder',
'NotARealGlobal' as AllowedGlobalName,
],
}),
).rejects.toThrow(/Invalid params/u);
});
});
});
5 changes: 5 additions & 0 deletions packages/ocap-kernel/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **BREAKING:** Type `VatConfig.globals` and `Kernel.make`'s `allowedGlobalNames` as `AllowedGlobalName[]` (a literal union) instead of `string[]`; unknown names are now rejected at the `initVat` RPC boundary ([#941](https://github.com/MetaMask/ocap-kernel/pull/941))
- Exports: `AllowedGlobalName`, `AllowedGlobalNameStruct`, `MakeAllowedGlobals`, `VatEndowmentsStruct`

### Added

- Integrate Snaps attenuated endowment factories into vat globals ([#937](https://github.com/MetaMask/ocap-kernel/pull/937))
Expand Down
5 changes: 3 additions & 2 deletions packages/ocap-kernel/src/Kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import type {
SystemSubclusterConfig,
} from './types.ts';
import { isVatId, isRemoteId } from './types.ts';
import type { AllowedGlobalName } from './vats/endowments.ts';
import { SubclusterManager } from './vats/SubclusterManager.ts';
import type { VatHandle } from './vats/VatHandle.ts';
import { VatManager } from './vats/VatManager.ts';
Expand Down Expand Up @@ -115,7 +116,7 @@ export class Kernel {
keySeed?: string | undefined;
mnemonic?: string | undefined;
ioChannelFactory?: IOChannelFactory;
allowedGlobalNames?: string[];
allowedGlobalNames?: AllowedGlobalName[];
} = {},
) {
this.#platformServices = platformServices;
Expand Down Expand Up @@ -245,7 +246,7 @@ export class Kernel {
mnemonic?: string | undefined;
ioChannelFactory?: IOChannelFactory;
systemSubclusters?: SystemSubclusterConfig[];
allowedGlobalNames?: string[];
allowedGlobalNames?: AllowedGlobalName[];
} = {},
): Promise<Kernel> {
const kernel = new Kernel(platformServices, kernelDatabase, options);
Expand Down
1 change: 1 addition & 0 deletions packages/ocap-kernel/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as indexModule from './index.ts';
describe('index', () => {
it('has the expected exports', () => {
expect(Object.keys(indexModule).sort()).toStrictEqual([
'AllowedGlobalNameStruct',
'CapDataStruct',
'ClusterConfigStruct',
'Kernel',
Expand Down
7 changes: 6 additions & 1 deletion packages/ocap-kernel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ export { Kernel } from './Kernel.ts';
export { VatHandle } from './vats/VatHandle.ts';
export { VatSupervisor } from './vats/VatSupervisor.ts';
export { createDefaultEndowments } from './vats/endowments.ts';
export type { VatEndowments } from './vats/endowments.ts';
export type {
AllowedGlobalName,
MakeAllowedGlobals,
VatEndowments,
} from './vats/endowments.ts';
export { initTransport } from './remotes/platform/transport.ts';
export type { IOChannel, IOChannelFactory } from './io/types.ts';
export type {
Expand Down Expand Up @@ -46,6 +50,7 @@ export {
KernelStatusStruct,
SubclusterStruct,
} from './types.ts';
export { AllowedGlobalNameStruct } from './vats/endowments.ts';
export { kunser, kser, kslot, krefOf } from './liveslots/kernel-marshal.ts';
export type { SlotValue } from './liveslots/kernel-marshal.ts';
export type { KernelFacet } from './kernel-facet.ts';
Expand Down
6 changes: 4 additions & 2 deletions packages/ocap-kernel/src/rpc/vat/initVat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import type { Infer } from '@metamask/superstruct';
import { VatDeliveryResultStruct } from './shared.ts';
import { VatConfigStruct } from '../../types.ts';
import type { VatConfig, VatDeliveryResult } from '../../types.ts';
import { AllowedGlobalNameStruct } from '../../vats/endowments.ts';
import type { AllowedGlobalName } from '../../vats/endowments.ts';

const paramsStruct = object({
vatConfig: VatConfigStruct,
state: array(tuple([string(), string()])),
allowedGlobalNames: exactOptional(array(string())),
allowedGlobalNames: exactOptional(array(AllowedGlobalNameStruct)),
});

type Params = Infer<typeof paramsStruct>;
Expand All @@ -35,7 +37,7 @@ export const initVatSpec: InitVatSpec = {
export type InitVat = (
vatConfig: VatConfig,
state: Map<string, string>,
allowedGlobalNames: string[] | undefined,
allowedGlobalNames: AllowedGlobalName[] | undefined,
) => Promise<VatDeliveryResult>;

type InitVatHooks = {
Expand Down
6 changes: 4 additions & 2 deletions packages/ocap-kernel/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import type {
RemoteCommsOptions,
} from './remotes/types.ts';
import { Fail } from './utils/assert.ts';
import { AllowedGlobalNameStruct } from './vats/endowments.ts';
import type { AllowedGlobalName } from './vats/endowments.ts';

/**
* # Branded types
Expand Down Expand Up @@ -588,7 +590,7 @@ export type VatConfig = UserCodeSpec & {
creationOptions?: Record<string, Json>;
parameters?: Record<string, Json>;
platformConfig?: Partial<PlatformConfig>;
globals?: string[];
globals?: AllowedGlobalName[];
};

const UserCodeSpecStruct = union([
Expand Down Expand Up @@ -618,7 +620,7 @@ export const VatConfigStruct = define<VatConfig>('VatConfig', (value) => {
(!creationOptions || is(creationOptions, UnsafeJsonStruct)) &&
(!parameters || is(parameters, UnsafeJsonStruct)) &&
(!platformConfig || is(platformConfig, platformConfigStruct)) &&
(!globals || is(globals, array(string())))
(!globals || is(globals, array(AllowedGlobalNameStruct)))
);
});

Expand Down
5 changes: 3 additions & 2 deletions packages/ocap-kernel/src/vats/VatHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
VatDeliveryResult,
EndpointHandle,
} from '../types.ts';
import type { AllowedGlobalName } from './endowments.ts';
import { VatSyscall } from './VatSyscall.ts';

type MessageFromVat = JsonRpcResponse | JsonRpcNotification;
Expand All @@ -45,7 +46,7 @@ type VatConstructorProps = {
kernelStore: KernelStore;
kernelQueue: KernelQueue;
logger?: Logger | undefined;
allowedGlobalNames?: string[] | undefined;
allowedGlobalNames?: AllowedGlobalName[] | undefined;
};

/**
Expand All @@ -65,7 +66,7 @@ export class VatHandle implements EndpointHandle {
readonly #logger: Logger | undefined;

/** Optional list of allowed global names for vat endowments */
readonly #allowedGlobalNames: string[] | undefined;
readonly #allowedGlobalNames: AllowedGlobalName[] | undefined;

/** Storage holding the kernel's persistent state */
readonly #kernelStore: KernelStore;
Expand Down
5 changes: 3 additions & 2 deletions packages/ocap-kernel/src/vats/VatManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
PlatformServices,
} from '../types.ts';
import { ROOT_OBJECT_VREF } from '../types.ts';
import type { AllowedGlobalName } from './endowments.ts';
import { VatHandle } from './VatHandle.ts';
import type { PingVatResult } from '../rpc/index.ts';

Expand All @@ -25,7 +26,7 @@ type VatManagerOptions = {
kernelStore: KernelStore;
kernelQueue: KernelQueue;
logger?: Logger;
allowedGlobalNames?: string[] | undefined;
allowedGlobalNames?: AllowedGlobalName[] | undefined;
};

/**
Expand All @@ -48,7 +49,7 @@ export class VatManager {
readonly #logger: Logger;

/** Optional list of allowed global names for vat endowments */
readonly #allowedGlobalNames: string[] | undefined;
readonly #allowedGlobalNames: AllowedGlobalName[] | undefined;

/**
* Creates a new VatManager instance.
Expand Down
7 changes: 5 additions & 2 deletions packages/ocap-kernel/src/vats/VatSupervisor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,10 @@ describe('VatSupervisor', () => {
expect(factory).toHaveBeenCalledTimes(1);
});

it('throws when a vat requests an unknown global', async () => {
it('rejects an unknown global at the initVat RPC boundary', async () => {
// VatConfig.globals is now typed as AllowedGlobalName[] and validated by
// AllowedGlobalNameStruct at the RPC boundary, so an unknown name is
// rejected before reaching the VatSupervisor's per-name check.
const dispatch = vi.fn();

const mockFetchBlob: FetchBlob = vi.fn().mockResolvedValue({
Expand Down Expand Up @@ -323,7 +326,7 @@ describe('VatSupervisor', () => {
expect.objectContaining({
id: 'test-init',
error: expect.objectContaining({
message: expect.stringContaining('unknown global "UnknownThing"'),
message: expect.stringContaining('Invalid params'),
}),
}),
);
Expand Down
25 changes: 17 additions & 8 deletions packages/ocap-kernel/src/vats/VatSupervisor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,16 @@ import type { JsonRpcMessage } from '@metamask/kernel-utils';
import type { Logger } from '@metamask/logger';
import { serializeError } from '@metamask/rpc-errors';
import type { DuplexStream } from '@metamask/streams';
import { assert } from '@metamask/superstruct';
import {
hasProperty,
isJsonRpcRequest,
isJsonRpcResponse,
} from '@metamask/utils';

import { loadBundle } from './bundle-loader.ts';
import { createDefaultEndowments } from './endowments.ts';
import type { VatEndowments } from './endowments.ts';
import { createDefaultEndowments, VatEndowmentsStruct } from './endowments.ts';
import type { AllowedGlobalName, MakeAllowedGlobals } from './endowments.ts';
import { makeGCAndFinalize } from '../garbage-collection/gc-finalize.ts';
import { makeDummyMeterControl } from '../liveslots/meter-control.ts';
import { makeSupervisorSyscall } from '../liveslots/syscall.ts';
Expand Down Expand Up @@ -60,7 +61,7 @@ type SupervisorConstructorProps = {
platformOptions?: Record<string, unknown>;
vatPowers?: Record<string, unknown> | undefined;
fetchBlob?: FetchBlob;
makeAllowedGlobals?: () => VatEndowments;
makeAllowedGlobals?: MakeAllowedGlobals;
};

const marshal = makeMarshal(undefined, undefined, {
Expand Down Expand Up @@ -162,10 +163,18 @@ export class VatSupervisor {
this.#fetchBlob = fetchBlob ?? defaultFetchBlob;
this.#platformOptions = platformOptions ?? {};
this.#makePlatform = makePlatform;
const { globals, teardown } = makeAllowedGlobals();
// Defense in depth: custom `makeAllowedGlobals` factories may skip hardening.
this.#allowedGlobals = harden(globals);
this.#endowmentsTeardown = teardown;
const endowments = makeAllowedGlobals();
// Defense in depth: custom `MakeAllowedGlobals` factories may return the
// wrong shape (e.g., no `teardown` callable) — assert before use so the
// failure surfaces at construction rather than during termination.
assert(
endowments,
VatEndowmentsStruct,
`makeAllowedGlobals returned an invalid VatEndowments value for vat "${id}"`,
);
// Defense in depth: custom `MakeAllowedGlobals` factories may skip hardening.
this.#allowedGlobals = harden(endowments.globals);
this.#endowmentsTeardown = endowments.teardown;

this.#rpcClient = new RpcClient(
vatSyscallMethodSpecs,
Expand Down Expand Up @@ -323,7 +332,7 @@ export class VatSupervisor {
async #initVat(
vatConfig: VatConfig,
state: Map<string, string>,
allowedGlobalNames: string[] | undefined,
allowedGlobalNames: AllowedGlobalName[] | undefined,
): Promise<VatDeliveryResult> {
if (this.#loaded) {
throw Error(
Expand Down
53 changes: 49 additions & 4 deletions packages/ocap-kernel/src/vats/endowments.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { buildCommonEndowments } from '@metamask/snaps-execution-environments/endowments';
import {
enums,
func,
object,
record,
string,
unknown,
} from '@metamask/superstruct';

/**
* The names of host/Web API globals that vats may request as endowments.
Expand All @@ -17,7 +25,7 @@ import { buildCommonEndowments } from '@metamask/snaps-execution-environments/en
* requires adjusting {@link createDefaultEndowments} to pass the right
* options through — see ocap-kernel issue #936 for the network case.
*/
const ALLOWED_GLOBAL_NAMES = new Set<string>([
const ALLOWED_GLOBAL_NAMES = [
// Attenuated timer factories — isolated per vat, with teardown for
// cancelling pending callbacks on termination.
'setTimeout',
Expand Down Expand Up @@ -50,7 +58,20 @@ const ALLOWED_GLOBAL_NAMES = new Set<string>([
'btoa',
'AbortController',
'AbortSignal',
]);
] as const;

/**
* A global name that vats may request as an endowment. Callers that accept
* this type get typo-checking at compile time, and the {@link AllowedGlobalNameStruct}
* enforces the same invariant at RPC boundaries.
*/
export type AllowedGlobalName = (typeof ALLOWED_GLOBAL_NAMES)[number];

export const AllowedGlobalNameStruct = enums(ALLOWED_GLOBAL_NAMES);

const ALLOWED_GLOBAL_NAMES_SET: ReadonlySet<string> = new Set(
ALLOWED_GLOBAL_NAMES,
);

/**
* The endowments produced for a single vat.
Expand All @@ -69,6 +90,30 @@ export type VatEndowments = {
teardown: () => Promise<void>;
};

/**
* Shape-only validator used to guard the `VatSupervisor` boundary against
* custom `MakeAllowedGlobals` factories returning malformed values. It checks
* that `globals` is a record and `teardown` is a function; it does not and
* cannot verify that `teardown` returns a promise.
*
* The `globals` key is validated as `string` rather than {@link AllowedGlobalNameStruct}
* so factories may surface extras from upstream sources (e.g., Snaps'
* `buildCommonEndowments`) without tripping the assertion. Extras are dropped
* when a vat's config is resolved — only names in {@link ALLOWED_GLOBAL_NAMES}
* can actually be requested.
*/
export const VatEndowmentsStruct = object({
globals: record(string(), unknown()),
teardown: func(),
});

/**
* Factory that produces a fresh {@link VatEndowments} for a single vat.
* Consumers supply this to a `VatSupervisor` to override the default
* endowment set (see {@link createDefaultEndowments}).
*/
export type MakeAllowedGlobals = () => VatEndowments;

/**
* Build a fresh set of vat endowments from the Snaps attenuated factories,
* filtered to the names in {@link ALLOWED_GLOBAL_NAMES}. Each call produces
Expand All @@ -91,7 +136,7 @@ export function createDefaultEndowments(): VatEndowments {
const teardowns: (() => Promise<void> | void)[] = [];

for (const { names, factory } of buildCommonEndowments()) {
if (!names.some((name) => ALLOWED_GLOBAL_NAMES.has(name))) {
if (!names.some((name) => ALLOWED_GLOBAL_NAMES_SET.has(name))) {
continue;
}
let result;
Expand All @@ -106,7 +151,7 @@ export function createDefaultEndowments(): VatEndowments {
}
const { teardownFunction, ...values } = result;
for (const [key, value] of Object.entries(values)) {
if (ALLOWED_GLOBAL_NAMES.has(key)) {
if (ALLOWED_GLOBAL_NAMES_SET.has(key)) {
globals[key] = value;
}
}
Expand Down
Loading