Skip to content

Commit 7e9324d

Browse files
grypezclaude
andcommitted
fix(ocap-kernel): guard against prototype injection via vat names
Validate that no vat name shadows Object.prototype built-ins (e.g. __proto__, constructor) before using vatName as a property key on the roots/vatRootKrefs maps. Adds // lgtm[js/remote-property-injection] suppressions on the three write sites as a belt-and-suspenders measure for CodeQL, which does not track the null-prototype path through the TypeScript cast. Object.create(null) was considered but rejected: @endo/marshal requires Object.prototype-chained objects for CopyRecord serialization. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e9323e2 commit 7e9324d

1 file changed

Lines changed: 12 additions & 16 deletions

File tree

packages/ocap-kernel/src/vats/SubclusterManager.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -303,10 +303,7 @@ export class SubclusterManager {
303303
}> {
304304
const vatEntries = Object.entries(config.vats);
305305

306-
const services: Record<string, SlotValue> = Object.create(null) as Record<
307-
string,
308-
SlotValue
309-
>;
306+
const services: Record<string, SlotValue> = {};
310307
const ioNames = config.io
311308
? new Set(Object.keys(config.io))
312309
: new Set<string>();
@@ -354,14 +351,13 @@ export class SubclusterManager {
354351
// Build the roots map. Succeeded vats receive real ko<N> refs; failed peer
355352
// vats receive an immediately-rejected kernel promise so bootstrap can
356353
// observe the failure via E(roots.peer).method() pipelining.
357-
const roots: Record<string, SlotValue> = Object.create(null) as Record<
358-
string,
359-
SlotValue
360-
>;
361-
const vatRootKrefs: Record<string, KRef> = Object.create(null) as Record<
362-
string,
363-
KRef
364-
>;
354+
const roots: Record<string, SlotValue> = {};
355+
const vatRootKrefs: Record<string, KRef> = {};
356+
// Reject vat names that shadow Object.prototype built-ins (__proto__,
357+
// constructor, etc.) before using them as property keys.
358+
for (const [name] of vatEntries) {
359+
!(name in Object.prototype) || Fail`invalid vat name '${name}'`;
360+
}
365361
let firstPeerFailure: Error | undefined;
366362
for (let i = 0; i < vatEntries.length; i++) {
367363
const vatEntry = vatEntries[i];
@@ -371,8 +367,8 @@ export class SubclusterManager {
371367
}
372368
const [vatName] = vatEntry;
373369
if (result.status === 'fulfilled') {
374-
roots[vatName] = kslot(result.value);
375-
vatRootKrefs[vatName] = result.value;
370+
roots[vatName] = kslot(result.value); // lgtm[js/remote-property-injection]
371+
vatRootKrefs[vatName] = result.value; // lgtm[js/remote-property-injection]
376372
} else {
377373
// launchVat always wraps failures in new Error(...), so reason is an Error
378374
const peerError =
@@ -385,7 +381,7 @@ export class SubclusterManager {
385381
this.#kernelQueue.resolvePromises('kernel', [
386382
[kpid, true, makeKernelError('VAT_TERMINATED', peerError.message)],
387383
]);
388-
roots[vatName] = kslot(kpid, 'vatRoot');
384+
roots[vatName] = kslot(kpid, 'vatRoot'); // lgtm[js/remote-property-injection]
389385
}
390386
}
391387

@@ -404,7 +400,7 @@ export class SubclusterManager {
404400
throw firstPeerFailure;
405401
}
406402

407-
return { rootKref, bootstrapResult, vatRootKrefs: { ...vatRootKrefs } };
403+
return { rootKref, bootstrapResult, vatRootKrefs };
408404
}
409405

410406
/**

0 commit comments

Comments
 (0)