Skip to content

terminal: saturate cursor subtraction in resizeCols - #12907

Merged
mitchellh merged 2 commits into
ghostty-org:mainfrom
c0x0o:main
Jun 4, 2026
Merged

mitchellh merged 2 commits into
ghostty-org:mainfrom
c0x0o:main

Conversation

@c0x0o

@c0x0o c0x0o commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

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.

This problem is reported by discussion#12905

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.
@c0x0o
c0x0o requested a review from a team as a code owner June 3, 2026 06:44
@ghostty-bot ghostty-bot Bot added the vt Control sequence related label Jun 3, 2026
@mitchellh

Copy link
Copy Markdown
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.
@c0x0o

c0x0o commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Two unit tests are added: one for PageList itself, one for libvt c API(respect the reproduce PoC in discussion), PTAL @mitchellh

@mitchellh mitchellh added this to the 1.3.2 milestone Jun 4, 2026
@mitchellh
mitchellh merged commit 4782e59 into ghostty-org:main Jun 4, 2026
93 checks passed
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", .{});
}
```
@mitchellh mitchellh modified the milestones: 1.3.2, 1.4.0 Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

vt Control sequence related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants