From a9bc331ab74f12b6b11fa6578d829d0da50ed636 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 6 Jul 2026 13:09:16 -0300 Subject: fix drag-select release crash: the 64-slot effect ring overflowed when one update emitted a big selection exec (emitWrite 64-byte chunks + padOutputBelowEdits CRs) and the emit assert panicked. Effects queue is now a growable ArrayList FIFO (payloads stay inline values); guard the two other Buf(256) asserts reachable from look (spawn cwd >256 -> error.PathTooLong in newShell, >256-char url skipped). New snap exec-bigdrag: giant middle-drag exec on a 60x200 grid, panicked before, alive-marker golden after. --- src/pardes.zig | 22 +++++++++----- test/snapshots/exec-bigdrag.golden | 61 ++++++++++++++++++++++++++++++++++++++ test/snapshots/exec-bigdrag.snap | 40 +++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 test/snapshots/exec-bigdrag.golden create mode 100644 test/snapshots/exec-bigdrag.snap diff --git a/src/pardes.zig b/src/pardes.zig index c4893b25..d617e552 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -628,9 +628,11 @@ pub const Pardes = struct { /// press during an active middle select-drag; consumed on middle release chord_arg: ?[]u8 = null, - effects: [64]Effect = undefined, + /// pending effects, drained by the shell after each update. Growable: one + /// update can emit unbounded writes (a big selection executed is chunked + /// into 64-byte write effects) — a fixed ring overflows and panics. + effects: std.ArrayList(Effect) = .empty, effects_head: usize = 0, - effects_tail: usize = 0, /// modal yank register (gpa-owned); a yank also mirrors out to the system /// clipboard via the set_clipboard effect. @@ -678,6 +680,7 @@ pub const Pardes = struct { if (p.yank) |y| gpa.free(y); if (p.dump_out) |d| gpa.free(d); if (p.chord_arg) |a| gpa.free(a); + p.effects.deinit(gpa); p.scratch.deinit(); gpa.free(p.surface.cells); gpa.destroy(p); @@ -717,6 +720,7 @@ pub const Pardes = struct { fn newShell(p: *Pardes, id: usize, cwd: []const u8) !*Pane { std.debug.assert(p.panes[id] == null); + if (cwd.len > 256) return error.PathTooLong; // spawn effect cwd is a Buf(256) const pane = try p.gpa.create(Pane); pane.* = .{ .vt = try ghostty_vt.Terminal.init(p.gpa, .{ @@ -780,14 +784,16 @@ pub const Pardes = struct { } fn emit(p: *Pardes, e: Effect) void { - std.debug.assert(p.effects_tail - p.effects_head < p.effects.len); - p.effects[p.effects_tail % p.effects.len] = e; - p.effects_tail += 1; + p.effects.append(p.gpa, e) catch {}; } pub fn nextEffect(p: *Pardes) ?Effect { - if (p.effects_head == p.effects_tail) return null; - const e = p.effects[p.effects_head % p.effects.len]; + if (p.effects_head == p.effects.items.len) { + p.effects.clearRetainingCapacity(); + p.effects_head = 0; + return null; + } + const e = p.effects.items[p.effects_head]; p.effects_head += 1; return e; } @@ -2607,7 +2613,7 @@ pub const Pardes = struct { }; switch (look.resolve(txt, cwd, &realbuf)) { .none => {}, - .url => |u| p.emit(.{ .open_link = .from(u) }), + .url => |u| if (u.len <= 256) p.emit(.{ .open_link = .from(u) }), .dir => |dir| { // focus an existing terminal on this dir, else fork one below for (p.panes, 0..) |slot, i| { diff --git a/test/snapshots/exec-bigdrag.golden b/test/snapshots/exec-bigdrag.golden new file mode 100644 index 00000000..613bb342 --- /dev/null +++ b/test/snapshots/exec-bigdrag.golden @@ -0,0 +1,61 @@ +== snap alive grid=200x60 cursor=104,37 +|Kill Newcol Tutor Debug Colors NextColor Dump +| nm /tmp/pardes-snap/exec-bigdrag/cwd Del nm /tmp/pardes-snap/exec-bigdrag/cwd Del +| bash: line: command not found +| big.txt +| bash: 4line: command not found +| +| bash: 15line: command not found +| +| bash: 726line: command not found +| +| bash: 2737line: command not found +| +| bash: 2748line: command not found +| +| bash: e: command not found +| +| bash: ne: command not found +| +| bash: ine: command not found +| +| bash: line: command not found +| +| bash: 2line: command not found +| +| bash: 13line: command not found +| +| bash: 824line: command not found +| +| bash: 2835line: command not found +| +| bash: 2846line: command not found +| nm /tmp/pardes-snap/exec-bigdrag/cwd Del +| bash: e: command not found +| big.txt +| bash: ne: command not found +| +| bash: ine: command not found +| echo alive-o''k +| bash: line: command not found +| +| bash: 0line: command not found +| +| bash: 11line: command not found +| +| bash: 922line: command not found +| +| bash: 2933line: command not found +| +| bash: 2944line: command not found +| +| bash: e: command not found +| +| bash: ne: command not found +| +| bash: ine: command not found +| +| bash: line: command not found +| +| alive-ok +| diff --git a/test/snapshots/exec-bigdrag.snap b/test/snapshots/exec-bigdrag.snap new file mode 100644 index 00000000..dae92205 --- /dev/null +++ b/test/snapshots/exec-bigdrag.snap @@ -0,0 +1,40 @@ +# a giant accidental middle-drag: the whole selection is executed, chunked +# into 64-byte write effects in ONE update — this used to burst the fixed +# 64-slot effect ring and panic the core (assert in emit). Needs a big grid: +# the selection must exceed 64*64 bytes, more than any 30x100 pane can hold. +lines big.txt 3000 +start 60 200 +wait 8000 big.txt +stable 500 20000 +press left 105 8 +release left 105 8 +stable 300 5000 +key i +text tr -d '\n' < big.txt +key esc +settle 400 +press middle 105 8 +drag middle 126 8 +release middle 126 8 +stable 800 30000 +# the accidental sweep over the full-height right pane, then release +press middle 105 4 +drag middle 150 30 +drag middle 198 58 +release middle 198 58 +settle 500 +stable 800 30000 +# still alive: type and execute a marker command +press left 105 40 +release left 105 40 +stable 300 5000 +key i +text echo alive-o''k +key esc +settle 300 +press middle 105 40 +drag middle 119 40 +release middle 119 40 +wait 10000 alive-ok +stable 800 20000 +snap alive -- cgit v1.3