fix(net): stabilize shared wire graph audits
This commit is contained in:
@@ -325,6 +325,96 @@ test('send serializes stable descriptor snapshots without invoking Proxy get tra
|
||||
);
|
||||
});
|
||||
|
||||
test('send audits one shared source identity instead of accepting path-local stable reads', async () => {
|
||||
const { client, transports } = makeHarness();
|
||||
client.start();
|
||||
const transport = transports[0]!;
|
||||
await transport.flush();
|
||||
const target = { value: 0 };
|
||||
const reportedValues = [1, 2, 1, 2] as const;
|
||||
let descriptorReads = 0;
|
||||
const shared = new Proxy(target, {
|
||||
getOwnPropertyDescriptor(source, key) {
|
||||
const descriptor = Reflect.getOwnPropertyDescriptor(source, key);
|
||||
if (key !== 'value' || !descriptor) return descriptor;
|
||||
const value = reportedValues[descriptorReads];
|
||||
descriptorReads++;
|
||||
if (value === undefined) throw new Error('shared source was audited more than twice');
|
||||
return { ...descriptor, value };
|
||||
},
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => client.send({
|
||||
app: 'youle',
|
||||
route: 'agent',
|
||||
rpc: 'shared_unstable',
|
||||
data: { left: shared, right: shared },
|
||||
}),
|
||||
/\$\.data\.left\.value.*descriptor.*changed/i,
|
||||
);
|
||||
assert.equal(descriptorReads, 2, 'the shared source must have one capture and one verification');
|
||||
assert.deepEqual(transport.sent, []);
|
||||
});
|
||||
|
||||
test('send expands an ordinary shared DAG reference at every wire path', async () => {
|
||||
const { client, transports } = makeHarness();
|
||||
client.start();
|
||||
const transport = transports[0]!;
|
||||
await transport.flush();
|
||||
const shared = { score: 7 };
|
||||
const completed = {
|
||||
app: 'youle',
|
||||
route: 'agent',
|
||||
rpc: 'shared_dag',
|
||||
data: { left: shared, right: shared },
|
||||
} as OutboundEnvelope;
|
||||
|
||||
client.send(completed);
|
||||
|
||||
assert.equal(
|
||||
transport.sent[0],
|
||||
'{"app":"youle","route":"agent","rpc":"shared_dag","data":{"left":{"score":7},"right":{"score":7}}}',
|
||||
);
|
||||
});
|
||||
|
||||
test('send rejects a cyclic prototype identity within a finite prototype walk', async () => {
|
||||
const { client, transports } = makeHarness();
|
||||
client.start();
|
||||
const transport = transports[0]!;
|
||||
await transport.flush();
|
||||
const finiteWalkLimit = 4;
|
||||
let prototypeReads = 0;
|
||||
let getCalls = 0;
|
||||
let selfPrototype!: object;
|
||||
selfPrototype = new Proxy(Object.create(null) as object, {
|
||||
getPrototypeOf() {
|
||||
prototypeReads++;
|
||||
if (prototypeReads > finiteWalkLimit) {
|
||||
throw new Error('prototype walk exceeded the finite test bound');
|
||||
}
|
||||
return selfPrototype;
|
||||
},
|
||||
get(source, key, receiver) {
|
||||
getCalls++;
|
||||
return Reflect.get(source, key, receiver);
|
||||
},
|
||||
});
|
||||
|
||||
assert.throws(
|
||||
() => client.send({
|
||||
app: 'youle',
|
||||
route: 'agent',
|
||||
rpc: 'prototype_cycle',
|
||||
data: { loop: selfPrototype },
|
||||
}),
|
||||
/\$\.data\.loop.*prototype.*cycle/i,
|
||||
);
|
||||
assert.ok(prototypeReads <= finiteWalkLimit);
|
||||
assert.equal(getCalls, 0, 'prototype validation must not invoke Proxy get');
|
||||
assert.deepEqual(transport.sent, []);
|
||||
});
|
||||
|
||||
test('send rejects source graphs whose descriptors, own-key order, or prototype change', async () => {
|
||||
const cases: ReadonlyArray<readonly [string, () => object, RegExp]> = [
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user