Write down that nothing has ever run under a sanitizer

This commit is contained in:
Joseph Ferano 2026-09-12 05:15:22 +07:00
parent afec482722
commit c05edef985

34
NEXT.md
View File

@ -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 01300 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`,