From a6678610b80211ef87f46e9bb53fcba2ffd5de27 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Thu, 27 Aug 2026 20:45:55 +0800 Subject: [PATCH] fix(code-runtime-python): cap the unknown-binding preview before JSON.stringify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reviewer's standing item: the unknown-binding reply ran JSON.stringify on the WHOLE capped target (global + '.' + name, each up to maxValueBytes code units), allocating the escaped form — up to ~6x under control-heavy input, a multi-hundred-MB spike near the maxValueBytes ceiling that no hostile-peer bound would have admitted. The escaped preview is now built from a 1 KiB prefix of the target (enough to identify the binding); capMessage still enforces the reply budget. A forged huge-name case drives the path. --- .../code-runtime-python/src/index.ts | 9 +++++++- .../code-runtime-python/tests/runtime.spec.ts | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/code-runtime/code-runtime-python/src/index.ts b/packages/code-runtime/code-runtime-python/src/index.ts index 2a0f25ad85..d8471a2fd9 100644 --- a/packages/code-runtime/code-runtime-python/src/index.ts +++ b/packages/code-runtime/code-runtime-python/src/index.ts @@ -1676,7 +1676,14 @@ export class PythonCodeRuntime extends CodeRuntime { // error. const cap = this.config.maxValueBytes const target = `${message.global.slice(0, cap)}.${message.name.slice(0, cap)}` - sendReply({ type: 'reply', id: message.id, ok: false, message: capMessage(`unknown binding ${JSON.stringify(target)}`, cap) }) + // JSON.stringify on the WHOLE capped target would still allocate + // the escaped form — up to ~6x under control-heavy input, a + // multi-hundred-MB spike near the maxValueBytes ceiling that no + // hostile-peer bound would have admitted. The message only needs + // to identify the binding, so the escaped form is built from a + // 1 KiB prefix; capMessage then enforces the reply budget. + const preview = JSON.stringify(target.slice(0, 1024)) + sendReply({ type: 'reply', id: message.id, ok: false, message: capMessage(`unknown binding ${preview}`, cap) }) return } void (async () => { diff --git a/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts b/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts index 8861b51c24..3dc98d10f8 100644 --- a/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts +++ b/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts @@ -3505,6 +3505,29 @@ describe('PythonCodeRuntime — hostile peer', () => { expect(seenLegitCall).toBe(true) }, 15_000) + it('caps the unknown-binding preview for a huge forged name', async () => { + // The unknown-binding reply's JSON.stringify ran on the WHOLE capped + // target, allocating the escaped form — up to ~6x under control-heavy + // input. The preview is now built from a 1 KiB prefix, so a forged call + // with a huge global/name cannot spike host memory near the value ceiling; + // the reply still identifies the binding. + const { runtime } = await setup({ maxWallMs: 8_000, maxValueBytes: 1024 * 1024 }) + const result = await runtime.run({ + program: [ + 'import os, json', + 'x = await tools.echo({"ping": True})', + 'name = "n" * 100000', + 'os.write(3, json.dumps({"type":"call","id":1,"global":"tools","name":name,"args":{}}).encode() + b"\\n")', + 'return x', + ].join('\n'), + bindings: tools({ + echo: async args => args as CodeJsonValue, + }), + }) + expect(result.error).toBeUndefined() + expect(result.value).toEqual({ ping: true }) + }, 15_000) + it('drops forged call frames whose ids are not the next in sequence, retaining no per-id state', async () => { // The host used to remember every answered id in a Set, so a program could // write an unbounded run of unique forged ids — each frame far below the