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.
This commit is contained in:
Chinesezjc
2026-08-07 13:27:54 +08:00
parent f0d669883f
commit 98ebe1315d
2 changed files with 20 additions and 4 deletions
@@ -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
@@ -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