mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-18 05:20:29 +00:00
terminal: fix printCell corrupting previous row when overwriting wide char
printCell, when overwriting a wide cell with a narrow cell at x<=1 and y>0, unconditionally sets the last cell of the previous row to .narrow. This is intended to clear a spacer_head left by a wrapped wide char, but the cell could be a spacer_tail if a wide char fit entirely on the previous row. Setting a spacer_tail to .narrow orphans the preceding .wide cell, which later causes an integrity violation in insertBlanks (assert that the cell after a .wide is .spacer_tail). Fix by guarding the assignment so it only fires when the previous row's last cell is actually a .spacer_head. The same fix is applied in both the .wide and .spacer_tail branches of printCell. Found by AFL++ stream fuzzer.
This commit is contained in:
@@ -723,9 +723,14 @@ fn printCell(
|
||||
self.screens.active.cursor.page_row,
|
||||
spacer_cell[0..1],
|
||||
);
|
||||
|
||||
// If we're near the left edge, a wide char may have
|
||||
// wrapped from the previous row, leaving a spacer_head
|
||||
// at the end of that row. Clear it so the previous row
|
||||
// doesn't keep a stale spacer_head.
|
||||
if (self.screens.active.cursor.y > 0 and self.screens.active.cursor.x <= 1) {
|
||||
const head_cell = self.screens.active.cursorCellEndOfPrev();
|
||||
head_cell.wide = .narrow;
|
||||
if (head_cell.wide == .spacer_head) head_cell.wide = .narrow;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -744,9 +749,13 @@ fn printCell(
|
||||
self.screens.active.cursor.page_row,
|
||||
wide_cell[0..1],
|
||||
);
|
||||
// If we're near the left edge, a wide char may have
|
||||
// wrapped from the previous row, leaving a spacer_head
|
||||
// at the end of that row. Clear it so the previous row
|
||||
// doesn't keep a stale spacer_head.
|
||||
if (self.screens.active.cursor.y > 0 and self.screens.active.cursor.x <= 1) {
|
||||
const head_cell = self.screens.active.cursorCellEndOfPrev();
|
||||
head_cell.wide = .narrow;
|
||||
if (head_cell.wide == .spacer_head) head_cell.wide = .narrow;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3341,6 +3350,44 @@ test "Terminal: print over wide char at 0,0" {
|
||||
try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 1 } }));
|
||||
}
|
||||
|
||||
test "Terminal: print over wide char at col 0 corrupts previous row" {
|
||||
// Crash found by AFL++ fuzzer (afl-out/stream/default/crashes/id:000002).
|
||||
//
|
||||
// printCell, when overwriting a wide cell with a narrow cell at x<=1
|
||||
// and y>0, sets the last cell of the previous row to .narrow — even
|
||||
// when that cell is a .spacer_tail rather than a .spacer_head. This
|
||||
// orphans the .wide cell at cols-2.
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .cols = 10, .rows = 3 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Fill rows 0 and 1 with wide chars (5 per row on a 10-col terminal).
|
||||
for (0..10) |_| try t.print(0x4E2D);
|
||||
|
||||
// Move cursor to row 1, col 0 (on top of a wide char) and print a
|
||||
// narrow character. This triggers printCell's .wide branch which
|
||||
// corrupts row 0's last cell: col 9 changes from .spacer_tail to
|
||||
// .narrow, orphaning the .wide at col 8.
|
||||
t.setCursorPos(2, 1);
|
||||
try t.print('A');
|
||||
|
||||
// Row 1, col 0 should be narrow (we just overwrote the wide char).
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?;
|
||||
try testing.expectEqual(Cell.Wide.narrow, list_cell.cell.wide);
|
||||
}
|
||||
// Row 0, col 8 should still be .wide (the last wide char on the row).
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 8, .y = 0 } }).?;
|
||||
try testing.expectEqual(Cell.Wide.wide, list_cell.cell.wide);
|
||||
}
|
||||
// Row 0, col 9 must remain .spacer_tail to pair with the .wide at col 8.
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 9, .y = 0 } }).?;
|
||||
try testing.expectEqual(Cell.Wide.spacer_tail, list_cell.cell.wide);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: print over wide spacer tail" {
|
||||
var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
Reference in New Issue
Block a user