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
#endifnative/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"