Skip to content
Merged
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
11 changes: 11 additions & 0 deletions docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ A kernel-space component that manages the lifecycle and communication of a [vat]
The [VatSupervisor](../packages/ocap-kernel/src/VatSupervisor.ts) handles [message
delivery](#delivery), [syscalls](#syscall), and vat initialization.

### endowment

A host/Web API value — `setTimeout`, `Date`, `crypto`, `URL`, etc. — that is installed in
a [vat's](#vat) SES Compartment at initialization. Compartments do not expose these
APIs by default; vats request them by name via the `globals` field in their `VatConfig`.
Many endowments are _attenuated_ for security or isolation (e.g., per-vat timer queues
so one vat cannot clear another's timers, monotonically-clamped `Date.now()`) — they
are callable but deliberately differ from host semantics. See [Vat
Endowments](../docs/kernel-guide.md#vat-endowments) and the default factory in
[endowments.ts](../packages/ocap-kernel/src/vats/endowments.ts).

### liveslots

A framework for managing object lifecycles within [vats](#vat). Liveslots provides the
Expand Down
102 changes: 94 additions & 8 deletions docs/kernel-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ This document explains the parts of the ocap kernel that are relevant to buildin
1. [Core Concepts](#core-concepts)
2. [The Kernel API](#the-kernel-api)
3. [Writing Vat Code](#writing-vat-code)
4. [Kernel Services](#kernel-services)
5. [System Subclusters](#system-subclusters)
6. [Eventual Send with E()](#eventual-send-with-e)
7. [Exos (Remotable Objects)](#exos-remotable-objects)
8. [Baggage (Persistent State)](#baggage-persistent-state)
9. [Revocation](#revocation)
10. [Glossary and Key Types](#glossary-and-key-types)
4. [Vat Endowments](#vat-endowments)
5. [Kernel Services](#kernel-services)
6. [System Subclusters](#system-subclusters)
7. [Eventual Send with E()](#eventual-send-with-e)
8. [Exos (Remotable Objects)](#exos-remotable-objects)
9. [Baggage (Persistent State)](#baggage-persistent-state)
10. [Revocation](#revocation)
11. [Glossary and Key Types](#glossary-and-key-types)

---

Expand Down Expand Up @@ -127,6 +128,91 @@ After a vat restart (resuscitation), `bootstrap` is **not** called again. The va

---

## Vat Endowments

SES Compartments do not expose host or Web APIs by default — `setTimeout`, `Date.now()`, `crypto`, `Math.random()`, `URL`, and friends are either absent or deliberately tamed (e.g., `Date.now()` and `Math.random()` throw in secure mode). Vats that need them must request them explicitly through the `globals` field on their `VatConfig`.

### Requesting endowments from a vat

Name the host/Web APIs the vat needs in its cluster config:

```ts
await kernel.launchSubcluster({
bootstrap: 'worker',
vats: {
worker: {
bundleSpec: '...',
parameters: {},
globals: ['setTimeout', 'clearTimeout', 'Date', 'crypto', 'URL'],
},
},
});
```

Only names in the vat's `globals` array are installed in the vat's compartment. Names not in the kernel's allowed set cause `initVat` to fail with `Vat "<id>" requested unknown global "<name>"`.

### Default allowed globals

The kernel ships with the following set, sourced from `@metamask/snaps-execution-environments`:

| Name | Category | Notes |
| ----------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `setTimeout` | Timer (attenuated) | Isolated per vat. Cancelled automatically on vat termination. |
| `clearTimeout` | Timer (attenuated) | Only clears timers created by the same vat. |
| `setInterval` | Timer (attenuated) | Isolated per vat. Cancelled automatically on vat termination. |
| `clearInterval` | Timer (attenuated) | Only clears intervals created by the same vat. |
| `Date` | Attenuated | Each `Date.now()` read adds up to 1 ms of random jitter, clamped monotonic non-decreasing; precise sub-millisecond timing cannot leak through. |
| `Math` | Attenuated | `Math.random()` is sourced from `crypto.getRandomValues`. **Not a CSPRNG** per the upstream NOTE — defends against stock-RNG timing side channels only. |
| `crypto` | Web Crypto | Hardened Web Crypto API. |
| `SubtleCrypto` | Web Crypto | Hardened Web Crypto API. |
| `TextEncoder` | Text codec | Plain hardened. |
| `TextDecoder` | Text codec | Plain hardened. |
| `URL` | URL | Plain hardened. |
| `URLSearchParams` | URL | Plain hardened. |
| `atob` | Base64 | Plain hardened. |
| `btoa` | Base64 | Plain hardened. |
| `AbortController` | Abort | Plain hardened. |
| `AbortSignal` | Abort | Plain hardened. |

"Plain hardened" means the value is the host's implementation wrapped with `harden()` — it behaves identically to the browser/Node version. "Attenuated" means the value is a deliberate reimplementation with different semantics; the Notes column flags the relevant differences. The canonical list lives in [`endowments.ts`](../packages/ocap-kernel/src/vats/endowments.ts).

### Restricting or replacing the allowed set

Two levers, applied at different layers:

**1. Kernel-level allowlist via `Kernel.make({ allowedGlobalNames })`.** Restrict the set of globals any vat is permitted to request. Names outside this list cause `initVat` to fail even if the vat asks for them.

```ts
const kernel = await Kernel.make(platformServices, db, {
allowedGlobalNames: ['TextEncoder', 'TextDecoder', 'URL'],
});
```

Omit the option to allow the full default set.

**2. Supervisor-level factory via `VatSupervisor({ makeAllowedGlobals })`.** Replace the endowment factory entirely — useful for tests or host applications that need custom attenuations. The factory is invoked once per supervisor and must return `{ globals, teardown }`, where `teardown` releases any resources the custom endowments hold.

```ts
import type { VatEndowments } from '@metamask/ocap-kernel';

const makeAllowedGlobals = (): VatEndowments =>
harden({
globals: harden({ Date: globalThis.Date }),
teardown: async () => undefined,
});

new VatSupervisor({
id,
kernelStream,
logger,
makeAllowedGlobals,
});
```

Most host applications should stick with the default `createDefaultEndowments()` factory; override only when you need bespoke attenuations.

---

## Kernel Services

A kernel service is a JavaScript object registered with the kernel. Vats interact with it via `E()` just like any other remote object, but it runs in the kernel's own context (not in a vat).
Expand Down Expand Up @@ -451,7 +537,7 @@ type VatConfig = {
creationOptions?: Record<string, Json>; // Options for vat creation
parameters?: Record<string, Json>; // Static parameters passed to buildRootObject
platformConfig?: Partial<PlatformConfig>; // Platform-specific configuration
globals?: string[]; // Additional globals to allow in the vat
globals?: string[]; // Host/Web API globals the vat requests — see [Vat Endowments](#vat-endowments)
};

// Configuration for a system subcluster
Expand Down
16 changes: 16 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,22 @@ The `bundleSpec` can be:
- A file path for Node.js (e.g., `file:///path/to/sample-vat.bundle`)
- A data URL containing the bundle content

A vat can also request host/Web API globals (timers, `Date`, `crypto`, `URL`, …) via the `globals` field. SES Compartments do not expose these by default, so anything the vat needs must be named explicitly:

```json
{
"bootstrap": "alice",
"vats": {
"alice": {
"bundleSpec": "http://localhost:3000/sample-vat.bundle",
"globals": ["setTimeout", "clearTimeout", "Date", "crypto"]
}
}
}
```

See [Vat Endowments](./kernel-guide.md#vat-endowments) in the kernel guide for the full list and for how to narrow the set with `Kernel.make({ allowedGlobalNames })`.

## Kernel API

The kernel exposes several methods for managing vats and sending messages:
Expand Down
13 changes: 11 additions & 2 deletions packages/kernel-node-runtime/test/e2e/remote-comms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,13 +407,22 @@ describe.sequential('Remote Communications E2E', () => {
it(
'handles reconnection with exponential backoff',
async () => {
// ackTimeoutMs must be long enough for kernel2 to restart + reconnect
// before Alice's URL redemption gives up (fires after
// ackTimeoutMs × (MAX_RETRIES + 1)). See PR #927 for the same fix
// applied to sibling tests with this pattern.
const reconnectOptions = {
...testBackoffOptions,
ackTimeoutMs: 5_000,
};

const { aliceRef, bobURL } = await setupAliceAndBob(
kernel1,
kernel2,
kernelStore1,
kernelStore2,
testRelays,
testBackoffOptions,
reconnectOptions,
);

const initialMessage = await sendRemoteMessage(
Expand Down Expand Up @@ -445,7 +454,7 @@ describe.sequential('Remote Communications E2E', () => {
false,
testRelays,
bobConfig,
testBackoffOptions,
reconnectOptions,
);
// eslint-disable-next-line require-atomic-updates
kernel2 = restartResult.kernel;
Expand Down
68 changes: 68 additions & 0 deletions packages/kernel-test/src/endowment-globals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ describe('global endowments', () => {
expect(logs).toContain('timer: fired');
});

it('can use setInterval and clearInterval', async () => {
const { kernel, entries } = await setup({
globals: ['setInterval', 'clearInterval'],
});

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

const logs = extractTestLogs(entries, vatId);
expect(logs).toContain('interval: ticks=2');
});

it('can use real Date (not tamed)', async () => {
const { kernel, entries } = await setup({ globals: ['Date'] });

Expand All @@ -123,6 +135,28 @@ describe('global endowments', () => {
expect(logs).toContain('date: isReal=true');
});

it('can use crypto.getRandomValues', async () => {
const { kernel, entries } = await setup({
globals: ['crypto', 'SubtleCrypto'],
});

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

const logs = extractTestLogs(entries, vatId);
expect(logs).toContain('crypto: hasRandomBytes=true');
});

it('can use Math.random sourced from crypto.getRandomValues', async () => {
const { kernel, entries } = await setup({ globals: ['Math'] });

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

const logs = extractTestLogs(entries, vatId);
expect(logs).toContain('math: inRange=true');
});

describe('host APIs are absent when not endowed', () => {
// These are Web/host APIs that are NOT JS intrinsics — they should
// be genuinely absent from a SES compartment unless explicitly endowed.
Expand All @@ -137,6 +171,10 @@ describe('global endowments', () => {
'AbortSignal',
'setTimeout',
'clearTimeout',
'setInterval',
'clearInterval',
'crypto',
'SubtleCrypto',
])('does not have %s without endowing it', async (name) => {
// Launch with no globals at all
const { kernel, entries } = await setup({ globals: [] });
Expand All @@ -155,6 +193,14 @@ describe('global endowments', () => {
'secure mode',
);
});

it('throws when calling tamed Math.random without endowing Math', async () => {
const { kernel } = await setup({ globals: [] });

await expect(kernel.queueMessage(v1Root, 'testMath', [])).rejects.toThrow(
'secure mode',
);
});
});

describe('kernel-level allowedGlobalNames restriction', () => {
Expand Down Expand Up @@ -192,5 +238,27 @@ describe('global endowments', () => {
const logs = extractTestLogs(entries, vatId);
expect(logs).toContain('url: /path params: 10');
});

it('rejects every vat global when allowedGlobalNames is empty', async () => {
await expect(
setup({
globals: ['TextEncoder'],
allowedGlobalNames: [],
}),
).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');
});
});
});
32 changes: 32 additions & 0 deletions packages/kernel-test/src/vats/endowment-globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,45 @@ export function buildRootObject(vatPowers: TestPowers) {
});
},

testInterval: async () => {
return new Promise((resolve) => {
let tickCount = 0;
const handle = setInterval(() => {
tickCount += 1;
if (tickCount >= 2) {
clearInterval(handle);
tlog(`interval: ticks=${tickCount}`);
resolve(tickCount);
}
}, 10);
});
},

testDate: () => {
const now = Date.now();
const isReal = !Number.isNaN(now) && now > 0;
tlog(`date: isReal=${String(isReal)}`);
return isReal;
},

testCrypto: () => {
// A 16-byte buffer makes the all-zero coincidence vanishingly small
// (2^-128); a 4-byte buffer has a ~1-in-4-billion false-negative rate.
const buffer = new Uint8Array(16);
// eslint-disable-next-line n/no-unsupported-features/node-builtins -- `crypto` here is the vat endowment, not the Node global; the rule's engines check does not apply inside a SES Compartment.
crypto.getRandomValues(buffer);
const hasRandomBytes = buffer.some((byte) => byte !== 0);
tlog(`crypto: hasRandomBytes=${String(hasRandomBytes)}`);
return hasRandomBytes;
},

testMath: () => {
const value = Math.random();
const inRange = value >= 0 && value < 1;
tlog(`math: inRange=${String(inRange)}`);
return inRange;
},

checkGlobal: (name: string) => {
// In a SES compartment, globalThis points to the compartment's own
// global object, so this correctly detects whether an endowment was
Expand Down
6 changes: 6 additions & 0 deletions packages/ocap-kernel/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Integrate Snaps attenuated endowment factories into vat globals ([#937](https://github.com/MetaMask/ocap-kernel/pull/937))
- Add `setInterval`, `clearInterval`, `crypto`, `SubtleCrypto`, and `Math` (crypto-backed `Math.random`) to the default vat endowments
- **BREAKING:** `setTimeout` now enforces a 10 ms minimum delay (upstream Snaps `MINIMUM_TIMEOUT`); shorter delays are silently coerced to 10 ms
- **BREAKING:** `Date.now()` is attenuated — each read adds up to 1 ms of random jitter, clamped monotonic non-decreasing; precise sub-millisecond timing no longer leaks through
- **BREAKING:** `clearTimeout`/`clearInterval` only clear handles created by the same vat's `setTimeout`/`setInterval`; passing a host-format handle is a no-op
- **BREAKING:** replace exported `DEFAULT_ALLOWED_GLOBALS` constant with `createDefaultEndowments()` factory and `VatEndowments` type; `VatSupervisor` now accepts `makeAllowedGlobals` in place of `allowedGlobals`
- Make vat global allowlist configurable and expand available endowments ([#933](https://github.com/MetaMask/ocap-kernel/pull/933))
- Export `DEFAULT_ALLOWED_GLOBALS` with `URL`, `URLSearchParams`, `atob`, `btoa`, `AbortController`, and `AbortSignal` in addition to the existing globals
- Accept optional `allowedGlobals` on `VatSupervisor` for custom allowlists
Expand Down
1 change: 1 addition & 0 deletions packages/ocap-kernel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"@metamask/kernel-utils": "workspace:^",
"@metamask/logger": "workspace:^",
"@metamask/rpc-errors": "^7.0.3",
"@metamask/snaps-execution-environments": "^11.1.0",
"@metamask/streams": "workspace:^",
"@metamask/superstruct": "^3.2.1",
"@metamask/utils": "^11.9.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/ocap-kernel/src/Kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ export class Kernel {
* @param options.mnemonic - Optional BIP39 mnemonic for deriving the kernel identity.
* @param options.ioChannelFactory - Optional factory for creating IO channels.
* @param options.systemSubclusters - Optional array of system subcluster configurations.
* @param options.allowedGlobalNames - Optional list of allowed global names for vat endowments. When set, only these names from DEFAULT_ALLOWED_GLOBALS are available to vats.
* @param options.allowedGlobalNames - Optional list of allowed global names for vat endowments. When set, only these names from the `VatSupervisor`'s configured endowments (see `createDefaultEndowments`) are available to vats.
* @returns A promise for the new kernel instance.
*/
static async make(
Expand Down
2 changes: 1 addition & 1 deletion packages/ocap-kernel/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ describe('index', () => {
expect(Object.keys(indexModule).sort()).toStrictEqual([
'CapDataStruct',
'ClusterConfigStruct',
'DEFAULT_ALLOWED_GLOBALS',
'Kernel',
'KernelStatusStruct',
'SubclusterStruct',
'VatConfigStruct',
'VatHandle',
'VatIdStruct',
'VatSupervisor',
'createDefaultEndowments',
'generateMnemonic',
'initTransport',
'insistKRef',
Expand Down
Loading
Loading