diff --git a/test/fuzz-libghostty/README.md b/test/fuzz-libghostty/README.md index a4572af37..509e2e6c5 100644 --- a/test/fuzz-libghostty/README.md +++ b/test/fuzz-libghostty/README.md @@ -7,9 +7,15 @@ libghostty-vt (Zig module). | Target | Binary | Description | | -------- | ------------- | ------------------------------------------------------- | +| `osc` | `fuzz-osc` | OSC parser with allocator (`osc.Parser.next` + `end`) | | `parser` | `fuzz-parser` | VT parser only (`Parser.next` byte-at-a-time) | | `stream` | `fuzz-stream` | Full terminal stream (`nextSlice` + `next` via handler) | +The osc target directly fuzzes the `osc.Parser` with an allocator enabled, +exercising the allocating writer code paths for large payloads. The first +byte selects the terminator variant (BEL, ST, or missing). Seeds cover OSC +52, 66, 133, 3008, 1337, and 5522. + The stream target creates a small `Terminal` and exercises the readonly `Stream` handler, covering printing, CSI dispatch, OSC, DCS, SGR, cursor movement, scrolling regions, and more. The first byte of each input selects @@ -33,13 +39,14 @@ zig build This compiles Zig static libraries for each fuzz target, emits LLVM bitcode, then links each with `afl.c` using `afl-cc` to produce instrumented binaries -at `zig-out/bin/fuzz-parser` and `zig-out/bin/fuzz-stream`. +at `zig-out/bin/fuzz-osc`, `zig-out/bin/fuzz-parser`, and `zig-out/bin/fuzz-stream`. ## Running the Fuzzer Each target has its own run step: ```sh +zig build run-osc # Run the OSC parser fuzzer zig build run-parser # Run the VT parser fuzzer zig build run-stream # Run the VT stream fuzzer ``` @@ -118,6 +125,8 @@ rename the output files to replace colons with underscores before committing: | Directory | Contents | | ------------------------ | ----------------------------------------------- | +| `corpus/osc-initial/` | Hand-written seed inputs for osc-parser | +| `corpus/osc-cmin/` | Output of `afl-cmin` (edge-deduplicated corpus) | | `corpus/parser-initial/` | Hand-written seed inputs for vt-parser | | `corpus/parser-cmin/` | Output of `afl-cmin` (edge-deduplicated corpus) | | `corpus/stream-initial/` | Hand-written seed inputs for vt-stream | diff --git a/test/fuzz-libghostty/build.zig b/test/fuzz-libghostty/build.zig index d5daca940..ec73c39c9 100644 --- a/test/fuzz-libghostty/build.zig +++ b/test/fuzz-libghostty/build.zig @@ -17,6 +17,7 @@ const Fuzzer = struct { }; const fuzzers: []const Fuzzer = &.{ + .{ .name = "osc" }, .{ .name = "parser" }, .{ .name = "stream" }, }; diff --git a/test/fuzz-libghostty/corpus/osc-cmin/01-osc52-clip-set-bel b/test/fuzz-libghostty/corpus/osc-cmin/01-osc52-clip-set-bel new file mode 100644 index 000000000..6dd0a8bff Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-cmin/01-osc52-clip-set-bel differ diff --git a/test/fuzz-libghostty/corpus/osc-cmin/02-osc52-clip-query-st b/test/fuzz-libghostty/corpus/osc-cmin/02-osc52-clip-query-st new file mode 100644 index 000000000..c030ffd19 --- /dev/null +++ b/test/fuzz-libghostty/corpus/osc-cmin/02-osc52-clip-query-st @@ -0,0 +1 @@ +52;c;? \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/osc-cmin/03-osc52-clip-clear-null b/test/fuzz-libghostty/corpus/osc-cmin/03-osc52-clip-clear-null new file mode 100644 index 000000000..a358175f0 --- /dev/null +++ b/test/fuzz-libghostty/corpus/osc-cmin/03-osc52-clip-clear-null @@ -0,0 +1 @@ +52;c; \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/osc-cmin/04-osc52-large-payload b/test/fuzz-libghostty/corpus/osc-cmin/04-osc52-large-payload new file mode 100644 index 000000000..f364da4be Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-cmin/04-osc52-large-payload differ diff --git a/test/fuzz-libghostty/corpus/osc-cmin/05-osc52-invalid-b64 b/test/fuzz-libghostty/corpus/osc-cmin/05-osc52-invalid-b64 new file mode 100644 index 000000000..843cfe69c Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-cmin/05-osc52-invalid-b64 differ diff --git a/test/fuzz-libghostty/corpus/osc-cmin/06-osc66-text-sizing b/test/fuzz-libghostty/corpus/osc-cmin/06-osc66-text-sizing new file mode 100644 index 000000000..85400b16c Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-cmin/06-osc66-text-sizing differ diff --git a/test/fuzz-libghostty/corpus/osc-cmin/07-osc66-empty b/test/fuzz-libghostty/corpus/osc-cmin/07-osc66-empty new file mode 100644 index 000000000..3a30c7329 --- /dev/null +++ b/test/fuzz-libghostty/corpus/osc-cmin/07-osc66-empty @@ -0,0 +1 @@ +66; \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/osc-cmin/08-osc133-prompt-start b/test/fuzz-libghostty/corpus/osc-cmin/08-osc133-prompt-start new file mode 100644 index 000000000..c8f5e2dac Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-cmin/08-osc133-prompt-start differ diff --git a/test/fuzz-libghostty/corpus/osc-cmin/09-osc133-cmd-start b/test/fuzz-libghostty/corpus/osc-cmin/09-osc133-cmd-start new file mode 100644 index 000000000..aca6c3ce4 --- /dev/null +++ b/test/fuzz-libghostty/corpus/osc-cmin/09-osc133-cmd-start @@ -0,0 +1 @@ +133;C \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/osc-cmin/10-osc133-cmd-end b/test/fuzz-libghostty/corpus/osc-cmin/10-osc133-cmd-end new file mode 100644 index 000000000..cc9891f89 --- /dev/null +++ b/test/fuzz-libghostty/corpus/osc-cmin/10-osc133-cmd-end @@ -0,0 +1 @@ +133;D;0 \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/osc-cmin/11-osc133-invalid b/test/fuzz-libghostty/corpus/osc-cmin/11-osc133-invalid new file mode 100644 index 000000000..059a65a26 Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-cmin/11-osc133-invalid differ diff --git a/test/fuzz-libghostty/corpus/osc-cmin/12-osc3008-context b/test/fuzz-libghostty/corpus/osc-cmin/12-osc3008-context new file mode 100644 index 000000000..1508b4625 Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-cmin/12-osc3008-context differ diff --git a/test/fuzz-libghostty/corpus/osc-cmin/13-osc3008-empty b/test/fuzz-libghostty/corpus/osc-cmin/13-osc3008-empty new file mode 100644 index 000000000..7bf818196 --- /dev/null +++ b/test/fuzz-libghostty/corpus/osc-cmin/13-osc3008-empty @@ -0,0 +1 @@ +3008; \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/osc-cmin/14-osc1337-file-inline b/test/fuzz-libghostty/corpus/osc-cmin/14-osc1337-file-inline new file mode 100644 index 000000000..9e2da3740 Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-cmin/14-osc1337-file-inline differ diff --git a/test/fuzz-libghostty/corpus/osc-cmin/15-osc1337-invalid b/test/fuzz-libghostty/corpus/osc-cmin/15-osc1337-invalid new file mode 100644 index 000000000..5bbb9a412 --- /dev/null +++ b/test/fuzz-libghostty/corpus/osc-cmin/15-osc1337-invalid @@ -0,0 +1 @@ +1337;badcmd \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/osc-cmin/16-osc5522-clip b/test/fuzz-libghostty/corpus/osc-cmin/16-osc5522-clip new file mode 100644 index 000000000..77fd6c24d Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-cmin/16-osc5522-clip differ diff --git a/test/fuzz-libghostty/corpus/osc-cmin/17-osc5522-empty b/test/fuzz-libghostty/corpus/osc-cmin/17-osc5522-empty new file mode 100644 index 000000000..2132395fb --- /dev/null +++ b/test/fuzz-libghostty/corpus/osc-cmin/17-osc5522-empty @@ -0,0 +1 @@ +5522; \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/osc-cmin/18-osc52-truncated b/test/fuzz-libghostty/corpus/osc-cmin/18-osc52-truncated new file mode 100644 index 000000000..069210bfe Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-cmin/18-osc52-truncated differ diff --git a/test/fuzz-libghostty/corpus/osc-cmin/19-single-byte b/test/fuzz-libghostty/corpus/osc-cmin/19-single-byte new file mode 100644 index 000000000..f76dd238a Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-cmin/19-single-byte differ diff --git a/test/fuzz-libghostty/corpus/osc-cmin/20-invalid-osc-num b/test/fuzz-libghostty/corpus/osc-cmin/20-invalid-osc-num new file mode 100644 index 000000000..54994a61d Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-cmin/20-invalid-osc-num differ diff --git a/test/fuzz-libghostty/corpus/osc-initial/01-osc52-clip-set-bel b/test/fuzz-libghostty/corpus/osc-initial/01-osc52-clip-set-bel new file mode 100644 index 000000000..6dd0a8bff Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-initial/01-osc52-clip-set-bel differ diff --git a/test/fuzz-libghostty/corpus/osc-initial/02-osc52-clip-query-st b/test/fuzz-libghostty/corpus/osc-initial/02-osc52-clip-query-st new file mode 100644 index 000000000..c030ffd19 --- /dev/null +++ b/test/fuzz-libghostty/corpus/osc-initial/02-osc52-clip-query-st @@ -0,0 +1 @@ +52;c;? \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/osc-initial/03-osc52-clip-clear-null b/test/fuzz-libghostty/corpus/osc-initial/03-osc52-clip-clear-null new file mode 100644 index 000000000..a358175f0 --- /dev/null +++ b/test/fuzz-libghostty/corpus/osc-initial/03-osc52-clip-clear-null @@ -0,0 +1 @@ +52;c; \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/osc-initial/04-osc52-large-payload b/test/fuzz-libghostty/corpus/osc-initial/04-osc52-large-payload new file mode 100644 index 000000000..f364da4be Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-initial/04-osc52-large-payload differ diff --git a/test/fuzz-libghostty/corpus/osc-initial/05-osc52-invalid-b64 b/test/fuzz-libghostty/corpus/osc-initial/05-osc52-invalid-b64 new file mode 100644 index 000000000..843cfe69c Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-initial/05-osc52-invalid-b64 differ diff --git a/test/fuzz-libghostty/corpus/osc-initial/06-osc66-text-sizing b/test/fuzz-libghostty/corpus/osc-initial/06-osc66-text-sizing new file mode 100644 index 000000000..85400b16c Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-initial/06-osc66-text-sizing differ diff --git a/test/fuzz-libghostty/corpus/osc-initial/07-osc66-empty b/test/fuzz-libghostty/corpus/osc-initial/07-osc66-empty new file mode 100644 index 000000000..3a30c7329 --- /dev/null +++ b/test/fuzz-libghostty/corpus/osc-initial/07-osc66-empty @@ -0,0 +1 @@ +66; \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/osc-initial/08-osc133-prompt-start b/test/fuzz-libghostty/corpus/osc-initial/08-osc133-prompt-start new file mode 100644 index 000000000..c8f5e2dac Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-initial/08-osc133-prompt-start differ diff --git a/test/fuzz-libghostty/corpus/osc-initial/09-osc133-cmd-start b/test/fuzz-libghostty/corpus/osc-initial/09-osc133-cmd-start new file mode 100644 index 000000000..aca6c3ce4 --- /dev/null +++ b/test/fuzz-libghostty/corpus/osc-initial/09-osc133-cmd-start @@ -0,0 +1 @@ +133;C \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/osc-initial/10-osc133-cmd-end b/test/fuzz-libghostty/corpus/osc-initial/10-osc133-cmd-end new file mode 100644 index 000000000..cc9891f89 --- /dev/null +++ b/test/fuzz-libghostty/corpus/osc-initial/10-osc133-cmd-end @@ -0,0 +1 @@ +133;D;0 \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/osc-initial/11-osc133-invalid b/test/fuzz-libghostty/corpus/osc-initial/11-osc133-invalid new file mode 100644 index 000000000..059a65a26 Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-initial/11-osc133-invalid differ diff --git a/test/fuzz-libghostty/corpus/osc-initial/12-osc3008-context b/test/fuzz-libghostty/corpus/osc-initial/12-osc3008-context new file mode 100644 index 000000000..1508b4625 Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-initial/12-osc3008-context differ diff --git a/test/fuzz-libghostty/corpus/osc-initial/13-osc3008-empty b/test/fuzz-libghostty/corpus/osc-initial/13-osc3008-empty new file mode 100644 index 000000000..7bf818196 --- /dev/null +++ b/test/fuzz-libghostty/corpus/osc-initial/13-osc3008-empty @@ -0,0 +1 @@ +3008; \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/osc-initial/14-osc1337-file-inline b/test/fuzz-libghostty/corpus/osc-initial/14-osc1337-file-inline new file mode 100644 index 000000000..9e2da3740 Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-initial/14-osc1337-file-inline differ diff --git a/test/fuzz-libghostty/corpus/osc-initial/15-osc1337-invalid b/test/fuzz-libghostty/corpus/osc-initial/15-osc1337-invalid new file mode 100644 index 000000000..5bbb9a412 --- /dev/null +++ b/test/fuzz-libghostty/corpus/osc-initial/15-osc1337-invalid @@ -0,0 +1 @@ +1337;badcmd \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/osc-initial/16-osc5522-clip b/test/fuzz-libghostty/corpus/osc-initial/16-osc5522-clip new file mode 100644 index 000000000..77fd6c24d Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-initial/16-osc5522-clip differ diff --git a/test/fuzz-libghostty/corpus/osc-initial/17-osc5522-empty b/test/fuzz-libghostty/corpus/osc-initial/17-osc5522-empty new file mode 100644 index 000000000..2132395fb --- /dev/null +++ b/test/fuzz-libghostty/corpus/osc-initial/17-osc5522-empty @@ -0,0 +1 @@ +5522; \ No newline at end of file diff --git a/test/fuzz-libghostty/corpus/osc-initial/18-osc52-truncated b/test/fuzz-libghostty/corpus/osc-initial/18-osc52-truncated new file mode 100644 index 000000000..069210bfe Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-initial/18-osc52-truncated differ diff --git a/test/fuzz-libghostty/corpus/osc-initial/19-single-byte b/test/fuzz-libghostty/corpus/osc-initial/19-single-byte new file mode 100644 index 000000000..f76dd238a Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-initial/19-single-byte differ diff --git a/test/fuzz-libghostty/corpus/osc-initial/20-invalid-osc-num b/test/fuzz-libghostty/corpus/osc-initial/20-invalid-osc-num new file mode 100644 index 000000000..54994a61d Binary files /dev/null and b/test/fuzz-libghostty/corpus/osc-initial/20-invalid-osc-num differ diff --git a/test/fuzz-libghostty/src/fuzz_osc.zig b/test/fuzz-libghostty/src/fuzz_osc.zig new file mode 100644 index 000000000..51a04ec82 --- /dev/null +++ b/test/fuzz-libghostty/src/fuzz_osc.zig @@ -0,0 +1,45 @@ +const std = @import("std"); +const ghostty_vt = @import("ghostty-vt"); +const mem = @import("mem.zig"); +const osc = ghostty_vt.osc; + +/// Use a single global allocator for simplicity and to avoid heap +/// allocation overhead in the fuzzer. The allocator is backed by a fixed +/// buffer, and every fuzz input resets the bump pointer to the start. +var fuzz_alloc: mem.FuzzAllocator(8 * 1024 * 1024) = .{}; + +pub export fn zig_fuzz_init() callconv(.c) void { + fuzz_alloc.init(); +} + +pub export fn zig_fuzz_test( + buf: [*]const u8, + len: usize, +) callconv(.c) void { + // Need at least one byte for the terminator selector. + if (len == 0) return; + + fuzz_alloc.reset(); + const alloc = fuzz_alloc.allocator(); + const input = buf[0..len]; + + // Use the first byte to select the terminator variant. + const selector = input[0]; + const payload = input[1..]; + + var p = osc.Parser.init(alloc); + defer p.deinit(); + for (payload) |byte| p.next(byte); + + // Exercise all three terminator paths: + // 0 -> BEL (0x07) + // 1 -> ST (0x9c) + // 2 -> missing terminator (null) + const terminator: ?u8 = switch (selector % 3) { + 0 => 0x07, + 1 => 0x9c, + else => null, + }; + + _ = p.end(terminator); +} diff --git a/test/fuzz-libghostty/src/fuzz_stream.zig b/test/fuzz-libghostty/src/fuzz_stream.zig index 0c9ab67fb..17f63766f 100644 --- a/test/fuzz-libghostty/src/fuzz_stream.zig +++ b/test/fuzz-libghostty/src/fuzz_stream.zig @@ -1,12 +1,13 @@ const std = @import("std"); const ghostty_vt = @import("ghostty-vt"); +const mem = @import("mem.zig"); const Terminal = ghostty_vt.Terminal; const ReadonlyStream = ghostty_vt.ReadonlyStream; /// Use a single global allocator for simplicity and to avoid heap /// allocation overhead in the fuzzer. The allocator is backed by a fixed /// buffer, and every fuzz input resets the bump pointer to the start. -var fuzz_alloc: FuzzAllocator = .{}; +var fuzz_alloc: mem.FuzzAllocator(64 * 1024 * 1024) = .{}; pub export fn zig_fuzz_init() callconv(.c) void { fuzz_alloc.init(); @@ -50,29 +51,3 @@ pub export fn zig_fuzz_test( std.debug.panic("next: {}", .{err}); } } - -/// Fixed-capacity allocator that avoids heap allocation and gives the -/// fuzzer deterministic, bounded memory behaviour. Backed by a single -/// fixed buffer; every `reset()` returns the bump pointer to the start -/// so the same memory is reused across iterations. -const FuzzAllocator = struct { - buf: [mem_size]u8 = undefined, - state: std.heap.FixedBufferAllocator = undefined, - - /// 64 MiB gives the fuzzer enough headroom to exercise terminal - /// resizes, large scrollback, and other allocation-heavy paths - /// without running into out-of-memory on every other input. - const mem_size = 64 * 1024 * 1024; - - fn init(self: *FuzzAllocator) void { - self.state = .init(&self.buf); - } - - fn allocator(self: *FuzzAllocator) std.mem.Allocator { - return self.state.allocator(); - } - - fn reset(self: *FuzzAllocator) void { - self.state.reset(); - } -}; diff --git a/test/fuzz-libghostty/src/mem.zig b/test/fuzz-libghostty/src/mem.zig new file mode 100644 index 000000000..b5b81598e --- /dev/null +++ b/test/fuzz-libghostty/src/mem.zig @@ -0,0 +1,26 @@ +const std = @import("std"); + +/// Fixed-capacity allocator that avoids heap allocation and gives the +/// fuzzer deterministic, bounded memory behaviour. Backed by a single +/// fixed buffer; every `reset()` returns the bump pointer to the start +/// so the same memory is reused across iterations. +pub fn FuzzAllocator(comptime mem_size: usize) type { + return struct { + buf: [mem_size]u8 = undefined, + state: std.heap.FixedBufferAllocator = undefined, + + const Self = @This(); + + pub fn init(self: *Self) void { + self.state = .init(&self.buf); + } + + pub fn allocator(self: *Self) std.mem.Allocator { + return self.state.allocator(); + } + + pub fn reset(self: *Self) void { + self.state.reset(); + } + }; +}