Commit2b0bca33Recorded16 Jul 2026Repositorysigil-vt

M2 gate: ASan/UBSan fuzz driver for the untrusted byte path

Message

native/vt-fuzz.c + spike/fuzz.sh: standalone fuzz+sanitizer driver. The pure C core (vt.c under -DVTFUZZ, VM glue guarded out) compiles free of libsigil and runs under zig cc -fsanitize=address,undefined. Deterministic 68-sequence corpus x 9 geometries + a biased-random mutation loop over the byte path (feedbyte) AND the string bulk-print path (feedrunscan), interleaving resize/reset/alt/drain. 5,000,000 iterations + corpus: CLEAN (no ASan/UBSan/leak). Optional libFuzzer entry under -DVT_LIBFUZZER.

vt.c: VT_FUZZ guards isolate the pure emulator from the Sigil glue; no change to the normal build (95/95 conformance still green).

Changed
 .gitignore       |   2 ++
 native/vt-fuzz.c | 250 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 native/vt.c      |  17 +++++++++---
 spike/fuzz.sh    |  17 ++++++++++++
 4 files changed, 283 insertions(+), 3 deletions(-)
Diff
.gitignoremodified
@@ -1,2 +1,4 @@
1
build/
2
.sigil/
+3
spike/fuzz-run.log
+4
staging/
native/vt-fuzz.cadded
@@ -0,0 +1,250 @@
+1
/*
+2
* sigil-vt fuzz + sanitizer driver — the M2 GATE.
+3
*
+4
* The VT parser processes UNTRUSTED pty output, so moving it into C moves the
+5
* trust boundary into C: a parser bug becomes a memory-safety bug fed by
+6
* arbitrary program output. This driver exercises the pure emulator core
+7
* (native/vt.c, included with VT_FUZZ so the Sigil VM glue is excluded) under
+8
* ASan + UBSan:
+9
*
+10
* 1. a deterministic seed corpus of real escape sequences (from the
+11
* conformance fixtures), replayed into fresh emulators of several
+12
* geometries;
+13
* 2. a large biased-random mutation loop over vt feed_byte (the untrusted
+14
* byte path), interleaving resize / reset / alt-screen / drain, so the
+15
* whole state machine + scrollback + resize reflow are stressed.
+16
*
+17
* Build (see spike/fuzz.sh):
+18
* zig cc -std=c99 -O1 -g -DVT_FUZZ -fsanitize=address,undefined \
+19
* -fno-sanitize-recover=all native/vt-fuzz.c -o build/vt-fuzz
+20
* ./build/vt-fuzz [iterations] # default 3,000,000
+21
*
+22
* Optional coverage-guided libFuzzer entry under -DVT_LIBFUZZER.
+23
*
+24
* Deterministic by construction (fixed-seed xorshift; no time/rand) so a
+25
* failure reproduces exactly.
+26
*/
+27
+28
#ifndef VT_FUZZ
+29
#define VT_FUZZ
+30
#endif
+31
#include "vt.c"
+32
+33
/* ---- deterministic PRNG (xorshift64) ------------------------------------- */
+34
static uint64_t g_rng = 0x9E3779B97F4A7C15ULL;
+35
static uint32_t rnd(void) {
+36
uint64_t x = g_rng;
+37
x ^= x << 13; x ^= x >> 7; x ^= x << 17;
+38
g_rng = x;
+39
return (uint32_t)(x >> 11);
+40
}
+41
static uint32_t rnd_below(uint32_t n) { return n ? rnd() % n : 0; }
+42
+43
/* ---- pure drain (mirrors the glue drains; frees event payloads) ---------- */
+44
static void fuzz_drain(Vt *t) {
+45
t->out_len = 0;
+46
for (int i = 0; i < t->nevents; i++) free(t->events[i].payload);
+47
t->nevents = 0;
+48
for (int i = 0; i < t->rows; i++) t->dirty[i] = 0;
+49
t->alldirty = 0;
+50
}
+51
+52
/* ---- read every accessor path so their scans are covered ----------------- */
+53
static volatile int g_sink;
+54
static void touch_accessors(Vt *t) {
+55
g_sink ^= t->cols ^ t->rows ^ t->cur_row ^ t->cur_col;
+56
g_sink ^= t->curvis ^ t->alt_active ^ t->bracket ^ t->appcur;
+57
g_sink ^= t->mouse ^ t->curstyle ^ (int)t->attr ^ t->sb_size;
+58
/* walk the active grid + a scrollback row (bounds coverage) */
+59
for (int r = 0; r < t->rows; r++) {
+60
const int32_t *row = t->grid + (size_t)r * t->cols * 4;
+61
for (int c = 0; c < t->cols; c++) g_sink ^= row[c * 4];
+62
}
+63
int32_t *sb = sb_get(t, rnd_below(t->sb_size ? t->sb_size + 1 : 1));
+64
if (sb) g_sink ^= sb[0];
+65
g_sink ^= color_256_rgb((int)rnd_below(300)); /* palette incl. out-of-range */
+66
}
+67
+68
/* ---- feed a buffer of raw bytes (the untrusted byte path) ---------------- */
+69
static void feed_buf(Vt *t, const uint8_t *data, size_t len) {
+70
for (size_t i = 0; i < len; i++) feed_byte(t, data[i]);
+71
}
+72
+73
/* ---- feed via the STRING path: decode to codepoints and run the ground-
+74
* state bulk-print scan (mirrors nat_feed). Also exercises print_run. -------*/
+75
static void feed_string_path(Vt *t, const uint8_t *data, size_t len) {
+76
int32_t cps[256];
+77
int nc = 0;
+78
size_t i = 0;
+79
while (i < len) {
+80
unsigned char b = data[i];
+81
int32_t cp; int adv;
+82
if (b < 0x80) { cp = b; adv = 1; }
+83
else if (b < 0xE0 && i + 1 < len) { cp = ((b & 0x1F) << 6) | (data[i+1] & 0x3F); adv = 2; }
+84
else if (b < 0xF0 && i + 2 < len) { cp = ((b & 0x0F) << 12) | ((data[i+1] & 0x3F) << 6) | (data[i+2] & 0x3F); adv = 3; }
+85
else if (i + 3 < len) { cp = ((b & 0x07) << 18) | ((data[i+1] & 0x3F) << 12) | ((data[i+2] & 0x3F) << 6) | (data[i+3] & 0x3F); adv = 4; }
+86
else { cp = 0xFFFD; adv = 1; }
+87
if (cp > 0x10FFFF || (cp >= 0xD800 && cp < 0xE000)) cp = 0xFFFD;
+88
cps[nc++] = cp;
+89
i += adv;
+90
if (nc == 256) { feed_run_scan(t, cps, nc); nc = 0; }
+91
}
+92
if (nc) feed_run_scan(t, cps, nc);
+93
}
+94
+95
/* Drive one emulator through an input buffer with interleaved geometry churn.
+96
* Used both for corpus replay and (with random data) the mutation loop. */
+97
static void run_input(const uint8_t *data, size_t len, int cols, int rows) {
+98
if (cols < 1) cols = 1; if (cols > 400) cols = 400;
+99
if (rows < 1) rows = 1; if (rows > 200) rows = 200;
+100
Vt *t = vt_new(cols, rows, 64);
+101
if (!t) return;
+102
size_t i = 0;
+103
while (i < len) {
+104
size_t chunk = 1 + rnd_below(64);
+105
if (chunk > len - i) chunk = len - i;
+106
/* alternate the byte path and the string bulk-print path */
+107
if (rnd_below(4) == 0) feed_string_path(t, data + i, chunk);
+108
else feed_buf(t, data + i, chunk);
+109
i += chunk;
+110
switch (rnd_below(24)) {
+111
case 0: term_resize(t, 1 + rnd_below(120), 1 + rnd_below(50)); break;
+112
case 1: term_resize(t, 1 + rnd_below(400), 1 + rnd_below(200)); break;
+113
case 2: term_reset(t); break;
+114
case 3: mark_all(t); break;
+115
case 4: fuzz_drain(t); break;
+116
case 5: touch_accessors(t); break;
+117
default: break;
+118
}
+119
}
+120
touch_accessors(t);
+121
fuzz_drain(t);
+122
vt_free(t);
+123
}
+124
+125
/* ---- the seed corpus: real escape sequences from the conformance suite --- */
+126
static const char *g_corpus[] = {
+127
"hello",
+128
"ab\r\ncd",
+129
"abc\rX",
+130
"ab\x08X",
+131
"0123456789AB",
+132
"\033[?7l0123456789AB",
+133
"aa\r\nbb\r\ncc",
+134
"a\tb",
+135
"\033[3;4HX",
+136
"\033[3;4H\033[A\033[2DX",
+137
"abc\033[10;20HZ",
+138
"\033[2;2H\033[3B\033[2CX",
+139
"hi\033[5GX",
+140
"hi\033[3dX",
+141
"abcdef\033[4G\033[K",
+142
"abcdef\033[4G\033[1K",
+143
"abcdef\033[2K",
+144
"aaaaaa\r\nbbbbbb\r\ncccccc\033[2;3H\033[J",
+145
"aaaaaa\r\nbbbbbb\r\ncccccc\033[2;3H\033[1J",
+146
"abcdef\033[3G\033[2@XY",
+147
"abcdef\033[3G\033[2P",
+148
"abcdef\033[3G\033[2X",
+149
"a\r\nb\r\nc\r\nd\033[2;1H\033[L",
+150
"a\r\nb\r\nc\r\nd\033[2;1H\033[M",
+151
"top\033[2;4r\033[2;1Hl1\r\nl2\r\nl3\r\nl4",
+152
"a\r\nb\r\nc\r\nd\r\ne\033[2;4r\033[2S",
+153
"a\r\nb\r\nc\r\nd\r\ne\033[2;4r\033[1T",
+154
"a\r\nb\r\nc\033[2;4r\033[2;1H\033M",
+155
"\033[2;4r\033[?6h\033[1;1HX",
+156
"\033[1;31mR\033[0mp",
+157
"\033[3;4;7mx",
+158
"\033[38;5;196m\033[48;5;22mc",
+159
"\033[38;2;255;128;0mt",
+160
"\033[38:5:99mQ",
+161
"\033[38:2::255:128:0;4mW",
+162
"\033[38:2:10:20:30mV",
+163
"\033[91mb\033[39md",
+164
"\033[1;31ma\033[22mb",
+165
"\033[48;5;19m\033[2J",
+166
"main\033[?1049h\033[1;1HALT\033[?1049l",
+167
"\033[?1049ha\r\nb\r\nc\r\nd",
+168
"\033[31m\033[2;3H\0337\033[0m\033[1;1H\0338X",
+169
"\033#8",
+170
"hi\033[31m\033cx",
+171
"\033]0;my title\x07x",
+172
"\033]2;st title\033\\y",
+173
"\033]52;c;aGVsbG8=\x07z",
+174
"\033[4 q",
+175
"\033[?2004h",
+176
"\033[?25l",
+177
"\033[?1h",
+178
"abc\033[2G\033[4hXY",
+179
"\033[?1002h\033[?1006h",
+180
"\033[3;4H\033[6n",
+181
"\033[5n",
+182
"\033[c",
+183
"\033[999999999999H" "ok",
+184
"\033[38;2mx",
+185
"\033P malicious dcs payload \033\\ok",
+186
"\033[999999999Ix",
+187
"\033[?9999h\033[<5m\033(0ok",
+188
/* hostile / adversarial shapes */
+189
"\033[1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1;1m",
+190
"\033]0;" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+191
"\033[38:2:1:2:3:4:5:6:7:8:9m",
+192
"\033[;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;m",
+193
"\xc3\xa9\xe2\x86\x92\xf0\x9f\x98\x80", /* multibyte UTF-8 */
+194
"\xed\xa0\x80\xf4\x90\x80\x80\xe0\x80\xa8", /* surrogate/overlong/range */
+195
"\xff\xfe\xfd\x80\x81\xc0\xc1", /* invalid lead bytes */
+196
};
+197
+198
#ifdef VT_LIBFUZZER
+199
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+200
/* geometry derived from the input tail so libFuzzer can steer it */
+201
int cols = 1 + (size ? data[size - 1] % 120 : 40);
+202
int rows = 1 + (size > 1 ? data[size - 2] % 50 : 24);
+203
run_input(data, size, cols, rows);
+204
return 0;
+205
}
+206
#else
+207
int main(int argc, char **argv) {
+208
long iters = (argc > 1) ? atol(argv[1]) : 3000000L;
+209
+210
/* 1) deterministic corpus replay across several geometries */
+211
const int geoms[][2] = {{1,1},{2,2},{5,3},{6,3},{10,2},{10,5},{20,4},{80,24},{132,50}};
+212
int ncorpus = (int)(sizeof(g_corpus) / sizeof(g_corpus[0]));
+213
int ngeom = (int)(sizeof(geoms) / sizeof(geoms[0]));
+214
for (int gi = 0; gi < ngeom; gi++)
+215
for (int ci = 0; ci < ncorpus; ci++)
+216
run_input((const uint8_t *)g_corpus[ci], strlen(g_corpus[ci]),
+217
geoms[gi][0], geoms[gi][1]);
+218
fprintf(stderr, "corpus: %d sequences x %d geometries replayed clean\n",
+219
ncorpus, ngeom);
+220
+221
/* 2) biased-random mutation loop. Each round: a fresh emulator + a random
+222
* byte burst weighted toward VT-vocabulary so the state machine, OSC/CSI
+223
* accumulators, scrollback and resize reflow are all reached. */
+224
static const uint8_t vocab[] = {
+225
0x1b, '[', ']', ';', ':', '?', ' ', '\\',
+226
'0','1','2','3','4','5','6','7','8','9',
+227
'H','f','A','B','C','D','J','K','m','r','h','l','n','c','q','P','L','M',
+228
'\r','\n','\t','\x08','\x07', 0x18, 0x1a,
+229
'a','Z','X', 38, 48, 5, 2,
+230
0xc3, 0xa9, 0xe2, 0x86, 0x92, 0xf0, 0x9f, 0x80, 0xff
+231
};
+232
int vocab_n = (int)sizeof(vocab);
+233
uint8_t buf[512];
+234
for (long it = 0; it < iters; it++) {
+235
int len = 1 + (int)rnd_below(sizeof(buf) - 1);
+236
for (int i = 0; i < len; i++) {
+237
uint32_t r = rnd_below(100);
+238
if (r < 70) buf[i] = vocab[rnd_below(vocab_n)]; /* biased */
+239
else buf[i] = (uint8_t)rnd(); /* pure random */
+240
}
+241
int cols = 1 + (int)rnd_below(140);
+242
int rows = 1 + (int)rnd_below(60);
+243
run_input(buf, len, cols, rows);
+244
if ((it & 0x3FFFF) == 0x3FFFF)
+245
fprintf(stderr, " mutation iters: %ld / %ld\n", it + 1, iters);
+246
}
+247
fprintf(stderr, "FUZZ CLEAN: %ld mutation iterations, sink=%d\n", iters, g_sink);
+248
return 0;
+249
}
+250
#endif
native/vt.cmodified
@@ -24,12 +24,19 @@
24
* colors: -1 default, 0..255 indexed, 0x1000000 + 0xRRGGBB truecolor.
25
*/
26
+27
/* VT_FUZZ builds the pure C emulator core WITHOUT the Sigil VM glue, so the
+28
* standalone ASan/UBSan fuzz driver (native/vt-fuzz.c) can #include this file
+29
* and exercise the parser directly. Everything that touches the Sigil runtime
+30
* is guarded out under VT_FUZZ. */
+31
#ifndef VT_FUZZ
32
#include <sigil/sigil.h>
+33
#endif
34
#include <stdint.h>
35
#include <stdlib.h>
36
#include <string.h>
37
#include <stdio.h>
38
+39
#ifndef VT_FUZZ
40
/* ---- internal libsigil helpers (exported from libsigil; not in the public
41
* header). Declared extern exactly as sigil-wasm-dom does. -------------------*/
42
extern void *sigil__gc_alloc(SigilVM *vm, SigilObjType type, size_t size);
@@ -37,6 +44,7 @@ extern void sigil__gc_push_temp_root(SigilVM *vm, Value v);
44
extern void sigil__gc_pop_temp_root(SigilVM *vm);
45
46
#define SIGIL_EXPORT __attribute__((visibility("default")))
+47
#endif
48
49
/* ---- attribute bits (slate's superset wins) ------------------------------ */
50
enum {
@@ -143,8 +151,6 @@ typedef struct {
151
VtEvent *events; int nevents, events_cap; /* out-of-band event queue */
152
} Vt;
153
146
static Value vt_type_tag = SIGIL_UNDEFINED;
147
154
/* ======================================================================== */
155
/* small utilities */
156
/* ======================================================================== */
@@ -1208,8 +1214,11 @@ static void vt_free(void *data) {
1214
}
1215
1216
/* ======================================================================== */
1211
/* Scheme glue */
+1217
/* Scheme glue (excluded from the pure-C fuzz build) */
1218
/* ======================================================================== */
+1219
#ifndef VT_FUZZ
+1220
+1221
static Value vt_type_tag = SIGIL_UNDEFINED;
1222
1223
static Vt *as_vt(Value v) {
1224
if (!sigil_is_foreign(v)) return NULL;
@@ -1652,3 +1661,5 @@ SIGIL_EXPORT void sigil__init_sigil_vt_module(SigilVM *vm) {
1661
SIGIL_EXPORT void sigil_wasm_vt_register(SigilVM *vm) {
1662
vt_register_all(vm);
1663
}
+1664
+1665
#endif /* !VT_FUZZ */
spike/fuzz.shadded
@@ -0,0 +1,17 @@
+1
#!/usr/bin/env bash
+2
# M2 GATE: build + run the sigil-vt fuzz driver under ASan + UBSan.
+3
# The pure C core (native/vt.c under -DVT_FUZZ) is standalone — no libsigil.
+4
set -euo pipefail
+5
cd "$(dirname "$0")/.."
+6
ZIG="${ZIG:-/home/daviwil/Projects/Code/sigil/sigil/tools/zig/zig}"
+7
ITERS="${1:-3000000}"
+8
mkdir -p build
+9
echo "== compiling vt-fuzz (ASan+UBSan) =="
+10
"$ZIG" cc -std=c99 -O1 -g -DVT_FUZZ \
+11
-Wall -Wextra -Wno-unused-parameter \
+12
-fsanitize=address,undefined -fno-sanitize-recover=all \
+13
native/vt-fuzz.c -o build/vt-fuzz
+14
echo "== running $ITERS iterations =="
+15
ASAN_OPTIONS=detect_leaks=1:abort_on_error=1 \
+16
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
+17
./build/vt-fuzz "$ITERS"