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
This commit is contained in:
Mitchell Hashimoto
2026-03-02 06:34:40 -08:00
parent 913c12097b
commit 90e96a3891

View File

@@ -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 });