From 90e96a3891b7718ff98819ee51ecbd7c133d422f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 2 Mar 2026 06:34:40 -0800 Subject: [PATCH] terminal: fix insertBlanks integrity violation with wide char at right margin insertBlanks checks whether the last source cell being shifted is wide and clears it to avoid splitting, but it did not check the destination cells at the right edge of the scroll region. When a wide character straddles the right scroll margin (head at the margin, spacer_tail just beyond it), the swap loop displaced the wide head without clearing the orphaned spacer_tail, causing a page integrity violation (InvalidSpacerTailLocation). Fix by checking the cell at the right margin (last destination cell) before the swap loop and clearing it along with its spacer_tail when it is wide. Found by AFL++ stream fuzzer. #11109 --- src/terminal/Terminal.zig | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index d5709ea53..fe32239ba 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -2235,6 +2235,18 @@ pub fn insertBlanks(self: *Terminal, count: usize) void { ); } + // If the cell at the right margin is wide, its spacer tail + // is outside the scroll region and won't be shifted. Clear + // both to avoid orphaning the spacer tail. + const dst_end: [*]Cell = left + (rem - 1); + if (dst_end[0].wide == .wide) { + self.screens.active.clearCells( + page, + self.screens.active.cursor.page_row, + dst_end[0..2], + ); + } + // We work backwards so we don't overwrite data. while (@intFromPtr(x) >= @intFromPtr(left)) : (x -= 1) { const src: *Cell = @ptrCast(x); @@ -9878,6 +9890,40 @@ test "Terminal: insertBlanks pushes hyperlink off end completely" { } } +test "Terminal: insertBlanks wide char straddling right margin" { + // Crash found by AFL++ fuzzer. + // + // When a wide character straddles the right scroll margin (head at the + // margin, spacer_tail just beyond it), insertBlanks shifts the wide head + // away via swapCells but leaves the orphaned spacer_tail in place, + // causing a page integrity violation. + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 10, .rows = 5 }); + defer t.deinit(alloc); + + // Fill row: A B C D 橋 _ _ _ _ _ + // Positions: 0 1 2 3 4W 5T 6 7 8 9 + t.setCursorPos(1, 1); + for ("ABCD") |c| try t.print(c); + try t.print('橋'); // wide char: head at 4, spacer_tail at 5 + + // Set right margin so the wide head is AT the boundary and the + // spacer_tail is just outside it. + t.scrolling_region.right = 4; + + // Position cursor at x=2 (1-indexed col 3) and insert one blank. + // This triggers the swap loop which displaces the wide head at + // position 4 without clearing the spacer_tail at position 5. + t.setCursorPos(1, 3); + t.insertBlanks(1); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("AB CD", str); + } +} + test "Terminal: insert mode with space" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 2 });