mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-09 04:02:35 +00:00
fix(code-runtime-python): catch the model exception with a pre-program local exception class
The _run outer try/except used the module-global BaseException, which the program (running as __main__) can rebind: __main__.BaseException = RuntimeError made the except resolve to RuntimeError, so a subsequent ValueError escaped _run with no done frame and misreported the run as worker-exit. Bind BaseException into a _run local before the program runs so the catch is immune; a regression test rebinds BaseException and raises, asserting an exception, not a worker-exit. Also correct the NUL-escape comment text: the JSON escape-result side is \^@ (6 bytes, the valid JSON NUL escape), not \x00, so the 6x-budget arithmetic in the comments is self-consistent. Register the BaseException-rebind case in the settlement note Testing (en + zh) and re-record the pairing.
This commit is contained in:
@@ -998,6 +998,15 @@ async def _run(channel: ProtocolChannel) -> None:
|
||||
_os_write_local = _os_write
|
||||
_memoryview_local = _memoryview
|
||||
_fallback_frame_local = _FALLBACK_DONE_FRAME
|
||||
# The exception class the outer try/except catches is bound into a LOCAL
|
||||
# here, before the program runs. This bootstrap is `__main__`, so
|
||||
# `__main__.BaseException = RuntimeError` would otherwise rebind the module
|
||||
# global `BaseException` the `except BaseException` clause resolves at
|
||||
# runtime, and a subsequent `ValueError` would then not match the clause —
|
||||
# escaping `_run` with no `done` frame and misreporting the run as a
|
||||
# `worker-exit`. Binding the class into a local makes the catch immune to a
|
||||
# one-line rebind.
|
||||
_BaseException = BaseException
|
||||
|
||||
def send_done(payload: dict[str, Any] | str) -> None:
|
||||
try:
|
||||
@@ -1061,7 +1070,7 @@ async def _run(channel: ProtocolChannel) -> None:
|
||||
flush_out()
|
||||
flush_err()
|
||||
done = _done_with_value(value, max_value_bytes)
|
||||
except BaseException as exc: # noqa: BLE001 -- report every failure to host
|
||||
except _BaseException as exc: # noqa: BLE001 -- report every failure to host; `_BaseException` is a pre-program local, not a rebindable module global
|
||||
done = {
|
||||
"type": "done",
|
||||
"error": {
|
||||
|
||||
@@ -848,7 +848,7 @@ describe('PythonCodeRuntime — programs and bindings', () => {
|
||||
|
||||
it('bounds a control-char-dense native residual by serialized cost, not raw length', async () => {
|
||||
// A newline-free NUL flood passes the cheap `length + 3` lower bound at a
|
||||
// raw length well under the budget, but each NUL serializes to `\x00` (6
|
||||
// raw length well under the budget, but each NUL serializes to `\^@` (6
|
||||
// bytes), so the true JSON cost is ~6x. The ledger must charge that
|
||||
// serialized cost — and `jsonStringCostUpTo` must measure it WITHOUT
|
||||
// allocating the escaped copy, so a near-budget line under a large
|
||||
@@ -1513,6 +1513,29 @@ describe('PythonCodeRuntime — programs and bindings', () => {
|
||||
expect(result.error?.kind).not.toBe('worker-exit')
|
||||
}, 15_000)
|
||||
|
||||
it('still reports a model exception when the program rebinds BaseException', async () => {
|
||||
// `_run`'s outer try/except catches the program's failure and builds a
|
||||
// `done` frame. The clause previously used the module-global `BaseException`,
|
||||
// which the program (running as `__main__`) can rebind: `__main__.BaseException
|
||||
// = RuntimeError` makes the `except BaseException` resolve to `RuntimeError`,
|
||||
// so a subsequent `ValueError` does not match and escapes `_run` with no
|
||||
// `done` frame — misreporting the run as a `worker-exit`. The exception class
|
||||
// is now bound into a `_run` LOCAL before the program runs, so the rebind
|
||||
// cannot change which class the clause catches; the run must still report an
|
||||
// `exception`, not a `worker-exit`.
|
||||
const { runtime } = await setup({ maxWallMs: 10_000 })
|
||||
const result = await runtime.run({
|
||||
program: [
|
||||
'import __main__',
|
||||
'__main__.BaseException = RuntimeError',
|
||||
'raise ValueError("real failure")',
|
||||
].join('\n'),
|
||||
bindings: [],
|
||||
})
|
||||
expect(result.error?.kind).toBe('exception')
|
||||
expect(result.error?.kind).not.toBe('worker-exit')
|
||||
}, 15_000)
|
||||
|
||||
it('bounds an over-cap exception-group nesting on the copy', async () => {
|
||||
// Exception groups link through `exceptions`, not the cause/context
|
||||
// dunders, so the cap has to count that edge too — otherwise a deeply
|
||||
@@ -3269,7 +3292,7 @@ describe('PythonCodeRuntime — hostile peer', () => {
|
||||
}, 20_000)
|
||||
|
||||
it('rejects a control-heavy oversized completion on its length, not its escaped copy', async () => {
|
||||
// Every "\x00" escapes to the six bytes "\x00", so the escaped form of a
|
||||
// Every "\x00" escapes to the six bytes "\^@", so the escaped form of a
|
||||
// 40 MB string is ~240 MB. The walk must refuse on the cheap
|
||||
// `len(current) + 2` lower bound; the 384 MiB address space holds the raw
|
||||
// string but not its escaped expansion, so a pre-escape check dies on
|
||||
@@ -3523,7 +3546,7 @@ describe('PythonCodeRuntime — hostile peer', () => {
|
||||
|
||||
it('caps a control-heavy exception diagnostic by its serialized cost, not raw bytes', async () => {
|
||||
// The diagnostic crosses fd 3 inside a JSON frame where a control character
|
||||
// escapes sixfold (a NUL is one raw byte, six as `\x00`). Capping by raw
|
||||
// escapes sixfold (a NUL is one raw byte, six as `\^@`). Capping by raw
|
||||
// UTF-8 length would let a NUL-heavy message near maxValueBytes serialize to
|
||||
// ~6x that and breach the frame ceiling — the silent worker-exit inversion
|
||||
// the load-time cap check exists to prevent. The child meters the diagnostic
|
||||
@@ -4157,7 +4180,7 @@ describe('PythonCodeRuntime — hostile peer', () => {
|
||||
it('drops a forged oversized log frame on its code-unit lower bound, before escaping it', async () => {
|
||||
// A forged `log` frame carrying a control-heavy string sits below the
|
||||
// 256 MiB fd-3 frame ceiling but escapes several-fold: 24 MiB of NULs
|
||||
// becomes ~144 MiB of `\x00`. Charging it required building that escaped
|
||||
// becomes ~144 MiB of `\^@`. Charging it required building that escaped
|
||||
// copy first, so a 32-byte maxLogBytes could still force a
|
||||
// hundreds-of-megabytes host allocation. The cheap `length + 3` lower bound
|
||||
// truncates it instead. The host's own heap is what is under test, so keep
|
||||
|
||||
Reference in New Issue
Block a user