terminal: fix integrity violation printing wide char with hyperlink at right edge (#11119)

Printing a wide character at the right edge of the screen with an active
hyperlink triggered a page integrity violation (UnwrappedSpacerHead).
printCell wrote the spacer_head to the cell and then called
cursorSetHyperlink, whose internal integrity check observed the
spacer_head before printWrap had a chance to set the row wrap flag.

Fix by setting row.wrap = true before calling printCell for the
spacer_head case, so all integrity checks see a consistent state.
printWrap sets wrap again afterward, which is harmless. Found by AFL++
stream fuzzer.

#11109
This commit is contained in:
Mitchell Hashimoto
2026-03-01 20:08:30 -08:00
committed by GitHub

View File

@@ -629,7 +629,16 @@ pub fn print(self: *Terminal, c: u21) !void {
// We only create a spacer head if we're at the real edge
// of the screen. Otherwise, we clear the space with a narrow.
// This allows soft wrapping to work correctly.
self.printCell(0, if (right_limit == self.cols) .spacer_head else .narrow);
if (right_limit == self.cols) {
// Special-case: we need to set wrap to true even
// though we call printWrap below because if there is
// a page resize during printCell then it'll fail
// integrity checks.
self.screens.active.cursor.page_row.wrap = true;
self.printCell(0, .spacer_head);
} else {
self.printCell(0, .narrow);
}
try self.printWrap();
}
@@ -5112,6 +5121,50 @@ test "Terminal: overwrite hyperlink" {
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
}
// Printing a wide char at the right edge with an active hyperlink causes
// printCell to write a spacer_head before printWrap sets the row wrap
// flag. The integrity check inside setHyperlink (or increaseCapacity)
// sees the unwrapped spacer head and panics. Found via fuzzing.
test "Terminal: print wide char at right edge with hyperlink" {
var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 });
defer t.deinit(testing.allocator);
try t.screens.active.startHyperlink("http://example.com", null);
// Move cursor to the last column (1-indexed)
t.setCursorPos(1, 10);
// Print a wide character; this will call printCell(0, .spacer_head)
// at the right edge before calling printWrap, triggering the
// integrity violation.
try t.print(0x4E2D); // U+4E2D '中'
// Cursor wraps to row 2, after the wide char + spacer tail
try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y);
try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
// Row 0, col 9: spacer head with hyperlink
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 9, .y = 0 } }).?;
try testing.expectEqual(Cell.Wide.spacer_head, list_cell.cell.wide);
try testing.expect(list_cell.cell.hyperlink);
try testing.expect(list_cell.row.wrap);
}
// Row 1, col 0: the wide char with hyperlink
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?;
try testing.expectEqual(@as(u21, 0x4E2D), list_cell.cell.content.codepoint);
try testing.expectEqual(Cell.Wide.wide, list_cell.cell.wide);
try testing.expect(list_cell.cell.hyperlink);
}
// Row 1, col 1: spacer tail with hyperlink
{
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?;
try testing.expectEqual(Cell.Wide.spacer_tail, list_cell.cell.wide);
try testing.expect(list_cell.cell.hyperlink);
}
}
test "Terminal: linefeed and carriage return" {
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
defer t.deinit(testing.allocator);