From 94343583eb6aefa8f421083409256c877aad036e Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 10:13:10 -0300 Subject: A terminal's rows asked for after its cached rows were dropped, before the step's sweep, are scratch: the cached ones are freed, not overwritten dropPane clears the rows cache's pane but keeps its allocations until the next step's sweep, since they may still be borrowed. shellRows took a cache with no pane as free and wrote a new gpa-built entry over it, leaking the old one: the 9P monkey's seed 109 (a look searching a terminal after its pane was torn down in the same step) ended its session with the debug allocator's leak panic. A slot still holding rows now gets scratch rows, as one holding another pane's already did. A unit test drops and asks again under the testing allocator (it leaks without this). Co-Authored-By: Claude Opus 5.5 --- src/terminal.zig | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) (limited to 'src/terminal.zig') diff --git a/src/terminal.zig b/src/terminal.zig index 6bf46bef..1a3b6a21 100644 --- a/src/terminal.zig +++ b/src/terminal.zig @@ -1343,7 +1343,10 @@ pub fn shellRows(p: *Pardes, pane: *Pane) ![]const []const u8 { if (pane.terminal == null) return &empty_grid; const c = &p.shell_rows; if (!c.stale and c.pane == pane) return c.rows; - if (c.pane != null) { + // Another pane's rows, or a dropped pane's still waiting for the step's + // sweep (dropPane keeps them, as they may still be borrowed): this + // step's rows are scratch, and nothing in the slot is written over. + if (c.pane != null or c.text_alloc.len > 0 or c.rows.len > 0) { c.stale = true; return (try buildRows(p.scratch.allocator(), p, pane)).rows; } @@ -1357,6 +1360,18 @@ pub fn shellRows(p: *Pardes, pane: *Pane) ![]const []const u8 { return c.rows; } +test "rows asked for after a pane's cached rows were dropped, before the sweep, leak nothing" { + if (comptime !enabled) return error.SkipZigTest; + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer p.deinit(); + const pane = p.panes[0].?; + try std.testing.expect(pane.terminal != null); + _ = try shellRows(p, pane); + // A pane's teardown drops its entry; the rows stay until the sweep. + p.shell_rows.dropPane(pane); + _ = try shellRows(p, pane); +} + /// The full modal motion surface: shell history with the live edit overlay /// spliced into the rows it covers. pub fn cursorLines(p: *Pardes, pane: *Pane) ![]const []const u8 { -- cgit v1.3