fix(code-runtime-python): guard teardown, log prefix, and settlement flush

Four independent corrections in the run lifecycle.

`killGroup` signalled `-child.pid` with a raw `process.kill`. Node keeps the
numeric `child.pid` after the leader is reaped and only clears its internal
handle, so `child.kill()` refuses while the raw call does not; `close` can
trail `exit` by seconds when a pipe-holding descendant keeps the streams open.
A recycled pgid could therefore receive this run's SIGTERM and armed SIGKILL.
`groupEmpty()` does not cover it: it reports whether the group has members, not
whether they are ours, and it first runs after the signal. The leader's start
time is now read at spawn and re-checked before each signal, matching the
position packages/subprocess/subprocess-local already states
("ProcessIdentity ... preventing teardown escalation after PID reuse"). Kept
local rather than depending on that package, which would add an architectural
edge. Linux reads /proc; Darwin has no /proc, so the reader reports undefined
and the guard degrades to the previous behavior instead of forking `ps` on a
teardown path.

`_push_bounded_prefix` built `(*self._pending, extra)`, copying every pending
reference into a same-size tuple before the bounded loop. For a
single-character drip that is a second pointer array as large as the list:
measured +80 MiB of tuple over a 40 MiB list for 5.2M chunks, the allocation
the bounded prefix exists to avoid. It now iterates the list in place and
handles `extra` in the loop's `else`; 4000 randomized inputs produce byte-identical
prefixes.

The settlement `flush_out()`/`flush_err()` ran outside any guard while `done`
was already decided, so a flush raising under memory pressure skipped
`send_done` and downgraded a child-classified `exception` into a host-side
`worker-exit`. Both are now wrapped, swallowing only the log tail.

The boot re-check's `if effective_soft != RLIM_INFINITY` was dead: `_clamped`
is asked for a finite `addr_bytes` on both sides and each branch returns that
value or a `min` with an inherited bound, so RLIM_INFINITY is unreachable. The
guard could only ever have skipped the re-check it claimed to protect.
This commit is contained in:
Chinesezjc
2026-08-31 14:26:23 +08:00
committed by Tianyi Cui
parent 8f7d9121d1
commit e6b547bef4
3 changed files with 122 additions and 17 deletions
@@ -3,8 +3,8 @@ import { mkdtemp, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, dirname, join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { PythonCodeRuntime } from '../src/index.ts'
import { Context } from '@deepseek-ai/cordis'
import { PythonCodeRuntime, readProcessStart } from '../src/index.ts'
import { logTruncationMarker } from '../src/protocol.ts'
import type { Config } from '../src/index.ts'
import type { CodeBindingFunction, CodeJsonValue, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
@@ -383,6 +383,32 @@ describe('PythonCodeRuntime — seam descriptors and misuse', () => {
}, 15_000)
})
describe('PythonCodeRuntime — process identity', () => {
it('reads a live process start time and distinguishes it from an absent pid', () => {
// The teardown guard signals `-child.pid` with a RAW `process.kill`, which
// (unlike `child.kill()`) has no handle check, so it would reach a recycled
// pgid during the window between the leader being reaped and `close` firing.
// A pid alone cannot separate the original from its replacement -- both
// answer `kill(pid, 0)` -- so the guard compares START TIME, and this pins
// that the reading is stable for one process and absent for a pid that
// cannot be read.
const own = readProcessStart(process.pid)
if (process.platform === 'linux') {
// Same process, two reads: the identity must be stable, or the guard would
// refuse to signal its own live group.
expect(own).toBeDefined()
expect(readProcessStart(process.pid)).toBe(own)
// Pid 0 is never a readable /proc entry, so the guard degrades to
// undefined rather than throwing on a teardown path.
expect(readProcessStart(0)).toBeUndefined()
} else {
// Darwin has no /proc: the reader reports undefined, and `killGroup` then
// keeps its pre-existing behavior instead of paying a `ps` fork per signal.
expect(own).toBeUndefined()
}
})
})
describe('PythonCodeRuntime — inherited resource limits', () => {
it('runs under an inherited hard limit tighter than addressSpaceMb', async () => {
// An unprivileged process may lower a hard rlimit but never raise it. Under