fix(code-runtime): contain deep output accounting

This commit is contained in:
Tianyi Cui
2026-07-21 22:54:12 +08:00
parent 18bd8082fc
commit fc97403946
5 changed files with 107 additions and 36 deletions

View File

@@ -38,42 +38,60 @@ export function jsonStringBytesUpTo(text: string, maxBytes: number): number | un
* @returns Exact serialized bytes, or `undefined` as soon as the cap is crossed.
*/
export function jsonValueBytesUpTo(value: CodeJsonValue, maxBytes: number): number | undefined {
if (value === null) return maxBytes >= 4 ? 4 : undefined
if (typeof value === 'string') return jsonStringBytesUpTo(value, maxBytes)
if (typeof value === 'number') {
const bytes = Buffer.byteLength(String(value), 'utf8')
return bytes <= maxBytes ? bytes : undefined
}
if (typeof value === 'boolean') {
const bytes = value ? 4 : 5
return bytes <= maxBytes ? bytes : undefined
}
type Task =
| { kind: 'value'; value: CodeJsonValue }
| { kind: 'array'; value: CodeJsonValue[]; index: number }
| { kind: 'object'; value: Record<string, CodeJsonValue>; keys: string[]; index: number }
let bytes = 2
if (bytes > maxBytes) return undefined
if (Array.isArray(value)) {
for (let index = 0; index < value.length; index++) {
if (index > 0 && ++bytes > maxBytes) return undefined
const item = value[index]
if (item === undefined) return undefined
const itemBytes = jsonValueBytesUpTo(item, maxBytes - bytes)
if (itemBytes === undefined) return undefined
bytes += itemBytes
let bytes = 0
const add = (cost: number): boolean => {
bytes += cost
return bytes <= maxBytes
}
const tasks: Task[] = [{ kind: 'value', value }]
for (let task = tasks.pop(); task !== undefined; task = tasks.pop()) {
if (task.kind === 'value') {
const current = task.value
if (current === null) {
if (!add(4)) return undefined
} else if (typeof current === 'string') {
const stringBytes = jsonStringBytesUpTo(current, maxBytes - bytes)
if (stringBytes === undefined) return undefined
bytes += stringBytes
} else if (typeof current === 'number') {
if (!add(Buffer.byteLength(String(current), 'utf8'))) return undefined
} else if (typeof current === 'boolean') {
if (!add(current ? 4 : 5)) return undefined
} else if (Array.isArray(current)) {
if (!add(2)) return undefined
if (current.length > 0) tasks.push({ kind: 'array', value: current, index: 0 })
} else {
if (!add(2)) return undefined
const keys = Object.keys(current)
if (keys.length > 0) tasks.push({ kind: 'object', value: current, keys, index: 0 })
}
continue
}
return bytes
}
let entries = 0
for (const [key, item] of Object.entries(value)) {
if (entries > 0 && ++bytes > maxBytes) return undefined
if (task.index > 0 && !add(1)) return undefined
if (task.kind === 'array') {
const item = task.value[task.index]
if (item === undefined) return undefined
if (task.index + 1 < task.value.length) tasks.push({ ...task, index: task.index + 1 })
tasks.push({ kind: 'value', value: item })
continue
}
const key = task.keys[task.index]
/* v8 ignore next -- an object frame is created and advanced only for an existing Object.keys entry. */
if (key === undefined) return undefined
const keyBytes = jsonStringBytesUpTo(key, maxBytes - bytes)
if (keyBytes === undefined) return undefined
bytes += keyBytes + 1
if (bytes > maxBytes) return undefined
const itemBytes = jsonValueBytesUpTo(item, maxBytes - bytes)
if (itemBytes === undefined) return undefined
bytes += itemBytes
entries += 1
if (!add(keyBytes + 1)) return undefined
const item = task.value[key]
if (item === undefined) return undefined
if (task.index + 1 < task.keys.length) tasks.push({ ...task, index: task.index + 1 })
tasks.push({ kind: 'value', value: item })
}
return bytes
}

View File

@@ -3,20 +3,35 @@
import type { CodeJsonValue } from '@deepseek-ai/dsh-code-runtime'
/* jscpd:ignore-start -- the source worker mirrors session JSON helpers without workspace runtime imports */
/** Whether a realm-owned intrinsic prototype names and points back to its constructor. */
function hasIntrinsicConstructor(prototype: object, name: 'Array' | 'Object'): boolean {
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'constructor')
const constructor: unknown = descriptor?.value
return typeof constructor === 'function'
&& constructor.name === name
&& constructor.prototype === prototype
}
/** Whether a candidate is one realm's intrinsic `Object.prototype`. */
function isIntrinsicObjectPrototype(value: object): boolean {
return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, 'Object')
}
/** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
function hasPlainArrayPrototype(value: unknown[]): boolean {
const prototype: unknown = Object.getPrototypeOf(value)
if (!Array.isArray(prototype)) return false
if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array')) return false
const objectPrototype: unknown = Object.getPrototypeOf(prototype)
return objectPrototype !== null
&& !Array.isArray(objectPrototype)
&& Object.getPrototypeOf(objectPrototype) === null
return typeof objectPrototype === 'object'
&& objectPrototype !== null
&& isIntrinsicObjectPrototype(objectPrototype)
}
/** Whether an object is a plain or null-prototype record from any JavaScript realm. */
function hasPlainObjectPrototype(value: object): boolean {
const prototype: unknown = Object.getPrototypeOf(value)
return prototype === null || Object.getPrototypeOf(prototype) === null
return prototype === null
|| typeof prototype === 'object' && isIntrinsicObjectPrototype(prototype)
}
/** Return every JSON-visible object key, or reject own data JSON would discard. */

