Repository navigation
terminal: saturate cursor subtraction in resizeCols - #12907
Merged
Merged
Conversation
PageList.resize takes the .lt branch when columns shrink, which calls resizeWithoutReflow (mutating self.rows to the new smaller value) and then resizeCols with the original opts.cursor.y. When both axes shrink in one call and the cursor sits at or past the new bottom row, the expression `self.rows - c.y - 1` underflows and panics in safety builds. Use saturating subtraction; "remaining rows below cursor" is 0 once the cursor sits at or past the new bottom.
Contributor
|
Thank you. We should cover this with a unit test. |
Adds a PageList regression test exercising the underflow path fixed in 7fa6fff, and a libghostty-vt C API test mirroring the original repro through ghostty_terminal_resize.
Contributor
Author
|
Two unit tests are added: one for PageList itself, one for libvt c API(respect the reproduce PoC in discussion), PTAL @mitchellh |
jcollie
approved these changes
Jun 3, 2026
1 task done
mitchellh
added a commit
that referenced
this pull request
Jun 5, 2026
…ck (#12935) Found this issue when testing some search features; follow up for #12907. You can either reproduce using the PoC below with `libghostty-vt` or run Ghostty debug on macOS and follow these steps: 1. Open Ghostty and start search `0`. 2. Press `cmd+=` to increase font size. 3. You should see a panic crash. ### AI Disclosure As the commit suggests, Claude implemented the fix, the unit test, and PoC file. I reviewed it(seems reasonable to me, but I’m not a Zig professional) and tested it myself. ```zig // PoC: resize panic when shrinking both axes with the cursor near the top // of a fully-populated screen. // // Build (with libghostty-vt headers + dylib on the standard search paths): // zig run poc.zig -lghostty-vt // // Or point at a local build: // zig run poc.zig -I <prefix>/include -L <prefix>/lib -lghostty-vt // // At runtime the dylib must be discoverable (DYLD_LIBRARY_PATH on macOS, // LD_LIBRARY_PATH on Linux, or an rpath baked in at link time). // // Without the fix, this aborts with // reached unreachable code (assert in PageList.Pin.pageIterator) // at _terminal.PageList.resizeCols on a debug/safe build. On release it // silently iterates an empty (reversed) range. const std = @import("std"); const c = @cImport({ @Cinclude("ghostty/vt.h"); }); pub fn main() !void { var term: c.GhosttyTerminal = null; const opts: c.GhosttyTerminalOptions = .{ .cols = 80, .rows = 24, .max_scrollback = 1000, }; if (c.ghostty_terminal_new(null, &term, opts) != c.GHOSTTY_SUCCESS) { return error.InitFailed; } defer c.ghostty_terminal_free(term); // Fill every one of the 24 active rows with non-blank content. This is // what makes the bug reachable: when rows shrink, resizeWithoutReflow // can only trim *blank* trailing rows, so non-blank rows are instead // pushed up into scrollback and the active-area top moves down. { var buf: [256]u8 = undefined; var i: usize = 0; while (i < 24) : (i += 1) { // "X" on each row; CR+LF between rows but not after the last so // we don't scroll the top row away. const line = if (i + 1 < 24) std.fmt.bufPrint(&buf, "X\r\n", .{}) catch unreachable else std.fmt.bufPrint(&buf, "X", .{}) catch unreachable; c.ghostty_terminal_vt_write(term, line.ptr, line.len); } } // CSI 1;1H -> park the cursor on the TOP row (1-based). The active area is // anchored to the bottom, so once we shrink rows this row falls above the // new active-area top, i.e. into scrollback. const move = "\x1b[1;1H"; c.ghostty_terminal_vt_write(term, move.ptr, move.len); // Shrink both axes. Columns must shrink to take resize()'s .lt branch, // which runs the row shrink first and then resizeCols with the original // (now out-of-active-area) cursor pin. Panics in // _terminal.PageList.resizeCols. _ = c.ghostty_terminal_resize(term, 79, 20, 8, 16); std.debug.print("survived resize (fix is present)\n", .{}); } ```
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PageList.resize takes the .lt branch when columns shrink, which calls resizeWithoutReflow (mutating self.rows to the new smaller value) and then resizeCols with the original opts.cursor.y. When both axes shrink in one call and the cursor sits at or past the new bottom row, the expression
self.rows - c.y - 1underflows and panics in safety builds.Use saturating subtraction; "remaining rows below cursor" is 0 once the cursor sits at or past the new bottom.
This problem is reported by discussion#12905