Skip to content

WASM memory corruption after freeing terminal that processed multi-codepoint grapheme clusters #141

Open

Description

@beorn

Environment

  • OS: macOS 15.4 (Darwin 25.2.0, arm64)
  • Package: ghostty-web@0.4.0
  • Runtime: Bun 1.3.9

Description

Calling terminal.free() after writing any multi-codepoint grapheme cluster (flag emoji, skin tone modifiers, ZWJ sequences, keycap sequences) corrupts the shared WASM memory. All subsequent createTerminal() + write() calls crash with an out-of-bounds memory access.

Single-codepoint characters (ASCII, CJK, simple emoji like 😀) do not trigger the bug.

Reproduction

import { Ghostty } from "ghostty-web";

const ghostty = await Ghostty.load();

const term1 = ghostty.createTerminal(80, 24);
term1.write("👋🏽");  // Any multi-codepoint grapheme cluster
term1.free();         // ← corrupts WASM memory

const term2 = ghostty.createTerminal(80, 24);
term2.write("Hello"); // ← crashes

Save as repro.ts and run with bun run repro.ts.

Characters that trigger the bug

All multi-codepoint grapheme clusters:

  • Flag emoji: 🇺🇸 🇬🇧 🇯🇵 (regional indicator pairs)
  • Skin tone: 👋🏽 (base + Fitzpatrick modifier)
  • ZWJ families: 👨‍👩‍👧 (ZWJ sequences)
  • Keycap: 1️⃣ (digit + VS16 + combining enclosing keycap)

Characters that do NOT trigger the bug

Single-codepoint characters:

  • ASCII: Hello
  • CJK: 漢字
  • Simple emoji: 😀 (single codepoint, no modifiers)

Error Output

RuntimeError: Out of bounds memory access (evaluating 'this.exports.ghostty_terminal_write(this.handle, g, B.length)')
    at write (ghostty-web.js:145:64)

The crash is in ghostty_terminal_write — the WASM linear memory has been corrupted by the preceding free(), so the next write() accesses invalid memory.

Impact

This blocks using ghostty-web for multi-terminal workflows (test suites, terminal pools, tab management) where terminals are created, used, and freed in sequence. If any terminal processes grapheme clusters before being freed, all subsequent terminals in the same WASM instance are broken.

Workaround

Don't free terminals that have processed multi-codepoint grapheme clusters. Leak them instead:

// Instead of term.free(), just abandon the reference
// term.free()  // ← skip this
term = null;

Or use one process per terminal (expensive but avoids shared WASM state).

Activity

  1. tsukasagenesis commented on Sep 15, 2026

    @tsukasagenesis

    Some data on this one, from chasing a similar-looking fault in an embedder.

    This bug is distinct from the cursorDownScroll one, and bec9e16 (#180) does not fix it. That distinction cost me a round of measurement, so it seems worth recording here.

    The two present almost identically — memory access out of bounds originating in BitmapAllocator — but they have different triggers:

    • The fix: clear scrolled row cells in wasm patch #180 bug needs only a write: Screen.cursorDownScroll skipped clearing a scrolled-in row unless a background colour was set, leaving stale content_tag = .codepoint_grapheme markers behind that point at grapheme offsets which no longer exist.
    • This issue additionally requires free() — the multi-codepoint cluster write and the terminal teardown together. Applying the fix: clear scrolled row cells in wasm patch #180 hunk alone leaves it reproducing.

    The faulting frame in both cases is BitmapAllocator.free (src/terminal/bitmap_allocator.zig). I confirmed that identification rather than guessing it: the shipped wasm-function[162] is byte-identical (177 bytes) to the same function in a from-source rebuild that reproduces the published 0.4.0 artifact bit-for-bit (d6f0326f1874ad2c).

    One practical note for anyone reproducing this: a single fault poisons the shared WASM heap, so every case after the first in the same page reports a fault regardless of its own input. Test cases have to run on a fresh page (or at minimum a fresh module instance) or the results are meaningless — several of my early runs were invalid for exactly this reason before I caught it.

    Environment: ghostty-web 0.4.0 from npm, Chromium 152 and Electron 42, Zig 0.15.2 for the rebuilds.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions