Skip to content

crash: capacity changes during print with managed memory can be unsafe #11261

Closed

Description

@mitchellh

From #11249

Test case (not cleaned up a ton):

// Reproduces a crash found by fuzzing where Terminal.print keeps a stale
// pointer to the previous cell when a hyperlink write causes page growth.
test "Terminal: grapheme append after hyperlink growth" {
    const alloc = testing.allocator;
    var t = try init(alloc, .{ .cols = 80, .rows = 24 });
    defer t.deinit(alloc);

    t.modes.set(.grapheme_cluster, true);
    try t.screens.active.startHyperlink("h", null);
    try t.printString("HeHelloooooooo");
    try t.print(0x1E);
    try t.print(0x1E);
    try t.print(0x1E);
    try t.print(0x1E);
    for (0..45) |_| try t.print(0xFFFD);
    try t.print(0x0600);
    try t.print(0xFFFD);
}

Stack:

thread 1206566 panic: reached unreachable code
/nix/store/aywnv0mzxax3lgr1ynn2ix88haxy5na8-zig-0.15.2/lib/std/debug.zig:559:14: 0x104d21d47 in assert (ghostty-test)
    if (!ok) unreachable; // assertion failure
             ^
/Users/mitchellh/Documents/ghostty/src/terminal/page.zig:1487:54: 0x104e4cf7f in appendGrapheme (ghostty-test)
        if (build_options.slow_runtime_safety) assert(cell.codepoint() != 0);
                                                     ^
/Users/mitchellh/Documents/ghostty/src/terminal/Screen.zig:2139:50: 0x104e3a623 in appendGrapheme (ghostty-test)
    self.cursor.page_pin.node.data.appendGrapheme(
                                                 ^
/Users/mitchellh/Documents/ghostty/src/terminal/Terminal.zig:533:51: 0x104e37263 in print (ghostty-test)
            try self.screens.active.appendGrapheme(prev.cell, c);
                                                  ^
/Users/mitchellh/Documents/ghostty/src/terminal/Terminal.zig:13022:16: 0x104e3e20f in test.Terminal: grapheme append after hyperlink growth (ghostty-test)
    try t.print(0xFFFD);
               ^
/nix/store/aywnv0mzxax3lgr1ynn2ix88haxy5na8-zig-0.15.2/lib/compiler/test_runner.zig:130:29: 0x104dbd9db in mainServer (ghostty-test)
                test_fn.func() catch |err| switch (err) {
                            ^
/nix/store/aywnv0mzxax3lgr1ynn2ix88haxy5na8-zig-0.15.2/lib/compiler/test_runner.zig:64:26: 0x104dbafdb in main (ghostty-test)
        return mainServer() catch @panic("internal test runner failure");
                         ^
/nix/store/aywnv0mzxax3lgr1ynn2ix88haxy5na8-zig-0.15.2/lib/std/start.zig:618:22: 0x104dbaa07 in main (ghostty-test)
            root.main();

Root cause:

The crash is a stale-pointer bug in grapheme-cluster handling.

What Is Happening (from AI, but verified its true):

  1. In Terminal.print, the grapheme path caches prev.cell early, then may reshape that cell as wide and emit a spacer via printCell.
  2. printCell applies active hyperlink state with cursorSetHyperlink.
  3. cursorSetHyperlink can call increaseCapacity, which reallocates/clones the page and reloads cursor pointers.
  4. The old prev.cell pointer is now invalid, but Terminal.print still uses it in appendGrapheme, triggering assertions in Page.appendGrapheme (cell.codepoint() != 0 in safety builds, row.grapheme consistency asserts in non-safety paths).

Activity

  1. added theissue type on Mar 9, 2026
  2. added
    crashCauses a crash, very high priority to fix
    vtControl sequence related
    on Mar 9, 2026
  3. mitchellh commented on Mar 9, 2026

    @mitchellh
    ContributorAuthor

    The AI-proposed solution was to recompute prev at every point it's used, but that's too expensive for how hot path this function is. I propose instead trying to pursue a solution that recomputes prev only if the underlying page capacity was changed during an operation.

    There are multiple ways to detect this, and we should consider what is cheapest...

  4. mitchellh commented on Mar 9, 2026

    @mitchellh
    ContributorAuthor

    Here's another test case:

    test "Terminal: VS16 widening with full hyperlink map" {
        // Regression test for a fuzz crash from current3-min.txt.
        // The crash path is print's grapheme `.wide` branch holding `prev.cell`
        // across a spacer-tail print that grows hyperlink storage.
        var t = try init(testing.allocator, .{ .rows = 10, .cols = 1024 });
        defer t.deinit(testing.allocator);
    
        t.modes.set(.grapheme_cluster, true);
        try t.screens.active.startHyperlink("http://example.com", null);
    
        // Fill hyperlink map to exactly one free slot so that the '#' write
        // fills the last slot and VS16's spacer-tail write triggers growth.
        while (true) {
            const page = &t.screens.active.cursor.page_pin.node.data;
            const count = page.hyperlinkCount();
            const capacity = page.hyperlinkCapacity();
            try testing.expect(capacity > 1);
            if (count + 1 >= capacity) break;
            try t.print('x');
        }
    
        {
            const page = &t.screens.active.cursor.page_pin.node.data;
            try testing.expectEqual(page.hyperlinkCapacity() - 1, page.hyperlinkCount());
        }
    
        // Keycap base, used by existing VS16 tests.
        try t.print('#');
        {
            const page = &t.screens.active.cursor.page_pin.node.data;
            try testing.expectEqual(page.hyperlinkCapacity(), page.hyperlinkCount());
        }
    
        // Previously panicked in page.appendGrapheme (unreachable assert).
        try t.print(0xFE0F);
    }
  5. added a commit that references this issue on Aug 21, 2026
    c8634f3
  6. added this to the 1.4.0 milestone on Aug 21, 2026
  7. added a commit that references this issue on Aug 21, 2026
    33cda4d
  8. added a commit that references this issue on Oct 11, 2026
    4beb237
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    crashCauses a crash, very high priority to fixvtControl sequence related

    Type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions