summaryrefslogtreecommitdiff
path: root/src/terminal.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 10:13:10 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit94343583eb6aefa8f421083409256c877aad036e (patch)
tree7e5417c3bfcda1b005384d5deee8433651a43c4f /src/terminal.zig
parent704af30412063c5efd61d8d54f843bf7fb750811 (diff)
downloadpardes-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.zig17
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 {