From c05edef9853107a3ad610d0b8fc7334bdb55c806 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 05:15:22 +0700 Subject: [PATCH] Write down that nothing has ever run under a sanitizer --- NEXT.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/NEXT.md b/NEXT.md index 2d61380..670413f 100644 --- a/NEXT.md +++ b/NEXT.md @@ -63,6 +63,40 @@ merged; 3 and 4 are still running. `Emit.redefinition` already takes and is already tested for. The second also unblocks source interleaving in the disassembly buffer. +### Queued — the runtime under a sanitizer + +Nothing has ever run under ASan or UBSan on the test path. `grep -i 'sanitize\|asan\|valgrind'` over `lib/ bin/ test/ +runtime/ vendor/` returns nothing at all. This is not a big project — the whole C surface is about 1,260 lines +(`flan_rt.c` 394, `flan_dev.c` 219, `flan_agent.c` 647) plus what `shim.ml` generates. + +**The bugs are not in allocation.** There are four `malloc`/`calloc`/`strdup` sites in the entire runtime and every one +is allocate-once-never-free by design — `rt_args` says so in its own comment — so LeakSanitizer would mostly produce +suppressions. The risk is **fixed static buffers with bounds arithmetic**, and the ones with no coverage are already +written down: `scratch[SCRATCH]` and `escaped[ESCAPE_MAX]` in `flan_rt.c`, the 4K result cap, the registry overflow +guard, and `SNAP_MAX`/`SNAP_NAMES` from the restart snapshot. + +There is evidence the sweep pays. Checking `flan_escape_bytes` by hand against lengths 0–1300 under ASan with a red +zone found the guard correct but its comment understating its own reserve by four bytes — worst output 1021 into 1024. +That was one buffer, found by looking. + +The shape: + +1. A `--sanitize` flag beside `--debug` in `lib/build.ml`, reaching both the clang run over the `.ll` and the runtime's + own C. +2. Run the existing corpus under it. `test/programs/` is about forty programs with pinned output — a second pass over + them is the cheapest coverage available here, and needs no new test written. +3. **UBSan is worth more than ASan**, with one exclusion that is not optional: arithmetic wraps by design, so + `-fno-sanitize=signed-integer-overflow` or every program trips on the first `+`. What is left is real — shift UB + (`(<< 1 32)` compiled to a bare `retq` at -O2, see Sharp edges), alignment, and the f32→i32 cast on NaN or an + infinity that `floor-f32` guards by hand and nothing else does. +4. The variant worth its own run: **ASan with `--no-bounds-checks`**. That asks whether the bounds checks are the only + thing between the language and corruption, which the checked build cannot ask. + +Two limits, so nobody is surprised. raylib and libm are not instrumented, so the windowed examples are noise and the +headless corpus is the target — `sand-headless`, `values`, `machine`, `virtual-controls-headless`. And ASan does not +see uninitialised reads, which is where `zeroed` and struct padding live; that wants Valgrind as a slower second pass, +because MSan needs every dependency instrumented and raylib settles that. + ### Queued — the `print-*` family goes `print` and `println` are the whole printing surface. `print-str`, `print-i64`, `print-f64`, `print-bytes`,