CLAUDE.md: kernel-enforced memory cap (ulimit -v) for asm testing

Other agents share this machine. An OOM crash takes their state
down too, not just mine. The earlier "trap + pgrep verify" pattern
depended on me not forgetting; two crashes proved that's not enough.

Adds a mandatory `ulimit -v 524288` (512 MB virt) to the pattern
alongside the existing timeout + trap + verify. The kernel kills
any rogue process at the cap, so even if all three earlier layers
fail simultaneously, shared RAM is untouched.

Verified: an asm process in a runaway (cons n ...) loop SIGKILL'd
by the kernel within ~1 second when capped at 512 MB, before
timeout 5 fired.

Also noted:
- ulimit only affects the current shell + children, so it cannot
  degrade other agents.
- Use `pkill -u "$USER"` explicitly — don't pkill foreign processes.
- Prefer foreground runs with timeout over backgrounding when
  feasible; the .lsp can exit on its own via *max-requests*.
This commit is contained in:
russell@unturf.com 2026-04-17 08:33:41 -04:00
parent 0f894734a2
commit fb41b73274

View file

@ -70,41 +70,51 @@ and failed to verify it was gone before moving on. The first crash
added this discipline section; the second proved the discipline
needed teeth. Hence the MANDATORY checklist below.
**MANDATORY pattern for any asm server test in a shell block:**
**Shared-machine context:** other agents run on this box. An OOM
crash is not just "my problem" — it takes their state down too.
The rules below are belt, suspenders, AND a parachute so that even
if two safeguards fail, the kernel itself backstops.
**MANDATORY pattern — every asm/uncommonlisp test in a shell block:**
```bash
# 1. set -e so any failure aborts before leaving a zombie
set -e
# 2. trap ALWAYS fires, even if a later command fails or the script is killed
trap 'pkill -9 -u "$USER" -f "examples/http-server|asm/uncommonlisp.*bench" 2>/dev/null || true' EXIT INT TERM
# 3. wrap the server in `timeout` so even a total trap failure has a hard ceiling
timeout 30 asm/uncommonlisp < server.lsp &
set -e # (1) fail-fast
ulimit -v 524288 # (2) KERNEL CAP: 512 MB virt
# process gets SIGKILL at cap, no matter what
trap 'pkill -9 -u "$USER" -f "examples/http-server|asm/uncommonlisp" 2>/dev/null || true' \
EXIT INT TERM # (3) cleanup always fires
timeout 30 asm/uncommonlisp < server.lsp & # (4) wall-clock ceiling
SPID=$!
# 4. do the work ... (client requests, RSS measurements, etc.)
# 5. explicit kill + wait, not just "assume the trap handles it"
kill -9 $SPID 2>/dev/null; wait $SPID 2>/dev/null
# 6. VERIFY before ending the block
pgrep -u "$USER" -f 'asm/uncommonlisp|examples/http-server' && exit 1 || true
# ... do the work (curl requests, measurements, etc.) ...
kill -9 $SPID 2>/dev/null; wait $SPID 2>/dev/null # (5) explicit cleanup
# (6) VERIFY the block is clean before moving on
pgrep -u "$USER" -f 'asm/uncommonlisp|examples/http-server' \
&& { echo "STRAGGLER"; exit 1; } || true
```
Six layers. Bypass any one and the next still catches. Two crashes
happened when I had only layers 3-5; the kernel cap (2) is what turns
"if I forget" from "fox reboots" into "my one rogue process dies at
512 MB without touching shared RAM."
Additional rules:
- Never start a bg asm process without `timeout N` wrapping it, even
for a "quick test." The mental overhead is zero; the consequence
of forgetting is a crashed machine.
- Bound iterations **inside the .lsp** (e.g., `(if (> n 1000) (exit 0)
(loop (+ n 1)))`) — never rely on external kill signals alone.
- After any block that spawned background processes, run:
`pgrep -u "$USER" -f 'asm/uncommonlisp|examples/http-server'`
and expect NO output. If output, kill with `pkill -9 -f ...` and
investigate. Do not move on.
- Use `examples/http-server.lsp`'s built-in `*max-requests* = 50000`
ceiling (never comment out, never raise for long-running tests).
- Python + C have real GCs; the hazard is asm-specific. But treat
backgrounded Python + C servers with the same hygiene — they
block ports and confuse later tests.
- `ulimit -v` affects only the shell it runs in and its children, so
it cannot degrade anyone else's agents. Always set it before
backgrounding any uncommonlisp process.
- Bound iterations **inside the .lsp** (e.g. `*max-requests* = 50000`
in `examples/http-server.lsp`). Never raise for long-running tests.
- Use `pkill -u "$USER" -f <pattern>` not `pkill` alone — others may
have their own processes on this machine.
- Prefer FOREGROUND runs when possible: `timeout 10 asm/uncommonlisp
< test.lsp` with the `.lsp` exiting on its own beats backgrounding.
- C has Boehm GC via `GC_MALLOC`. Python has Python's GC. Asm has
neither. Risk scales with how long the asm process lives.
- If `ulimit` is unavailable (some container setups), use
`systemd-run --user --scope -p MemoryMax=512M -- asm/uncommonlisp ...`
as the equivalent cgroup-based cap.
## MOAD Scanner