View File

@@ -1,4 +1,5 @@
import { describe, expect, it, vi } from 'vitest'
import type { CodeJsonValue } from '@deepseek-ai/dsh-code-runtime'
import { jsonStringBytesUpTo, jsonValueBytesUpTo, truncateJsonStringBytes } from '../src/output-json.ts'
describe('truncateJsonStringBytes', () => {
@@ -45,6 +46,8 @@ describe('jsonValueBytesUpTo', () => {
expect(jsonValueBytesUpTo(value, bytes)).toBe(bytes)
expect(jsonValueBytesUpTo(value, bytes - 1)).toBeUndefined()
expect(jsonValueBytesUpTo({}, 1)).toBeUndefined()
expect(jsonValueBytesUpTo([], 1)).toBeUndefined()
expect(jsonValueBytesUpTo([], 2)).toBe(2)
expect(jsonValueBytesUpTo(null, 3)).toBeUndefined()
expect(jsonValueBytesUpTo(10, 1)).toBeUndefined()
expect(jsonValueBytesUpTo(false, 4)).toBeUndefined()
@@ -55,5 +58,14 @@ describe('jsonValueBytesUpTo', () => {
expect(jsonValueBytesUpTo({ long: null }, 2)).toBeUndefined()
expect(jsonValueBytesUpTo({ '': null }, 4)).toBeUndefined()
expect(jsonValueBytesUpTo({ a: null }, 9)).toBeUndefined()
expect(jsonValueBytesUpTo({ a: undefined } as unknown as CodeJsonValue, 100)).toBeUndefined()
})
it('meters deeply nested arrays without recursive stack growth', () => {
let value: CodeJsonValue = null
for (let depth = 0; depth < 5_000; depth++) value = [value]
expect(jsonValueBytesUpTo(value, 10_004)).toBe(10_004)
expect(jsonValueBytesUpTo(value, 10_003)).toBeUndefined()
})
})

View File

@@ -430,6 +430,29 @@ describe('WorkerCodeRuntime — hostile programs (real workers)', () => {
expect(result).toEqual({ logs: [], error: { kind: 'exception', message: 'fake failure' } })
})
it('contains a deeply nested forged completion without overflowing the host meter', async () => {
const { runtime } = await setup()
const result = await runtime.run({
program: `
const { parentPort } = await import('node:worker_threads');
let value = null;
for (let depth = 0; depth < 3_000; depth++) value = [value];
parentPort.postMessage({ type: 'done', value });
`,
bindings: [],
})
expect(result.error).toBeUndefined()
let value = result.value
let depth = 0
while (Array.isArray(value)) {
expect(value).toHaveLength(1)
value = value[0]
depth += 1
}
expect(depth).toBe(3_000)
expect(value).toBeNull()
})
it('turns forged over-limit error text into output-limit at the host', async () => {
const { runtime } = await setup({ maxOutputBytes: 64 })
const result = await runtime.run({

View File

@@ -79,6 +79,8 @@ describe('snapshotCodeJsonValue', () => {
Object.defineProperty(symbolDecorated, Symbol('extra'), { value: true })
const hiddenObject = Object.defineProperty({}, 'hidden', { value: true })
const symbolObject = { [Symbol('extra')]: true }
const customPrototype = Object.create(null) as Record<string, unknown>
const customPrototypeObject = Object.assign(Object.create(customPrototype) as Record<string, unknown>, { value: 1 })
const forgedPrototype: unknown[] = []
Object.setPrototypeOf(forgedPrototype, null)
const forgedArray = [1]
@@ -94,6 +96,7 @@ describe('snapshotCodeJsonValue', () => {
symbolDecorated,
hiddenObject,
symbolObject,
customPrototypeObject,
forgedArray,
cyclic,
[undefined],