Files
ghostty/test/fuzz-libghostty/src/fuzz_stream.zig
Mitchell Hashimoto 2044e5030f terminal: make stream processing infallible
The terminal.Stream next/nextSlice functions can now no longer fail.
All prior failure modes were fully isolated in the handler `vt`
callbacks. As such, vt callbacks are now required to not return an error
and handle their own errors somehow.

Allowing streams to be fallible before was an incorrect design. It
caused problematic scenarios like in `nextSlice` early terminating
processing due to handler errors. This should not be possible.

There is no safe way to bubble up vt errors through the stream because
if nextSlice is called and multiple errors are returned, we can't
coalesce them. We could modify that to return a partial result but its
just more work for stream that is unnecessary. The handler can do all of
this.

This work was discovered due to cleanups to prepare for more C APIs.
Less errors make C APIs easier to implement! And, it helps clean up our
Zig, too.
2026-03-13 13:56:14 -07:00

52 lines
1.6 KiB
Zig

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: mem.FuzzAllocator(64 * 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 {
// Do not test zero-length input paths.
if (len == 0) return;
fuzz_alloc.reset();
const alloc = fuzz_alloc.allocator();
const input = buf[0..len];
// Allocate a terminal; if we run out of fixed-buffer space just
// skip this input (not a bug, just a very large allocation).
var t = Terminal.init(alloc, .{
.cols = 80,
.rows = 24,
.max_scrollback = 100,
}) catch return;
defer t.deinit(alloc);
var stream: ReadonlyStream = t.vtStream();
defer stream.deinit();
// Use the first byte to decide between the scalar and slice paths
// so both code paths get exercised by the fuzzer.
const mode = input[0];
const data = input[1..];
if (mode & 1 == 0) {
// Slice path — exercises SIMD fast-path if enabled
stream.nextSlice(data);
} else {
// Scalar path — exercises byte-at-a-time UTF-8 decoding
for (data) |byte| stream.next(byte);
}
}