From fb41b73274934d9b2378ba8fe518304cd3e75411 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Fri, 17 Apr 2026 08:33:41 -0400 Subject: [PATCH] CLAUDE.md: kernel-enforced memory cap (ulimit -v) for asm testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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*. --- CLAUDE.md | 62 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 22ad014..3bdeb6e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 ` 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