1
0
Эх сурвалжийг харах

fix(code-runtime-python): reject -0 call ids and document done value/error

- Drop a CALL frame whose id is negative zero: it passes Number.isFinite but
  the reply re-serializes it as `0`, colliding with a real call id `0`. The
  honest child never issues `-0`.
- Document that validateChildFrame preserves a forged done frame's value and
  error together on purpose, so consumers must check error before value.
Chinesezjc 2 сар өмнө
parent
commit
98ebe1315d

+ 10 - 4
packages/code-runtime/code-runtime-python/src/protocol.ts

@@ -82,8 +82,11 @@ interface LogMessage {
  * (traceback text), an `invalid-output` (completion value was not lossless
  * JSON), or an `output-limit` (serialized completion exceeded the configured
  * cap); wall/CPU budgets, aborts, and substrate death are observed host-side.
- * `value` is present only on a clean completion that produced one, and crosses
- * as exact lossless JSON — never substituted or truncated.
+ * From the honest child `value` is present only on a clean completion that
+ * produced one, and crosses as exact lossless JSON — never substituted or
+ * truncated. A forged frame CAN carry both `value` and `error`;
+ * {@link validateChildFrame} preserves both rather than guessing which to drop,
+ * so a consumer MUST check `error` first and ignore `value` when it is set.
  */
 interface DoneMessage {
   type: 'done'
@@ -387,8 +390,11 @@ export function validateChildFrame(raw: unknown): ChildToHost | undefined {
     case 'call': {
       // The id must be a finite number: it is echoed verbatim into the reply
       // frame, and a forged `1e400` id (Infinity after JSON.parse) would make
-      // the reply unencodable as strict JSON.
-      if (typeof m.id !== 'number' || !Number.isFinite(m.id) || typeof m.global !== 'string' || typeof m.name !== 'string') return undefined
+      // the reply unencodable as strict JSON. Negative zero is rejected too:
+      // it passes `Number.isFinite`, but the reply re-serializes it as `0`
+      // (`JSON.stringify({id:-0})` is `{"id":0}`), colliding with a real call
+      // whose id is `0` — the honest child never issues `-0`.
+      if (typeof m.id !== 'number' || !Number.isFinite(m.id) || Object.is(m.id, -0) || typeof m.global !== 'string' || typeof m.name !== 'string') return undefined
       // A forged frame can omit `args` entirely; rebuilding it as `undefined`
       // would invoke the binding with a non-JSON value, bypassing the
       // lossless-JSON argument boundary. Any PRESENT value is JSON-plain by

+ 10 - 0
packages/code-runtime/code-runtime-python/tests/protocol.spec.ts

@@ -98,6 +98,16 @@ describe('validateChildFrame', () => {
       .toEqual({ type: 'call', id: 1, global: 'tools', name: 'x', args: [0, 1.5] })
   })
 
+  it('drops a CALL frame whose id is negative zero', () => {
+    // `-0` passes Number.isFinite, but the reply re-serializes it as `0`
+    // (JSON.stringify({id:-0}) === '{"id":0}'), so a forged `-0` id would
+    // collide with a real call whose id is `0`. The honest child never sends it.
+    expect(validateChildFrame({ type: 'call', id: -0, global: 'tools', name: 'x', args: null })).toBeUndefined()
+    // Plain positive zero is a legitimate id and passes.
+    expect(validateChildFrame({ type: 'call', id: 0, global: 'tools', name: 'x', args: null }))
+      .toEqual({ type: 'call', id: 0, global: 'tools', name: 'x', args: null })
+  })
+
   it('passes DONE values through untouched — losslessness is metered later', () => {
     // validateChildFrame no longer scans done.value: an unbounded scan would
     // push every member of a wide forged payload before any byte cap ran. The