a73x

d529c286

tools: valgrind recipe for the QUIC/C stack + first-run record

a73x   2026-08-14 09:29

Commit message
tools: valgrind recipe for the QUIC/C stack + first-run record

Non-gating on purpose: the 10-50x slowdown turns every timing-sensitive
path into flake territory, and the C side only changes when deps/quic
does. This is the one lens the Zig-side verdict (6a) cannot provide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

docs/decisions.md
Old New
@@ -3205,3 +3205,9 @@ two dialects.
3205 zlint targets). The compiler's native strictness (unused locals/params, 3205 zlint targets). The compiler's native strictness (unused locals/params,
3206 shadowing) already owns the highest-value lint classes; re-try at the 3206 shadowing) already owns the highest-value lint classes; re-try at the
3207 next toolchain bump if its 0.16 support lands. 3207 next toolchain bump if its 0.16 support lands.
3208 - **valgrind first run (2026-08-14): pending install.**
3209 Recipe at tools/valgrind-quic.sh, non-gating (10-50x slowdown distorts every
3210 timing path). `sudo -n pacman -S --noconfirm valgrind` was refused with
3211 `sudo: a password is required` (exit 1): this box grants no non-interactive
3212 sudo, so the run half is pending install. valgrind not installable
3213 non-interactively; run `sudo pacman -S valgrind` then the script.
tools/valgrind-quic.sh
Old New
@@ -0,0 +1,46 @@
1 #!/bin/sh
2 # tools/valgrind-quic.sh — periodic deep leak run over the QUIC/C stack.
3 #
4 # NON-GATING and wired into no build step, on purpose: valgrind runs
5 # 10-50x slow, which distorts every timing-sensitive path (handshakes,
6 # keepalives, settle windows) into flake territory. Its unique value is
7 # the C side — wolfSSL/ngtcp2 allocate with malloc, which the Zig
8 # DebugAllocator verdict (6a) never sees. Run it by hand after touching
9 # the QUIC stack or bumping deps/quic.
10 #
11 # Usage: tools/valgrind-quic.sh [path/to/muxd [path/to/muxa]]
12 # (defaults to zig-out/bin — run `zig build` first; Debug build is the
13 # point: Zig debug builds carry valgrind client requests natively)
14 set -eu
15 MUXD="${1:-zig-out/bin/muxd}"
16 MUXA="${2:-zig-out/bin/muxa}"
17 command -v valgrind >/dev/null || { echo "valgrind is not installed"; exit 1; }
18 [ -x "$MUXD" ] && [ -x "$MUXA" ] || { echo "binaries missing — run zig build"; exit 1; }
19 TMP="${TMPDIR:-/tmp}/mux-valgrind-$$"
20 mkdir -p "$TMP"
21 trap 'kill "$VGPID" 2>/dev/null; rm -rf "$TMP"' EXIT INT TERM
22 VGPID=""
23 # Hermetic key: keygen into a private XDG home, --key it explicitly.
24 XDG_CONFIG_HOME="$TMP/cfg"; export XDG_CONFIG_HOME
25 "$MUXD" keygen > /dev/null
26 KEY="$TMP/cfg/mux/key"
27 SOCK="$TMP/vg.sock"
28 PORT=$((47000 + ($$ % 900)))
29 valgrind --leak-check=full --error-exitcode=99 --log-file="$TMP/vg.log" \
30 "$MUXD" run --sock "$SOCK" --shell /bin/sh \
31 --quic "127.0.0.1:$PORT" --key "$KEY" &
32 VGPID=$!
33 # valgrind start is SLOW; give the bind a full minute.
34 i=0
35 while [ ! -S "$SOCK" ] && [ "$i" -lt 600 ]; do sleep 0.1; i=$((i + 1)); done
36 [ -S "$SOCK" ] || { echo "daemon never bound under valgrind"; exit 1; }
37 # One real command over QUIC — handshake, frames, teardown — with a
38 # timeout sized for valgrind's clock, then a clean stop so every exit
39 # path (and the allocator teardown) runs.
40 "$MUXA" run --quic "127.0.0.1:$PORT" --key "$KEY" --timeout 60000 "echo vg-probe" \
41 || echo "muxa run failed under valgrind (timing?) — the leak summary below still stands"
42 "$MUXD" stop --sock "$SOCK"
43 wait "$VGPID" || true
44 VGPID=""
45 echo "---- valgrind summary ----"
46 grep -E "definitely lost|indirectly lost|ERROR SUMMARY" "$TMP/vg.log" || cat "$TMP/vg.log"