From e7030e73dbafd3f986c57b1a015d16cd53e7435b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 2 Mar 2026 07:23:06 -0800 Subject: [PATCH] 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. --- src/terminal/Terminal.zig | 51 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index fe32239ba..29108a17a 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -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);