fix(code-runtime-python): recheck CPU against the effective clamped soft limit

The settlement-time CPU recheck compared spent CPU against the configured
cpuSeconds, but _clamped may have lowered the effective soft limit to a stricter
inherited value. A program that traps SIGXCPU, burns past the inherited soft,
and returns inside the soft-to-hard gap was checked against the configured value
and falsely reported successful, bypassing the inherited limit. The recheck now
uses the clamped cpu_soft. Adds a regression test that inherits a 1s soft CPU
limit and asserts a SIGXCPU-trapping over-burn is a timeout, not a success.
This commit is contained in:
Chinesezjc
2026-08-31 14:21:57 +08:00
committed by Tianyi Cui
parent d432603b81
commit 6141f0062d
2 changed files with 39 additions and 1 deletions
@@ -766,7 +766,14 @@ async def _run(channel: ProtocolChannel) -> None:
# bounds are the RLIMIT_CPU hard limit and the host wall clock
# (see _make_cpu_enforcer).
die_if_cpu_exhausted = _DIE_IF_CPU_EXHAUSTED
cpu_seconds = int(boot["cpuSeconds"])
# The settlement recheck compares against the EFFECTIVE soft CPU limit
# (`cpu_soft`, clamped to any stricter inherited limit above), NOT the
# configured `cpuSeconds`. When the deployment inherited a soft limit below
# the configured value, a program that traps SIGXCPU, burns past the
# inherited soft, and returns inside the soft-to-hard gap must be reported as
# a timeout — checking the configured value would falsely pass it and bypass
# the inherited limit.
cpu_seconds = cpu_soft
# Same capture, same reason, for the failure path and the send that follows
# it. The reporter was a module-global lookup inside the `except` block, so
# ``import __main__; __main__._SAFE_MODEL_TRACEBACK = ...`` put model code
@@ -434,6 +434,37 @@ describe('PythonCodeRuntime — inherited resource limits', () => {
// The applied SOFT limit is the inherited 5 s, not the configured 30 s.
expect(result.value).toBe(5)
}, 15_000)
it('rechecks CPU at settlement against the effective inherited soft limit', async () => {
// The settlement-time CPU recheck must compare against the EFFECTIVE soft
// limit (`_clamped` may have lowered it to a stricter inherited value), not
// the configured `cpuSeconds`. A program that traps SIGXCPU, burns past the
// inherited soft, and returns inside the soft-to-hard gap would otherwise be
// compared to the configured value and falsely reported successful, bypassing
// the inherited limit. The wrapper sets a 1 s soft CPU limit; the program
// traps SIGXCPU and busy-loops past it, then returns — the recheck must
// re-deliver SIGXCPU so the host classifies the run as a timeout.
const dir = await mkdtemp(join(tmpdir(), 'dsh-cpu-recheck-'))
const wrapper = join(dir, 'python3-cpu-capped')
await writeFile(wrapper, '#!/bin/sh\nulimit -S -t 1\nexec python3 "$@"\n', { mode: 0o755 })
const { runtime } = await setup({ pythonBin: wrapper, cpuSeconds: 30, maxWallMs: 12_000 })
const result = await runtime.run({
program: [
'import signal, time',
// Trap SIGXCPU so the soft limit does not terminate the program; burn
// CPU well past the inherited 1 s soft, then return normally.
'signal.signal(signal.SIGXCPU, lambda *a: None)',
'end = time.process_time() + 2.5',
'while time.process_time() < end:',
' pass',
'return "returned"',
].join('\n'),
bindings: [],
})
// The recheck compares spent CPU against the effective 1 s soft, not 30 s, so
// the run is a timeout rather than a false success.
expect(result.error?.kind).toBe('timeout')
}, 20_000)
})
describe('PythonCodeRuntime — programs and bindings', () => {