Просмотр исходного кода

fix(code-runtime-python): cap the unknown-binding preview before JSON.stringify

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.
Chinesezjc 2 недель назад
Родитель
Сommit
a6678610b8

+ 8 - 1
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 () => {

+ 23 - 0
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