terminal: insertBlanks should not crash with count 0 and CSI @ clamps to 1 min (#11111)

CSI @ (ICH) with an explicit parameter of 0 should be clamped to 1,
matching xterm behavior. Previously, a zero count reached
Terminal.insertBlanks which called clearCells with an empty slice,
triggering an out-of-bounds panic.

Fix the stream dispatch to clamp 0 to 1 via @max, and add a defensive
guard in insertBlanks for count == 0. Found by AFL++ stream fuzzer.
#11109
This commit is contained in:
Mitchell Hashimoto
2026-03-01 14:54:56 -08:00
committed by GitHub
2 changed files with 48 additions and 1 deletions

View File

@@ -2166,6 +2166,12 @@ pub fn insertBlanks(self: *Terminal, count: usize) void {
// xterm does.
self.screens.active.cursor.pending_wrap = false;
// If we're given a zero then we do nothing. The rest of this function
// assumes count > 0 and will crash if zero so return early. Note that
// this shouldn't be possible with real CSI sequences because the value
// is clamped to 1 min.
if (count == 0) return;
// If our cursor is outside the margins then do nothing. We DO reset
// wrap state still so this must remain below the above logic.
if (self.screens.active.cursor.x < self.scrolling_region.left or
@@ -9409,6 +9415,25 @@ test "Terminal: DECALN resets graphemes with protected mode" {
}
}
test "Terminal: insertBlanks zero" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 5, .rows = 2 });
defer t.deinit(alloc);
try t.print('A');
try t.print('B');
try t.print('C');
t.setCursorPos(1, 1);
t.insertBlanks(0);
{
const str = try t.plainString(testing.allocator);
defer testing.allocator.free(str);
try testing.expectEqualStrings("ABC", str);
}
}
test "Terminal: insertBlanks" {
// NOTE: this is not verified with conformance tests, so these
// tests might actually be verifying wrong behavior.

View File

@@ -1894,7 +1894,7 @@ pub fn Stream(comptime Handler: type) type {
'@' => switch (input.intermediates.len) {
0 => try self.handler.vt(.insert_blanks, switch (input.params.len) {
0 => 1,
1 => input.params[0],
1 => @max(1, input.params[0]),
else => {
@branchHint(.unlikely);
log.warn("invalid ICH command: {f}", .{input});
@@ -2966,6 +2966,28 @@ test "stream: insert characters" {
try testing.expect(!s.handler.called);
}
test "stream: insert characters explicit zero clamps to 1" {
const H = struct {
const Self = @This();
value: ?usize = null,
pub fn vt(
self: *Self,
comptime action: anytype,
value: anytype,
) !void {
switch (action) {
.insert_blanks => self.value = value,
else => {},
}
}
};
var s: Stream(H) = .init(.{});
for ("\x1B[0@") |c| try s.next(c);
try testing.expectEqual(@as(usize, 1), s.handler.value.?);
}
test "stream: SCOSC" {
const H = struct {
const Self = @This();