terminal: fix insertLines/deleteLines orphaned cells on full clear

When deleteLines or insertLines count >= scroll region height, all rows
go through the clear-only path (no shifting). This path did not call
rowWillBeShifted, leaving orphaned spacer_tail cells when wide characters
straddled the right margin boundary, causing a "spacer tail not following
wide" page integrity violation.

Add rowWillBeShifted before clearCells in the else branch of both
functions.

Found via AFL++ fuzzing. #11109
This commit is contained in:
Mitchell Hashimoto
2026-03-02 10:38:53 -08:00
parent 8cddd384c6
commit b39a00ddfa

View File

@@ -1965,6 +1965,7 @@ pub fn insertLines(self: *Terminal, count: usize) void {
}
} else {
// Clear the cells for this row, it has been shifted.
self.rowWillBeShifted(&cur_p.node.data, cur_row);
const page = &cur_p.node.data;
const cells = page.getCells(cur_row);
self.screens.active.clearCells(
@@ -2152,6 +2153,7 @@ pub fn deleteLines(self: *Terminal, count: usize) void {
}
} else {
// Clear the cells for this row, it's from out of bounds.
self.rowWillBeShifted(&cur_p.node.data, cur_row);
const page = &cur_p.node.data;
const cells = page.getCells(cur_row);
self.screens.active.clearCells(
@@ -12934,3 +12936,32 @@ test "Terminal: mode 1049 alt screen plain" {
try testing.expectEqualStrings("", str);
}
}
// Reproduces a crash found by AFL++ fuzzer (afl-out/stream/default/crashes/
// id:000007,sig:06,src:004522). The crash is a page integrity violation
// "spacer tail not following wide" triggered during scrollUp -> deleteLines
// -> clearCells. When deleteLines count >= scroll region height, all rows
// are cleared (no shifting), so rowWillBeShifted is never called and wide
// characters straddling the right margin boundary leave orphaned spacer_tails.
test "Terminal: deleteLines wide char at right margin with full clear" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .cols = 80, .rows = 24 });
defer t.deinit(alloc);
// Place a wide character at col 39 (1-indexed) on several rows.
// The wide cell lands at col 38 (0-indexed) with spacer_tail at col 39.
t.setCursorPos(10, 39);
try t.print(0x4E2D); // '中'
// Set left/right scroll margins so scrolling_region.right = 38.
// clearCells will clear cells[4..39], which includes the wide cell
// at col 38 but NOT the spacer_tail at col 39.
t.modes.set(.enable_left_and_right_margin, true);
t.setLeftAndRightMargin(5, 39);
// scrollUp with count >= region height causes deleteLines to clear
// ALL rows without any shifting, so rowWillBeShifted is never called
// and the orphaned spacer_tail at col 39 triggers a page integrity
// violation in clearCells.
try t.scrollUp(t.rows);
}