Skip to content
38 changes: 38 additions & 0 deletions packages/kernel-test/src/ocap-url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs';
import { waitUntilQuiescent } from '@metamask/kernel-utils';
import { describe, expect, it } from 'vitest';

import {
extractTestLogs,
getBundleSpec,
makeKernel,
makeTestLogger,
} from './utils.ts';

describe('ocap-url', () => {
it('user-code can make an ocap url', async () => {
const { logger, entries } = makeTestLogger();
const database = await makeSQLKernelDatabase({});
const kernel = await makeKernel(database, true, logger);
const vatIds = ['v1'];
const vat = await kernel.launchSubcluster({
bootstrap: 'alice',
vats: {
alice: {
bundleSpec: getBundleSpec('ocap-url'),
parameters: {},
},
},
});
expect(vat).toBeDefined();
const vats = kernel.getVatIds();
expect(vats).toStrictEqual(vatIds);

await waitUntilQuiescent();
const vatLogs = vatIds.map((vatId) => extractTestLogs(entries, vatId));
expect(vatLogs).toStrictEqual([
// This is a placeholder for the actual ocap url.
[expect.stringContaining(`Alice's ocap url: ocap://o+`)],
]);
});
});
45 changes: 45 additions & 0 deletions packages/kernel-test/src/revocation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs';
import { waitUntilQuiescent } from '@metamask/kernel-utils';
import { describe, expect, it } from 'vitest';

import {
extractTestLogs,
getBundleSpec,
makeKernel,
makeTestLogger,
} from './utils.ts';

describe('revocation', () => {
it('user-code revoker can call kernel syscall', async () => {
const { logger, entries } = makeTestLogger();
const database = await makeSQLKernelDatabase({});
const kernel = await makeKernel(database, true, logger);
const vatIds = ['v1', 'v2'];
const vat = await kernel.launchSubcluster({
bootstrap: 'main',
vats: {
main: {
bundleSpec: getBundleSpec('revocation-bootstrap'),
parameters: {},
},
provider: {
bundleSpec: getBundleSpec('revocation-provider'),
parameters: {},
},
},
});
expect(vat).toBeDefined();
const vats = kernel.getVatIds();
expect(vats).toStrictEqual(vatIds);

await waitUntilQuiescent();
expect(kernel.isRevoked('ko1')).toBe(false);
expect(kernel.isRevoked('ko2')).toBe(false);
expect(kernel.isRevoked('ko3')).toBe(true);
const vatLogs = vatIds.map((vatId) => extractTestLogs(entries, vatId));
expect(vatLogs).toStrictEqual([
['foo', 'bar', 'revoked object', 'done'],
['slam:0'],
]);
});
});
31 changes: 31 additions & 0 deletions packages/kernel-test/src/vats/ocap-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { E } from '@endo/eventual-send';
import { Far } from '@endo/marshal';

/**
* Build function for vats that will run various tests.
*
* @param {object} vatPowers - Special powers granted to this vat.
* @param {object} vatPowers.logger - The logger for this vat.
* @returns {*} The root object for the new vat.
*/
export function buildRootObject({ logger }) {
const { log } = logger.subLogger({ tags: ['test'] });
let contact;
return Far('root', {
async bootstrap({ alice }) {
contact = Far('contact', {
// An external actor can send a message to Alice by following an
// ocap url like "ocap://.../contact?whoAmI=Bob&message=Hello".
contact: (whoAmI, message) => E(alice).contact(whoAmI, message),
});
const ocapUrl = E(alice).makeContactUrl();
log(`Alice's ocap url: ${await ocapUrl}`);
},
// `makeOcapUrl` is an endowment available in global scope.
// eslint-disable-next-line no-undef
makeContactUrl: () => makeOcapUrl(contact),
async contact(sender = 'unknown', message = 'hello') {
log(`contact from ${sender}: ${message}`);
},
});
}
24 changes: 24 additions & 0 deletions packages/kernel-test/src/vats/revocation-bootstrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { E } from '@endo/eventual-send';
import { Far } from '@endo/marshal';

/**
* Build function for vats that will run various tests.
*
* @param {object} vatPowers - Special powers granted to this vat.
* @returns {*} The root object for the new vat.
*/
export function buildRootObject(vatPowers) {
const { log } = vatPowers.logger.subLogger({ tags: ['test'] });
return Far('root', {
async bootstrap({ provider }) {
const [gate, revoker] = await E(provider).requestPlatform();
await E(gate).foo().then(log);
await E(gate).bar().then(log);
await E(revoker).slam();
// XXX Methods called on a revoked object should reject, but currently
// resolve with a 'revoked object' string.
await E(gate).foo().catch(log);
log('done');
},
});
}
32 changes: 32 additions & 0 deletions packages/kernel-test/src/vats/revocation-provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Far } from '@endo/marshal';

/**
* Build function for vats that will run various tests.
*
* @param {object} vatPowers - The vat powers.
* @param {object} vatPowers.logger - The logger for this vat.
* @returns {*} The root object for the new vat.
*/
export function buildRootObject({ logger }) {
const { log } = logger.subLogger({ tags: ['test'] });
const platform = { foo: () => `foo`, bar: () => `bar` };
let revokerCount = 0;
const revocable = (obj) => {
const gate = Far('gate', { ...obj });
// XXX makeRevoker is defined as an endowment (in VatSupervisor.ts), but
// the linter has no way to know that it is defined.
// eslint-disable-next-line no-undef
const revoker = makeRevoker(gate);
const id = revokerCount;
revokerCount += 1;
const slam = () => {
revoker();
log(`slam:${id}`);
};
return [gate, Far(`slam:${id}`, { slam })];
};
return Far('root', {
requestPlatform: () => revocable(platform),
revokerCount: () => revokerCount,
});
}
2 changes: 1 addition & 1 deletion packages/ocap-kernel/src/KernelQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export class KernelQueue {
if (state !== 'unresolved') {
Fail`${kpid} was already resolved`;
}
if (decider !== vatId) {
if (vatId && decider !== vatId) {
const why = decider ? `its decider is ${decider}` : `it has no decider`;
Fail`${vatId} not permitted to resolve ${kpid} because ${why}`;
}
Expand Down
10 changes: 8 additions & 2 deletions packages/ocap-kernel/src/VatSupervisor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { makeLiveSlots as localMakeLiveSlots } from '@agoric/swingset-liveslots';
import type {
VatDeliveryObject,
VatSyscallObject,
VatSyscallResult,
} from '@agoric/swingset-liveslots';
import { importBundle } from '@endo/import-bundle';
Expand All @@ -17,13 +16,19 @@ import { serializeError } from '@metamask/rpc-errors';
import type { DuplexStream } from '@metamask/streams';
import { isJsonRpcRequest, isJsonRpcResponse } from '@metamask/utils';

import { makeEndowments } from './endowments/index.ts';
import { vatSyscallMethodSpecs, vatHandlers } from './rpc/index.ts';
import { makeGCAndFinalize } from './services/gc-finalize.ts';
import { makeDummyMeterControl } from './services/meter-control.ts';
import { makeSupervisorSyscall } from './services/syscall.ts';
import type { DispatchFn, MakeLiveSlotsFn, GCTools } from './services/types.ts';
import { makeVatKVStore } from './store/vat-kv-store.ts';
import type { VatConfig, VatDeliveryResult, VatId } from './types.ts';
import type {
VatConfig,
VatDeliveryResult,
VatId,
VatSyscallObject,
} from './types.ts';
import { isVatConfig, coerceVatSyscallObject } from './types.ts';

const makeLiveSlots: MakeLiveSlotsFn = localMakeLiveSlots;
Expand Down Expand Up @@ -254,6 +259,7 @@ export class VatSupervisor {
const workerEndowments = {
console: this.#logger.subLogger({ tags: ['console'] }),
assert: globalThis.assert,
...makeEndowments(syscall, gcTools, this.id),
};

const { bundleSpec, parameters } = vatConfig;
Expand Down
32 changes: 26 additions & 6 deletions packages/ocap-kernel/src/VatSyscall.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {
SwingSetCapData,
VatOneResolution,
VatSyscallObject,
VatSyscallResult,
} from '@agoric/swingset-liveslots';
import { Logger } from '@metamask/logger';
Expand All @@ -11,7 +10,7 @@ import { makeError } from './services/kernel-marshal.ts';
import type { KernelStore } from './store/index.ts';
import { parseRef } from './store/utils/parse-ref.ts';
import { coerceMessage } from './types.ts';
import type { Message, VatId, KRef } from './types.ts';
import type { VatSyscallObject, Message, VatId, KRef } from './types.ts';

type VatSyscallProps = {
vatId: VatId;
Expand Down Expand Up @@ -100,6 +99,23 @@ export class VatSyscall {
}
}

/**
* Handle a 'revoke' syscall from the vat.
*
* @param krefs - The KRefs of the objects to be revoked.
*/
#handleSyscallRevoke(krefs: KRef[]): void {
for (const kref of krefs) {
const owner = this.#kernelStore.getOwner(kref);
if (owner !== this.vatId) {
throw Error(
`vat ${this.vatId} cannot revoke ${kref} (owned by ${owner})`,
);
}
this.#kernelStore.revoke(kref);
}
}

/**
* Handle a 'dropImports' syscall from the vat.
*
Expand Down Expand Up @@ -186,10 +202,7 @@ export class VatSyscall {
return harden(['error', 'vat not found']);
}

const kso: VatSyscallObject = this.#kernelStore.translateSyscallVtoK(
this.vatId,
vso,
);
const kso = this.#kernelStore.translateSyscallVtoK(this.vatId, vso);
const [op] = kso;
const { vatId } = this;
const { log } = console;
Expand Down Expand Up @@ -226,6 +239,13 @@ export class VatSyscall {
this.vatRequestedTermination = { reject: isFailure, info };
break;
}
case 'revoke': {
// [KRef[]];
const [, krefs] = kso;
log(`@@@@ ${vatId} syscall revoke ${JSON.stringify(krefs)}`);
this.#handleSyscallRevoke(krefs);
break;
}
case 'dropImports': {
// [KRef[]];
const [, refs] = kso;
Expand Down
50 changes: 50 additions & 0 deletions packages/ocap-kernel/src/endowments/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { makeMarshaller } from '@agoric/swingset-liveslots';
import { Fail } from '@endo/errors';

// Used in the docs for a safe use of `toRef`.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { factory as makeRevoker } from './factories/make-revoker.ts';
import type { EndowmentContext, ToRef, Marshaller } from './types.ts';
import type { GCTools, Syscall } from '../services/types.ts';
import type { VatId } from '../types.ts';

/**
* Make a function that converts an object to a vref.
*
* **ATTN**: Do not expose the return value of this function to user code.
*
* This is a hack that disrespects liveslots's encapsulation of the marshaller.
* If the user code gets a handle on `toRef` and a capability to send messages
* to the kernel, it can break vat containment by impersonating its supervisor.
*
* It is fine to expose a hardened function which passes an object to `toRef`,
* as long as the vref cannot escape the scope of that function.
*
* @see {@link makeRevoker} for an example of safe use of `toRef`.
*
* @param marshaller - The liveslots marshaller.
* @returns A function that converts an object to a vref.
*/
function makeToRef(marshaller: Marshaller): ToRef {
const toRef: ToRef = (object) =>
marshaller.toCapData(object).slots[0] ??
Fail`cannot make ocap url for object ${object}`;
return harden(toRef);
}

/**
* Make a context for an endowment.
*
* @param syscall - The syscall object.
* @param gcTools - The gc tools.
* @param vatId - The vat id.
* @returns A context for an endowment.
*/
export function makeEndowmentContext(
syscall: Syscall,
gcTools: GCTools,
vatId: VatId,
): EndowmentContext {
const toRef = makeToRef(makeMarshaller(syscall, gcTools, vatId).m);
return { syscall, gcTools, vatId, toRef };
}
18 changes: 18 additions & 0 deletions packages/ocap-kernel/src/endowments/factories/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as makeOcapUrl from './make-ocap-url.ts';
import * as makeRevoker from './make-revoker.ts';
import * as scry from './scry.ts';
import type { EndowmentDefinition } from '../types.ts';

const endowmentDefinitions = {
makeOcapUrl,
makeRevoker,
scry,
} as const satisfies Record<string, EndowmentDefinition>;

export type EndowmentName = keyof typeof endowmentDefinitions;

export type Endowments = {
[K in EndowmentName]: ReturnType<(typeof endowmentDefinitions)[K]['factory']>;
};

export default endowmentDefinitions;
Loading
Loading