AtlatestRepositorysigil-vt

sigil-vt / tree / nativevt-fuzz.c

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 */
28#ifndef VT_FUZZ
29#define VT_FUZZ
30#endif
31#include "vt.c"
33/* ---- deterministic PRNG (xorshift64) ------------------------------------- */
34static uint64_t g_rng = 0x9E3779B97F4A7C15ULL;
35static 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);
41static uint32_t rnd_below(uint32_t n) { return n ? rnd() % n : 0; }
43/* ---- pure drain (mirrors the glue drains; frees event payloads) ---------- */
44static 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;
52/* ---- read every accessor path so their scans are covered ----------------- */
53static volatile int g_sink;
54static 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 is
65 * ALWAYS in bounds. That is why 5,000,000 ASan iterations never saw the
66 * heap over-read in t-d4c7: the harness had the right shape and stopped one
67 * cell short. A history row keeps its push width, so reading it at t->cols
68 * after a widening resize runs off the allocation — exactly what a full
69 * walk catches on the first widening.
70 *
71 * SCOPE, honestly: walking at sb_get_w exercises the ring's width
72 * BOOKKEEPING — a stale or too-large sb_w faults here under ASan. It does
73 * NOT catch a reader reverting to t->cols, because the Sigil-glue readers
74 * (nat_scrollback_row / nat_scrollback_runs) are compiled out of this
75 * build by VT_FUZZ. That regression is covered by the Scheme-level test in
76 * test/vt-test.sgl ("scrollback row keeps its push width"), which calls the
77 * 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 */
87/* ---- feed a buffer of raw bytes (the untrusted byte path) ---------------- */
88static 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]);
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. -------*/
94static 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);
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. */
116static 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);
144/* ---- the seed corpus: real escape sequences from the conformance suite --- */
145static 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_LIBFUZZER
218int 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;
225#else
226int 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 random
241 * byte burst weighted toward VT-vocabulary so the state machine, OSC/CSI
242 * 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, 0xff
250 };
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;
269#endif