AtlatestRepositorysigil-vt
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 the5
* trust boundary into C: a parser bug becomes a memory-safety bug fed by6
* arbitrary program output. This driver exercises the pure emulator core7
* (native/vt.c, included with VT_FUZZ so the Sigil VM glue is excluded) under8
* ASan + UBSan:9
*10
* 1. a deterministic seed corpus of real escape sequences (from the11
* conformance fixtures), replayed into fresh emulators of several12
* geometries;13
* 2. a large biased-random mutation loop over vt feed_byte (the untrusted14
* byte path), interleaving resize / reset / alt-screen / drain, so the15
* 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-fuzz20
* ./build/vt-fuzz [iterations] # default 3,000,00021
*22
* Optional coverage-guided libFuzzer entry under -DVT_LIBFUZZER.23
*24
* Deterministic by construction (fixed-seed xorshift; no time/rand) so a25
* failure reproduces exactly.26
*/28
#ifndef VT_FUZZ29
#define VT_FUZZ30
#endif31
#include "vt.c"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; }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
}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
/* WALK A SCROLLBACK ROW IN FULL, at the width the real readers use.64
* This line used to be `if (sb) g_sink ^= sb[0];` — one cell, which is65
* ALWAYS in bounds. That is why 5,000,000 ASan iterations never saw the66
* heap over-read in t-d4c7: the harness had the right shape and stopped one67
* cell short. A history row keeps its push width, so reading it at t->cols68
* after a widening resize runs off the allocation — exactly what a full69
* walk catches on the first widening.70
*71
* SCOPE, honestly: walking at sb_get_w exercises the ring's width72
* BOOKKEEPING — a stale or too-large sb_w faults here under ASan. It does73
* NOT catch a reader reverting to t->cols, because the Sigil-glue readers74
* (nat_scrollback_row / nat_scrollback_runs) are compiled out of this75
* build by VT_FUZZ. That regression is covered by the Scheme-level test in76
* test/vt-test.sgl ("scrollback row keeps its push width"), which calls the77
* real readers. Two gates, different halves. */78
int sbk = (int)rnd_below(t->sb_size ? t->sb_size + 1 : 1);79
int32_t *sb = sb_get(t, sbk);80
if (sb) {81
int w = sb_get_w(t, sbk);82
for (int c = 0; c < w; c++) g_sink ^= sb[c * 4];83
}84
g_sink ^= color_256_rgb((int)rnd_below(300)); /* palette incl. out-of-range */85
}87
/* ---- feed a buffer of raw bytes (the untrusted byte path) ---------------- */88
static void feed_buf(Vt *t, const uint8_t *data, size_t len) {89
for (size_t i = 0; i < len; i++) feed_byte(t, data[i]);90
}92
/* ---- feed via the STRING path: decode to codepoints and run the ground-93
* state bulk-print scan (mirrors nat_feed). Also exercises print_run. -------*/94
static void feed_string_path(Vt *t, const uint8_t *data, size_t len) {95
int32_t cps[256];96
int nc = 0;97
size_t i = 0;98
while (i < len) {99
unsigned char b = data[i];100
int32_t cp; int adv;101
if (b < 0x80) { cp = b; adv = 1; }102
else if (b < 0xE0 && i + 1 < len) { cp = ((b & 0x1F) << 6) | (data[i+1] & 0x3F); adv = 2; }103
else if (b < 0xF0 && i + 2 < len) { cp = ((b & 0x0F) << 12) | ((data[i+1] & 0x3F) << 6) | (data[i+2] & 0x3F); adv = 3; }104
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; }105
else { cp = 0xFFFD; adv = 1; }106
if (cp > 0x10FFFF || (cp >= 0xD800 && cp < 0xE000)) cp = 0xFFFD;107
cps[nc++] = cp;108
i += adv;109
if (nc == 256) { feed_run_scan(t, cps, nc); nc = 0; }110
}111
if (nc) feed_run_scan(t, cps, nc);112
}114
/* Drive one emulator through an input buffer with interleaved geometry churn.115
* Used both for corpus replay and (with random data) the mutation loop. */116
static void run_input(const uint8_t *data, size_t len, int cols, int rows) {117
if (cols < 1) cols = 1; if (cols > 400) cols = 400;118
if (rows < 1) rows = 1; if (rows > 200) rows = 200;119
Vt *t = vt_new(cols, rows, 64);120
if (!t) return;121
size_t i = 0;122
while (i < len) {123
size_t chunk = 1 + rnd_below(64);124
if (chunk > len - i) chunk = len - i;125
/* alternate the byte path and the string bulk-print path */126
if (rnd_below(4) == 0) feed_string_path(t, data + i, chunk);127
else feed_buf(t, data + i, chunk);128
i += chunk;129
switch (rnd_below(24)) {130
case 0: term_resize(t, 1 + rnd_below(120), 1 + rnd_below(50)); break;131
case 1: term_resize(t, 1 + rnd_below(400), 1 + rnd_below(200)); break;132
case 2: term_reset(t); break;133
case 3: mark_all(t); break;134
case 4: fuzz_drain(t); break;135
case 5: touch_accessors(t); break;136
default: break;137
}138
}139
touch_accessors(t);140
fuzz_drain(t);141
vt_free(t);142
}144
/* ---- the seed corpus: real escape sequences from the conformance suite --- */145
static const char *g_corpus[] = {146
"hello",147
"ab\r\ncd",148
"abc\rX",149
"ab\x08X",150
"0123456789AB",151
"\033[?7l0123456789AB",152
"aa\r\nbb\r\ncc",153
"a\tb",154
"\033[3;4HX",155
"\033[3;4H\033[A\033[2DX",156
"abc\033[10;20HZ",157
"\033[2;2H\033[3B\033[2CX",158
"hi\033[5GX",159
"hi\033[3dX",160
"abcdef\033[4G\033[K",161
"abcdef\033[4G\033[1K",162
"abcdef\033[2K",163
"aaaaaa\r\nbbbbbb\r\ncccccc\033[2;3H\033[J",164
"aaaaaa\r\nbbbbbb\r\ncccccc\033[2;3H\033[1J",165
"abcdef\033[3G\033[2@XY",166
"abcdef\033[3G\033[2P",167
"abcdef\033[3G\033[2X",168
"a\r\nb\r\nc\r\nd\033[2;1H\033[L",169
"a\r\nb\r\nc\r\nd\033[2;1H\033[M",170
"top\033[2;4r\033[2;1Hl1\r\nl2\r\nl3\r\nl4",171
"a\r\nb\r\nc\r\nd\r\ne\033[2;4r\033[2S",172
"a\r\nb\r\nc\r\nd\r\ne\033[2;4r\033[1T",173
"a\r\nb\r\nc\033[2;4r\033[2;1H\033M",174
"\033[2;4r\033[?6h\033[1;1HX",175
"\033[1;31mR\033[0mp",176
"\033[3;4;7mx",177
"\033[38;5;196m\033[48;5;22mc",178
"\033[38;2;255;128;0mt",179
"\033[38:5:99mQ",180
"\033[38:2::255:128:0;4mW",181
"\033[38:2:10:20:30mV",182
"\033[91mb\033[39md",183
"\033[1;31ma\033[22mb",184
"\033[48;5;19m\033[2J",185
"main\033[?1049h\033[1;1HALT\033[?1049l",186
"\033[?1049ha\r\nb\r\nc\r\nd",187
"\033[31m\033[2;3H\0337\033[0m\033[1;1H\0338X",188
"\033#8",189
"hi\033[31m\033cx",190
"\033]0;my title\x07x",191
"\033]2;st title\033\\y",192
"\033]52;c;aGVsbG8=\x07z",193
"\033[4 q",194
"\033[?2004h",195
"\033[?25l",196
"\033[?1h",197
"abc\033[2G\033[4hXY",198
"\033[?1002h\033[?1006h",199
"\033[3;4H\033[6n",200
"\033[5n",201
"\033[c",202
"\033[999999999999H" "ok",203
"\033[38;2mx",204
"\033P malicious dcs payload \033\\ok",205
"\033[999999999Ix",206
"\033[?9999h\033[<5m\033(0ok",207
/* hostile / adversarial shapes */208
"\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",209
"\033]0;" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",210
"\033[38:2:1:2:3:4:5:6:7:8:9m",211
"\033[;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;m",212
"\xc3\xa9\xe2\x86\x92\xf0\x9f\x98\x80", /* multibyte UTF-8 */213
"\xed\xa0\x80\xf4\x90\x80\x80\xe0\x80\xa8", /* surrogate/overlong/range */214
"\xff\xfe\xfd\x80\x81\xc0\xc1", /* invalid lead bytes */215
};217
#ifdef VT_LIBFUZZER218
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {219
/* geometry derived from the input tail so libFuzzer can steer it */220
int cols = 1 + (size ? data[size - 1] % 120 : 40);221
int rows = 1 + (size > 1 ? data[size - 2] % 50 : 24);222
run_input(data, size, cols, rows);223
return 0;224
}225
#else226
int main(int argc, char **argv) {227
long iters = (argc > 1) ? atol(argv[1]) : 3000000L;229
/* 1) deterministic corpus replay across several geometries */230
const int geoms[][2] = {{1,1},{2,2},{5,3},{6,3},{10,2},{10,5},{20,4},{80,24},{132,50}};231
int ncorpus = (int)(sizeof(g_corpus) / sizeof(g_corpus[0]));232
int ngeom = (int)(sizeof(geoms) / sizeof(geoms[0]));233
for (int gi = 0; gi < ngeom; gi++)234
for (int ci = 0; ci < ncorpus; ci++)235
run_input((const uint8_t *)g_corpus[ci], strlen(g_corpus[ci]),236
geoms[gi][0], geoms[gi][1]);237
fprintf(stderr, "corpus: %d sequences x %d geometries replayed clean\n",238
ncorpus, ngeom);240
/* 2) biased-random mutation loop. Each round: a fresh emulator + a random241
* byte burst weighted toward VT-vocabulary so the state machine, OSC/CSI242
* accumulators, scrollback and resize reflow are all reached. */243
static const uint8_t vocab[] = {244
0x1b, '[', ']', ';', ':', '?', ' ', '\\',245
'0','1','2','3','4','5','6','7','8','9',246
'H','f','A','B','C','D','J','K','m','r','h','l','n','c','q','P','L','M',247
'\r','\n','\t','\x08','\x07', 0x18, 0x1a,248
'a','Z','X', 38, 48, 5, 2,249
0xc3, 0xa9, 0xe2, 0x86, 0x92, 0xf0, 0x9f, 0x80, 0xff250
};251
int vocab_n = (int)sizeof(vocab);252
uint8_t buf[512];253
for (long it = 0; it < iters; it++) {254
int len = 1 + (int)rnd_below(sizeof(buf) - 1);255
for (int i = 0; i < len; i++) {256
uint32_t r = rnd_below(100);257
if (r < 70) buf[i] = vocab[rnd_below(vocab_n)]; /* biased */258
else buf[i] = (uint8_t)rnd(); /* pure random */259
}260
int cols = 1 + (int)rnd_below(140);261
int rows = 1 + (int)rnd_below(60);262
run_input(buf, len, cols, rows);263
if ((it & 0x3FFFF) == 0x3FFFF)264
fprintf(stderr, " mutation iters: %ld / %ld\n", it + 1, iters);265
}266
fprintf(stderr, "FUZZ CLEAN: %ld mutation iterations, sink=%d\n", iters, g_sink);267
return 0;268
}269
#endif