Skip to content

Commit ec3d701

Browse files
FUDCoclaude
andauthored
fix(ocap-kernel): accept durable/virtual/faceted vrefs in isVRef (#949)
## Summary - Widens `isVRef` to match the full vref grammar produced by `@agoric/swingset-liveslots`'s `parseVatSlot` — durable (`o+d10/1`), virtual (`o+v3/4`), and faceted (`o+d10/1:0`) forms. - Previously the regex was `^[op][+-]\d+$`, which rejected any vat using `defineDurableKind`. The branding refactor in #917 made `isVRef` load-bearing through `EndpointMessageStruct`, `insistERef`, and `parseReachableAndVatSlot`, so any vat using durable kinds would fail outgoing-send validation and persisted-slot reads. Existing kernel-internal tests didn't exercise this path; the bug surfaced when a vat in a downstream branch used `defineDurableKind` for a public facet and bootstrap died with `not a valid endpoint message`. `isKRef` and `isRRef` are unchanged — kernel and remote allocators don't emit durability suffixes. ## Test plan - [x] `yarn workspace @MetaMask/ocap-kernel test` (2341 passing locally) - [x] Live verification: a vat using `VatData.defineDurableKind` for its public facet now boots cleanly through `launchSubcluster` (previously failed at the first outgoing eventual-send from `bootstrap`) - [ ] CI green 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes core reference validation (`isVRef`/`insistERef`) used at RPC and message translation boundaries; a regex bug could either reject valid traffic or inadvertently accept malformed refs. > > **Overview** > Fixes vat reference validation to accept the full liveslots vref grammar, including durable (`o+d10/1`), virtual (`o+v3/4`), and faceted (`o+d10/1:0`) object refs. > > Updates `isVRef` to a stricter grammar-aware regex (only allowing durability/subid/facet syntax on `o+`), expands unit tests for `isVRef`/`insistVRef`/`isERef`/`insistERef`, and records the fix in the changelog to prevent endpoint message validation from breaking vats that use durable kinds. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 9c1cffa. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent eeae1ee commit ec3d701

3 files changed

Lines changed: 48 additions & 12 deletions

File tree

packages/ocap-kernel/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3535
- Deserialize CapData rejections in `Kernel.queueMessage` so vat errors surface as plain `Error` objects to all callers ([#928](https://github.com/MetaMask/ocap-kernel/pull/928))
3636
- Detect peer restart across receiver state loss so the receiving kernel no longer silently drops a restarted peer's `seq=1` messages ([#948](https://github.com/MetaMask/ocap-kernel/pull/948))
3737
- Persist the peer's last-observed incarnation and compare it on every successful handshake; on a detected restart, clear the peer's c-list contributions and reject the promises it was deciding before the new incarnation reuses any erefs
38+
- Accept liveslots-allocated durable, virtual, and faceted vrefs (e.g. `o+d10/1`, `o+v3/4:0`) in `isVRef` / `insistERef` / `EndpointMessageStruct` validation ([#949](https://github.com/MetaMask/ocap-kernel/pull/949))
39+
- Previously the regex only matched plain `[op][+-]N`, so any vat using `defineDurableKind` failed outgoing-send validation and persisted-slot reads
3840

3941
## [0.7.0]
4042

packages/ocap-kernel/src/types.test.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -602,12 +602,21 @@ describe('insistKRef', () => {
602602
});
603603

604604
describe('isVRef', () => {
605-
it.each(['o+0', 'o-1', 'p+42', 'p-0', 'o+123456789'])(
606-
'returns true for valid VRef %s',
607-
(value) => {
608-
expect(isVRef(value)).toBe(true);
609-
},
610-
);
605+
it.each([
606+
'o+0',
607+
'o-1',
608+
'p+42',
609+
'p-0',
610+
'o+123456789',
611+
// Vat-allocated durable / virtual objects (liveslots `defineDurableKind`
612+
// and friends). See parseVatSlot in @agoric/swingset-liveslots.
613+
'o+d10/1',
614+
'o+v3/4',
615+
'o+d10/1:0',
616+
'o+v3/4:7',
617+
])('returns true for valid VRef %s', (value) => {
618+
expect(isVRef(value)).toBe(true);
619+
});
611620

612621
it.each([
613622
{ name: 'missing sign', value: 'o1' },
@@ -616,6 +625,16 @@ describe('isVRef', () => {
616625
{ name: 'non-digit suffix', value: 'o+1abc' },
617626
{ name: 'kernel ref', value: 'ko1' },
618627
{ name: 'remote ref', value: 'ro+1' },
628+
// Durability marker only valid on `o+`. `o-` is kernel-allocated;
629+
// promises and devices never carry a durability/subid suffix.
630+
{ name: 'durable on import', value: 'o-d10/1' },
631+
{ name: 'durable on promise', value: 'p+d10/1' },
632+
// Subid grammar requires a durability marker.
633+
{ name: 'subid without marker', value: 'o+10/1' },
634+
// Facet requires a subid.
635+
{ name: 'facet without subid', value: 'o+d10:0' },
636+
// Device refs are not supported by this kernel.
637+
{ name: 'device ref', value: 'd+0' },
619638
{ name: 'number', value: 123 },
620639
{ name: 'null', value: null },
621640
])('returns false for $name', ({ value }) => {
@@ -624,7 +643,7 @@ describe('isVRef', () => {
624643
});
625644

626645
describe('insistVRef', () => {
627-
it.each(['o+0', 'p-1', 'o+42'])(
646+
it.each(['o+0', 'p-1', 'o+42', 'o+d10/1', 'o+v3/4:7'])(
628647
'does not throw for valid VRef %s',
629648
(value) => {
630649
expect(() => insistVRef(value)).not.toThrow();
@@ -680,7 +699,7 @@ describe('insistRRef', () => {
680699
});
681700

682701
describe('isERef', () => {
683-
it.each(['o+0', 'p-1', 'ro+1', 'rp-2'])(
702+
it.each(['o+0', 'p-1', 'ro+1', 'rp-2', 'o+d10/1', 'o+v3/4:7'])(
684703
'returns true for valid ERef %s',
685704
(value) => {
686705
expect(isERef(value)).toBe(true);
@@ -698,7 +717,7 @@ describe('isERef', () => {
698717
});
699718

700719
describe('insistERef', () => {
701-
it.each(['o+0', 'p-1', 'ro+1', 'rp-2'])(
720+
it.each(['o+0', 'p-1', 'ro+1', 'rp-2', 'o+d10/1', 'o+v3/4:7'])(
702721
'does not throw for valid ERef %s',
703722
(value) => {
704723
expect(() => insistERef(value)).not.toThrow();

packages/ocap-kernel/src/types.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,22 @@ export type KRef = string & { readonly [KRefBrand]: never };
101101

102102
declare const VRefBrand: unique symbol;
103103
/**
104-
* Vat-space reference. Format: `${'o'|'p'}${'+' | '-'}${number}`.
105-
* E.g. `"o+0"`, `"p-7"`.
104+
* Vat-space reference. Mirrors the vref grammar produced by
105+
* `@agoric/swingset-liveslots`'s `parseVatSlot`:
106+
*
107+
* - `${'o'|'p'}${'+' | '-'}${number}` for plain refs
108+
* (kernel imports, ephemeral exports, promises).
109+
* E.g. `"o+0"`, `"o-3"`, `"p-7"`.
110+
* - `o+${'d'|'v'}${kindId}/${instanceId}` for vat-allocated
111+
* virtual (`v`) or durable (`d`) objects.
112+
* E.g. `"o+d10/1"`, `"o+v3/4"`.
113+
* - `o+${'d'|'v'}${kindId}/${instanceId}:${facetId}` for the
114+
* same with a facet selector.
115+
* E.g. `"o+d10/1:0"`.
116+
*
117+
* The durability/subid/facet syntax is only valid for `o+`
118+
* (vat-allocated objects); kernel imports, promises, and remotes
119+
* never carry it.
106120
*/
107121
export type VRef = string & { readonly [VRefBrand]: never };
108122

@@ -139,7 +153,8 @@ export const isKRef = (value: unknown): value is KRef =>
139153
typeof value === 'string' && /^k[op]\d+$/u.test(value);
140154

141155
export const isVRef = (value: unknown): value is VRef =>
142-
typeof value === 'string' && /^[op][+-]\d+$/u.test(value);
156+
typeof value === 'string' &&
157+
/^(?:p[+-]\d+|o-\d+|o\+(?:\d+|[dv]\d+\/\d+(?::\d+)?))$/u.test(value);
143158

144159
export const isRRef = (value: unknown): value is RRef =>
145160
typeof value === 'string' && /^r[op][+-]\d+$/u.test(value);

0 commit comments

Comments
 (0)