diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 10:13:10 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | 94343583eb6aefa8f421083409256c877aad036e (patch) | |
| tree | 7e5417c3bfcda1b005384d5deee8433651a43c4f /src/terminal.zig | |
| parent | 704af30412063c5efd61d8d54f843bf7fb750811 (diff) | |
| download | pardes-94343583eb6aefa8f421083409256c877aad036e.tar.gz pardes-94343583eb6aefa8f421083409256c877aad036e.zip | |
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 <[email protected]>
Diffstat (limited to 'src/terminal.zig')
| -rw-r--r-- | src/terminal.zig | 17 |
1 files changed, 16 insertions, 1 deletions
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 { |
