AtlatestRepositorysigil-vt

sigil-vt / tree / spikesanitizer-selftest.c

1/* Does the sanitizer build ACTUALLY detect a heap over-read?
2 *
3 * CONTRACT (fuzz.sh depends on this):
4 * killed by the sanitizer, printing an AddressSanitizer diagnostic
5 * = CAUGHT. The toolchain's ASan is real. This process never
6 * regains control, so it can NEVER exit 0 on success.
7 * exit 1 = NOT caught. The build is blind, and any clean fuzz run from
8 * it is meaningless.
9 *
10 * fuzz.sh matches the DIAGNOSTIC, not the exit status: a bare non-zero exit
11 * could equally mean the binary failed to launch, which must never read as a
12 * pass.
13 *
14 * WHY THIS EXISTS (t-d4c7). The M2 gate reported "5,000,000 iterations,
15 * ASan/UBSan clean" for months while ASan WAS NOT PRESENT AT ALL. The pinned
16 * zig ships no ASan runtime: `zig cc -fsanitize=address` alone fails to link
17 * (undefined __asan_report_load4), and `-fsanitize=address,undefined` links but
18 * SILENTLY DROPS ASan — the binary contains zero __asan symbols. Only UBSan
19 * survived. A real heap over-read shipped straight through that green.
20 *
21 * The trap that makes this hard to notice: a naive self-test PASSES anyway.
22 * If the malloc is visible in the same function, UBSan's __builtin_object_size
23 * check fires and you conclude "sanitizers work". They don't — you measured
24 * UBSan. So the allocation here is deliberately behind a noinline function, out
25 * of the compiler's static reach, exactly like sb_push. Then ONLY ASan's heap
26 * redzones can catch it.
27 *
28 * Keep this shape. If you "simplify" the malloc back into main(), this file
29 * silently starts passing on a broken toolchain again.
30 */
31#include <stdlib.h>
32#include <stdio.h>
34/* Opaque to __builtin_object_size — mirrors sb_push allocating a row. */
35__attribute__((noinline)) static int *make_block(int n) {
36 int *p = (int *)malloc((size_t)n * sizeof(int));
37 if (!p) exit(2);
38 for (int i = 0; i < n; i++) p[i] = i;
39 return p;
42static long sink = 0;
44int main(void) {
45 /* 160 ints = 640 bytes: the same shape as a 40-column scrollback row. */
46 int *p = make_block(160);
47 /* Read at int index 524 (byte 2096) — 1456 bytes past the end. This is
48 * precisely the t-d4c7 over-read: a 40-wide row read at 132 columns. */
49 sink ^= p[524];
50 /* Reaching here means the sanitizer did NOT catch a 1456-byte heap
51 * over-read. Print, then fail loudly. */
52 printf("sink=%ld\n", sink);
53 free(p);
54 return 1;