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
"\033(0lqqk\033(Bqq",147
"\033)0q\016qx\017q",148
"\033)0\016\0337\033)B\017\0338q\017q",149
"\033(0\033[?1049h\033(B\033[?1049lq\033(Bq",150
"\033(\030q\033(\033(0q\033(\032q",151
"hello",152
"ab\r\ncd",153
"abc\rX",154
"ab\x08X",155
"0123456789AB",156
"\033[?7l0123456789AB",157
"aa\r\nbb\r\ncc",158
"a\tb",159
"\033[3;4HX",160
"\033[3;4H\033[A\033[2DX",161
"abc\033[10;20HZ",162
"\033[2;2H\033[3B\033[2CX",163
"hi\033[5GX",164
"hi\033[3dX",165
"abcdef\033[4G\033[K",166
"abcdef\033[4G\033[1K",167
"abcdef\033[2K",168
"aaaaaa\r\nbbbbbb\r\ncccccc\033[2;3H\033[J",169
"aaaaaa\r\nbbbbbb\r\ncccccc\033[2;3H\033[1J",170
"abcdef\033[3G\033[2@XY",171
"abcdef\033[3G\033[2P",172
"abcdef\033[3G\033[2X",173
"a\r\nb\r\nc\r\nd\033[2;1H\033[L",174
"a\r\nb\r\nc\r\nd\033[2;1H\033[M",175
"top\033[2;4r\033[2;1Hl1\r\nl2\r\nl3\r\nl4",176
"a\r\nb\r\nc\r\nd\r\ne\033[2;4r\033[2S",177
"a\r\nb\r\nc\r\nd\r\ne\033[2;4r\033[1T",178
"a\r\nb\r\nc\033[2;4r\033[2;1H\033M",179
"\033[2;4r\033[?6h\033[1;1HX",180
"\033[1;31mR\033[0mp",181
"\033[3;4;7mx",182
"\033[38;5;196m\033[48;5;22mc",183
"\033[38;2;255;128;0mt",184
"\033[38:5:99mQ",185
"\033[38:2::255:128:0;4mW",186
"\033[38:2:10:20:30mV",187
"\033[91mb\033[39md",188
"\033[1;31ma\033[22mb",189
"\033[48;5;19m\033[2J",190
"main\033[?1049h\033[1;1HALT\033[?1049l",191
"\033[?1049ha\r\nb\r\nc\r\nd",192
"\033[31m\033[2;3H\0337\033[0m\033[1;1H\0338X",193
"\033#8",194
"hi\033[31m\033cx",195
"\033]0;my title\x07x",196
"\033]2;st title\033\\y",197
"\033]52;c;aGVsbG8=\x07z",198
"\033[4 q",199
"\033[?2004h",200
"\033[?25l",201
"\033[?1h",202
"abc\033[2G\033[4hXY",203
"\033[?1002h\033[?1006h",204
"\033[3;4H\033[6n",205
"\033[5n",206
"\033[c",207
"\033[999999999999H" "ok",208
"\033[38;2mx",209
"\033P malicious dcs payload \033\\ok",210
"\033[999999999Ix",211
"\033[?9999h\033[<5m\033(0ok",212
/* hostile / adversarial shapes */213
"\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",214
"\033]0;" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",215
"\033[38:2:1:2:3:4:5:6:7:8:9m",216
"\033[;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;m",217
"\xc3\xa9\xe2\x86\x92\xf0\x9f\x98\x80", /* multibyte UTF-8 */218
"\xed\xa0\x80\xf4\x90\x80\x80\xe0\x80\xa8", /* surrogate/overlong/range */219
"\xff\xfe\xfd\x80\x81\xc0\xc1", /* invalid lead bytes */220
};222
#ifdef VT_LIBFUZZER223
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {224
/* geometry derived from the input tail so libFuzzer can steer it */225
int cols = 1 + (size ? data[size - 1] % 120 : 40);226
int rows = 1 + (size > 1 ? data[size - 2] % 50 : 24);227
run_input(data, size, cols, rows);228
return 0;229
}230
#else231
int main(int argc, char **argv) {232
long iters = (argc > 1) ? atol(argv[1]) : 3000000L;234
/* 1) deterministic corpus replay across several geometries */235
const int geoms[][2] = {{1,1},{2,2},{5,3},{6,3},{10,2},{10,5},{20,4},{80,24},{132,50}};236
int ncorpus = (int)(sizeof(g_corpus) / sizeof(g_corpus[0]));237
int ngeom = (int)(sizeof(geoms) / sizeof(geoms[0]));238
for (int gi = 0; gi < ngeom; gi++)239
for (int ci = 0; ci < ncorpus; ci++)240
run_input((const uint8_t *)g_corpus[ci], strlen(g_corpus[ci]),241
geoms[gi][0], geoms[gi][1]);242
fprintf(stderr, "corpus: %d sequences x %d geometries replayed clean\n",243
ncorpus, ngeom);245
/* 2) biased-random mutation loop. Each round: a fresh emulator + a random246
* byte burst weighted toward VT-vocabulary so the state machine, OSC/CSI247
* accumulators, scrollback and resize reflow are all reached. */248
static const uint8_t vocab[] = {249
0x1b, '[', ']', ';', ':', '?', ' ', '\\', '(', ')', 0x0e, 0x0f,250
'0','1','2','3','4','5','6','7','8','9',251
'H','f','A','B','C','D','J','K','m','r','h','l','n','c','q','P','L','M',252
'\r','\n','\t','\x08','\x07', 0x18, 0x1a,253
'a','Z','X', 38, 48, 5, 2,254
0xc3, 0xa9, 0xe2, 0x86, 0x92, 0xf0, 0x9f, 0x80, 0xff255
};256
int vocab_n = (int)sizeof(vocab);257
uint8_t buf[512];258
for (long it = 0; it < iters; it++) {259
int len = 1 + (int)rnd_below(sizeof(buf) - 1);260
for (int i = 0; i < len; i++) {261
uint32_t r = rnd_below(100);262
if (r < 70) buf[i] = vocab[rnd_below(vocab_n)]; /* biased */263
else buf[i] = (uint8_t)rnd(); /* pure random */264
}265
int cols = 1 + (int)rnd_below(140);266
int rows = 1 + (int)rnd_below(60);267
run_input(buf, len, cols, rows);268
if ((it & 0x3FFFF) == 0x3FFFF)269
fprintf(stderr, " mutation iters: %ld / %ld\n", it + 1, iters);270
}271
fprintf(stderr, "FUZZ CLEAN: %ld mutation iterations, sink=%d\n", iters, g_sink);272
return 0;273
}274
#endif