Repository navigation
Description
Version
codebase-memory-mcp dev (source build, main @ 0d6f26a)
Platform
Linux (x64)
Install channel
Built from source
Binary variant
standard
What happened, and what did you expect?
On any machine whose soft open-files limit is high, every cbm_subprocess_spawn burns hundreds of milliseconds between fork and exec: cbm_posix_child_exec (src/foundation/subprocess.c) closes inherited descriptors with for (fd = 3; fd < max_fd; fd++) close(fd), where max_fd = sysconf(_SC_OPEN_MAX) capped at 1,048,576. With nofile=524288 (Debian 13's default hard limit, and the soft limit inside agent harnesses that raise it, e.g. Claude Code's shell) that is ~524K syscalls ≈ 0.7–0.85 s per spawn — measured: a standalone probe spawning the documented ignoring-tree script sees the child's first shell command execute 716–842 ms after spawn, while the script itself takes 4 ms.
Deterministic test fallout: subprocess_quiet_timeout_kills_ignoring_tree fails every run (FAIL tests/test_subprocess.c:510: ASSERT(ready)) because the fixture waits only 500 ms for the pid file and arms a 750 ms quiet timeout — the tree is SIGKILLed before it ever writes its pids. Secondary effect: each failed run leaks an orphaned cbm-parent/cbm-grandchild tree that ignores SIGTERM (cleanup only knows pids read from the pid file, which never appeared).
Expected: spawn cost independent of RLIMIT_NOFILE — close_range(3, ~0U, 0) on Linux ≥ 5.9 (glibc ≥ 2.34), closefrom() on the BSDs, falling back to the current loop (or /proc/self/fd iteration) elsewhere. This also affects production spawn-heavy paths, not just tests.
Reproduction
- Code: this repository @ 0d6f26a (no indexing involved — subprocess supervision only).
- Command:
ulimit -Sn 524288 && scripts/test.sh --suites subprocess(any Linux where the hard limit allows it; Debian 13 ships hard nofile 524288, so a shell that raises its soft limit — e.g. agent harnesses do — hits this out of the box). - Result:
subprocess_quiet_timeout_kills_ignoring_tree FAIL tests/test_subprocess.c:510: ASSERT(ready)— deterministic, 4/4 runs. Expected: green, as withulimit -Sn 1024(verified: 29/29 pass).
Isolation evidence (standalone probe linking src/foundation/subprocess.c, quiet timeout disabled, same ignoring-tree script as the fixture):
- pid file appears 842 ms after
cbm_subprocess_spawnreturns; withdatemarkers inside the script, the child's first command runs at +716 ms and the whole script needs only 4 ms more — the entire delay sits betweenforkand the shell reaching the script, i.e. theclose()loop. - plain
/bin/sh -c '/bin/sh -c true'in the same shell: 7 ms — the environment itself spawns fast. - with the quiet timeout armed (750 ms) the tree is killed before the
echo, sowait_for_tree_pids(500 ms budget) never succeeds and the orphaned tree survives SIGKILL cleanup (pids unknown), ignoring TERM forever.
Suggested fix: replace the loop in cbm_posix_child_exec with close_range(STDERR_FILENO + 1, ~0U, 0) when available (Linux ≥ 5.9 / glibc ≥ 2.34, FreeBSD 12.2+), keeping the loop as fallback. Optionally also relax the fixture budgets, but with close_range the current budgets are fine.
Logs
$ ulimit -Sn
524288
$ build/c/test-runner subprocess
subprocess_quiet_timeout_kills_ignoring_tree FAIL tests/test_subprocess.c:510: ASSERT(ready)
28 passed, 1 failed
$ ulimit -Sn 1024 && build/c/test-runner subprocess
29 passed
standalone probe (quiet_timeout_ms=0, same spawn options as the fixture):
[ 0 ms] spawn rc=0
[ 0 ms] iter=0 fopen errno=2 (No such file or directory)
...
[500 ms] TIMEOUT waiting for pidfile <- fixture's wait_for_tree_pids budget
[842 ms] pid file: 638002 638009 <- write finally lands
script-side date markers relative to spawn:
start: +716 ms (first command of the -c script)
trap: +717 ms
bg: +719 ms
echo: +720 ms (script body itself: 4 ms)
leaked trees after failed runs (ignore TERM, reparented to init):
359572 /bin/sh -c trap '' TERM; /bin/sh -c '...' cbm-grandchild & ... wait cbm-parent /tmp/cbm-subprocess-tree-mTiXJj
459335 ... /tmp/cbm-subprocess-tree-0Uix7h
459358 ... /tmp/cbm-subprocess-tree-Q83ZXg
Diagnostics trajectory (memory / performance / leak issues)
Project scale (if relevant)
No response
Confirmations
- I searched existing issues and this is not a duplicate.
- My reproduction uses shareable code (a dummy snippet or a public OSS repository), not proprietary code.
Activity
DeusData commentedon Aug 10, 2026
Thank you for the precise benchmark and proposed platform primitives. Confirmed in current
main: child setup insrc/foundation/subprocess.cstill walks file descriptors from 3 to a maximum derived from_SC_OPEN_MAX, so spawn cost scales with the descriptor limit. I have routed this as a high-priority performance bug in the immediate train under the large-repository performance epic.fix(subprocess): close inherited descriptors with kernel range operation
DeusData commentedon Oct 19, 2026
Thank you again for isolating the spawn delay to the descriptor-limit loop and recording the timeout/cleanup consequences. I have no new verified fix or benchmark to report. The existing limit-controlled reproduction is sufficient for maintainer follow-through.
DeusData commentedon Oct 25, 2026
Thank you for isolating the descriptor-limit startup cost and its cleanup consequences. The limit-controlled reproduction is sufficient for the next maintainer step. Sorry for the delay; this is waiting on maintainer follow-up, and I’ll follow up soon.