fix(net): stabilize shared wire graph audits

This commit is contained in:
2026-09-05 04:52:02 +08:00
parent 33904e0e58
commit e47d9d3007
2 changed files with 110 additions and 2 deletions
@@ -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]> = [
[