mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(code-runtime): close outer boundary bypasses
This commit is contained in:
@@ -269,7 +269,7 @@ export class WorkerCodeRuntime extends CodeRuntime {
|
||||
if (this.disposed) throw new Error('dsh-code-runtime-worker: run() after disposal')
|
||||
const bindings = this.validateBindings(request)
|
||||
if (request.signal?.aborted) {
|
||||
return { logs: [], error: { kind: 'abort', message: String(request.signal.reason) } }
|
||||
return this.failureBeforeWorker({ kind: 'abort', message: String(request.signal.reason) })
|
||||
}
|
||||
|
||||
let code: string
|
||||
@@ -280,12 +280,17 @@ export class WorkerCodeRuntime extends CodeRuntime {
|
||||
// A program that does not survive the type-strip (syntax error,
|
||||
// non-erasable syntax like `enum`) is a program failure, reported the
|
||||
// same way a thrown exception would be — and no worker ever spawns.
|
||||
return { logs: [], error: { kind: 'exception', message: messageOf(error) } }
|
||||
return this.failureBeforeWorker({ kind: 'exception', message: messageOf(error) })
|
||||
}
|
||||
|
||||
return await this.execute(request, code, bindings)
|
||||
}
|
||||
|
||||
/** Apply the outer-output ledger to failures that occur before a worker owns one. */
|
||||
private failureBeforeWorker(error: CodeRunFailure): CodeRunResult {
|
||||
return new OutputLedger(this.config.maxOutputBytes).failure([], error)
|
||||
}
|
||||
|
||||
/** Reject (seam misuse) malformed binding namespaces: non-identifier or reserved globals, duplicates, and the `console` collision. */
|
||||
private validateBindings(request: CodeRunRequest): Map<string, Record<string, CodeBindingFunction>> {
|
||||
const bindings = new Map<string, Record<string, CodeBindingFunction>>()
|
||||
@@ -405,9 +410,19 @@ export class WorkerCodeRuntime extends CodeRuntime {
|
||||
reply({ type: 'reply', id: message.id, ok: false, message: `unknown binding ${JSON.stringify(`${message.global}.${message.name}`)}` })
|
||||
return
|
||||
}
|
||||
let args: CodeJsonValue | undefined
|
||||
try {
|
||||
args = snapshotJsonValue(message.args) as CodeJsonValue | undefined
|
||||
} catch {
|
||||
args = undefined
|
||||
}
|
||||
if (args === undefined) {
|
||||
reply({ type: 'reply', id: message.id, ok: false, message: 'binding arguments must be lossless JSON' })
|
||||
return
|
||||
}
|
||||
void (async () => {
|
||||
try {
|
||||
const resolved = await fn(message.args)
|
||||
const resolved = await fn(args)
|
||||
let value: CodeJsonValue | undefined
|
||||
try {
|
||||
value = snapshotJsonValue(resolved)
|
||||
|
||||
@@ -171,6 +171,19 @@ describe('WorkerCodeRuntime — budgets and containment (real workers)', () => {
|
||||
expect(result.error).toEqual({ kind: 'abort', message: 'too-late' })
|
||||
})
|
||||
|
||||
it('applies the outer-output cap to failures before worker startup', async () => {
|
||||
const capped = await setup({ maxOutputBytes: 64 })
|
||||
const controller = new AbortController()
|
||||
controller.abort('A'.repeat(1_000))
|
||||
const aborted = await capped.runtime.run({ program: 'return 1', bindings: [], signal: controller.signal })
|
||||
expect(aborted).toEqual({ logs: [], error: { kind: 'output-limit', message: 'outer output exceeded 64 bytes' } })
|
||||
|
||||
const minimal = await setup({ maxOutputBytes: 4 })
|
||||
const invalid = await minimal.runtime.run({ program: 'enum E { A }\nreturn 1', bindings: [] })
|
||||
expect(invalid.error?.kind).toBe('output-limit')
|
||||
expect(Buffer.byteLength(JSON.stringify(invalid.logs), 'utf8') + Buffer.byteLength(JSON.stringify(invalid.error?.message), 'utf8')).toBeLessThanOrEqual(4)
|
||||
})
|
||||
|
||||
it('drops a binding resolution that lands after the run settled', async () => {
|
||||
const { runtime } = await setup()
|
||||
const controller = new AbortController()
|
||||
@@ -449,6 +462,41 @@ describe('WorkerCodeRuntime — hostile programs (real workers)', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
it('rejects forged lossy binding arguments again at the host boundary', async () => {
|
||||
const { runtime } = await setup()
|
||||
let calls = 0
|
||||
const result = await runtime.run({
|
||||
program: `
|
||||
const { parentPort } = await import('node:worker_threads');
|
||||
const forged = (id, args) => new Promise((resolve) => {
|
||||
const receive = (message) => {
|
||||
if (message?.type !== 'reply' || message.id !== id) return;
|
||||
parentPort.off('message', receive);
|
||||
resolve(message);
|
||||
};
|
||||
parentPort.on('message', receive);
|
||||
parentPort.postMessage({ type: 'call', id, global: 'tools', name: 'never', args });
|
||||
});
|
||||
const sparse = []; sparse.length = 1;
|
||||
const cycle = {}; cycle.self = cycle;
|
||||
return await Promise.all([
|
||||
forged(8001, new Date()),
|
||||
forged(8002, -0),
|
||||
forged(8003, sparse),
|
||||
forged(8004, cycle),
|
||||
]);
|
||||
`,
|
||||
bindings: tools({ never: async () => { calls += 1; return null } }),
|
||||
})
|
||||
expect(calls).toBe(0)
|
||||
expect(result.value).toEqual([8001, 8002, 8003, 8004].map(id => ({
|
||||
type: 'reply',
|
||||
id,
|
||||
ok: false,
|
||||
message: 'binding arguments must be lossless JSON',
|
||||
})))
|
||||
})
|
||||
|
||||
it('contains throwing getters while snapshotting binding resolutions', async () => {
|
||||
const { runtime } = await setup()
|
||||
const result = await runtime.run({
|
||||
|
||||
Reference in New Issue
Block a user