mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
fix(code-runtime-python): bound the open-merge hold by the ledger budget
The review's critical: the open-merge branch accumulated the held fragment before any ledger check, so a forged open flood could grow host memory without touching logBudget. The held fragment is now bounded by the exact-cost walk (jsonStringCostUpTo against the remaining budget; the closing frame's admit() still bills the merged entry once), and the open field is registered in the README wire-contract section and the fd-3 protocol note (en + zh). A forged open-flood case asserts truncation to the marker under a 64-byte budget.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/code-runtime/code-runtime-python/README.md
|
||||
README.md: c6ab48b5ccd4e386f9ad416dee64b090500594f6
|
||||
README.zh.md: 637de914212be9b7e6a3b63e443d7252c092acf2
|
||||
README.md: 97a4b6d4ccf51cf999dcef237aac1480a532bce6
|
||||
README.zh.md: 649ca8ddd4c1ad02f27b8131c60f22511801c1e9
|
||||
|
||||
@@ -33,7 +33,7 @@ The package's default export is the `PythonCodeRuntime` plugin. Its public surfa
|
||||
|
||||
### The wire
|
||||
|
||||
Frames travel on the child's fd 3 as JSON-lines — one object per line — so stdout/stderr stay clear for the program's own output. Child → host: `boot-ack`, `call`, `log`, `done`. Host → child: `boot` (first frame, carrying every cap and the namespace declarations), `run` (after `boot-ack`, carrying only the program body), and one `reply` per `call`. A forged frame can carry both `value` and `error` on `done`, so a consumer must check `error` first and ignore `value` when it is set.
|
||||
Frames travel on the child's fd 3 as JSON-lines — one object per line — so stdout/stderr stay clear for the program's own output. Child → host: `boot-ack`, `call`, `log`, `done`. Host → child: `boot` (first frame, carrying every cap and the namespace declarations), `run` (after `boot-ack`, carrying only the program body), and one `reply` per `call`. A forged frame can carry both `value` and `error` on `done`, so a consumer must check `error` first and ignore `value` when it is set. A `log` frame's `open` flag marks an unterminated line committed by an explicit flush: the host appends the next log frame to the same entry, so `print('a', end='', flush=True); print('b')` reads back as one `'ab'` entry rather than a fake newline.
|
||||
|
||||
### What can go wrong
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ kind: "package-reference"
|
||||
|
||||
### wire
|
||||
|
||||
帧在子进程 fd 3 上以 JSON-lines 传输——每行一个对象——因此 stdout/stderr 留给程序自己的输出。子进程 → 宿主:`boot-ack`、`call`、`log`、`done`。宿主 → 子进程:`boot`(首帧,携带全部上限与命名空间声明)、`run`(`boot-ack` 之后,只携带程序体)与每个 `call` 一个 `reply`。伪造帧可在 `done` 上同时携带 `value` 与 `error`,因此消费方必须先检查 `error`,在它存在时忽略 `value`。
|
||||
帧在子进程 fd 3 上以 JSON-lines 传输——每行一个对象——因此 stdout/stderr 留给程序自己的输出。子进程 → 宿主:`boot-ack`、`call`、`log`、`done`。宿主 → 子进程:`boot`(首帧,携带全部上限与命名空间声明)、`run`(`boot-ack` 之后,只携带程序体)与每个 `call` 一个 `reply`。伪造帧可在 `done` 上同时携带 `value` 与 `error`,因此消费方必须先检查 `error`,在它存在时忽略 `value`。`log` 帧的 `open` 标志标记由显式 flush 提交的未结束行:宿主把下一个 log 帧追加到同一条目,因此 `print('a', end='', flush=True); print('b')` 读回为一条 `'ab'` 条目而不是假换行。
|
||||
|
||||
### 可能出错的地方
|
||||
|
||||
|
||||
@@ -1504,9 +1504,23 @@ export class PythonCodeRuntime extends CodeRuntime {
|
||||
// An explicit flush of an unterminated line: hold it so the next
|
||||
// frame appends to the SAME entry (print('a', end='', flush=True)
|
||||
// followed by print('b') reads back as one 'ab' entry, not a fake
|
||||
// newline). The residual is admitted by finish() if the run ends
|
||||
// with it still open.
|
||||
openLog = (openLog ?? '') + message.text
|
||||
// newline). The held fragment is BOUNDED by the ledger budget via
|
||||
// the exact-cost walk (a forged open flood would otherwise grow
|
||||
// openLog without touching logBudget — the same unbounded-retention
|
||||
// attack the ledger exists to stop). The cost is NOT billed here:
|
||||
// the closing frame's admit() bills the whole merged entry once.
|
||||
if (!logsTruncated) {
|
||||
const merged = (openLog ?? '') + message.text
|
||||
if (jsonStringCostUpTo(merged, logBudget - 1) === undefined) {
|
||||
logsTruncated = true
|
||||
logs.push(logTruncationMarker(this.config.maxLogBytes))
|
||||
clearStray(strayOut)
|
||||
clearStray(strayErr)
|
||||
openLog = undefined
|
||||
} else {
|
||||
openLog = merged
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
admit((openLog ?? '') + message.text)
|
||||
|
||||
@@ -1878,6 +1878,26 @@ describe('PythonCodeRuntime — programs and bindings', () => {
|
||||
expect(result.logs).toEqual(['committed'])
|
||||
}, 15_000)
|
||||
|
||||
it('bounds a forged open-frame flood against the log budget', async () => {
|
||||
// The open hold must be bounded by the ledger: without the exact-cost check
|
||||
// a forged open flood would grow the held fragment without touching
|
||||
// logBudget — unbounded host retention under a small budget. The flood now
|
||||
// truncates to the marker like any over-budget log traffic.
|
||||
const { runtime } = await setup({ maxLogBytes: 64 })
|
||||
const result = await runtime.run({
|
||||
program: [
|
||||
'import os',
|
||||
// 2000 forged open frames, each under the frame parse cap.
|
||||
'for _ in range(2000):',
|
||||
" os.write(3, b'{\"type\":\"log\",\"text\":\"a\",\"open\":true}\\n')",
|
||||
'return "done"',
|
||||
].join('\n'),
|
||||
bindings: [],
|
||||
})
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.logs).toEqual([logTruncationMarker(64)])
|
||||
}, 15_000)
|
||||
|
||||
it('keeps a float completion exact when the program mutates the decimal context', async () => {
|
||||
// The float encoder's Decimal(repr(value)).normalize() used the process
|
||||
// GLOBAL decimal context: a legitimate program setting
|
||||
|
||||
Reference in New Issue
Block a user