terminal: invalidate split source page refs

PageList.split moved the source suffix into a fresh target but left
the shortened source on its old generation. Cached matches in that
suffix could therefore pass validation against the live source pointer
and reach pin tracking with invalid coordinates.

Renew the source generation only after target cloning succeeds, and
mark compression activity so restored history is reconsidered. The
conservative floor keeps bounded pruning safe even when the renewed
source and fresh target precede older successors.
This commit is contained in:
Mitchell Hashimoto
2026-07-10 07:20:29 -07:00
parent a804e3ac78
commit 91a63e5287
2 changed files with 68 additions and 0 deletions

View File

@@ -3252,6 +3252,12 @@ pub fn split(
// error handling. It is possible but we haven't written it.
errdefer comptime unreachable;
// The source is about to describe a shorter row range. Renew its
// generation only after every failable step has succeeded so a failed
// split leaves existing references valid.
self.invalidateNodeLayout(original_node);
self.page_compression.markActivity();
// Move any tracked pins from the copied rows
for (self.tracked_pins.keys()) |tracked| {
if (tracked.node.page() != page or
@@ -7053,6 +7059,35 @@ test "PageList partial erase restarts compression before continuation" {
try testing.expect(first.isCompressed());
}
test "PageList bounded pruning after split invalidation preserves live serials" {
const testing = std.testing;
var s = try init(
testing.allocator,
80,
24,
2 * PagePool.item_size,
);
defer s.deinit();
while (s.totalPages() < 2) _ = try s.grow();
const first = s.pages.first.?;
const old_serial = first.serial;
const activity = s.page_compression.activity_serial;
try s.split(.{
.node = first,
.y = first.rows() / 2,
.x = 0,
});
try testing.expect(!s.nodeIsValid(first, old_serial));
try testing.expect(activity != s.page_compression.activity_serial);
try s.fillLastPageForTest();
_ = try s.grow();
try s.expectLivePageSerialsValidForTest();
}
test "PageList repeated bounded pruning after split preserves live serials" {
const testing = std.testing;

View File

@@ -1649,6 +1649,39 @@ test "select after partial history page erase ignores shifted results" {
try testing.expect(search.selected == null);
}
test "select after history page split ignores moved results" {
const alloc = testing.allocator;
var t: Terminal = try .init(alloc, .{
.cols = 10,
.rows = 2,
.max_scrollback = std.math.maxInt(usize),
});
defer t.deinit(alloc);
const list: *PageList = &t.screens.active.pages;
var stream = t.vtStream();
defer stream.deinit();
const first = list.pages.first.?;
while (first.rows() < first.capacity().rows) stream.nextSlice("\r\n");
stream.nextSlice("error");
for (0..list.rows + 1) |_| stream.nextSlice("\r\n");
var search: ScreenSearch = try .init(alloc, t.screens.active, "error");
defer search.deinit();
try search.searchAll();
try testing.expectEqual(1, search.history_results.items.len);
try list.split(.{
.node = first,
.y = first.rows() / 2,
.x = 0,
});
try testing.expect(!try search.select(.next));
try testing.expect(search.selected == null);
}
test "screen search no scrollback has no history" {
const alloc = testing.allocator;
var t: Terminal = try .init(alloc, .{