coder / coder/ghostty-web

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

Open
#141 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
2.8k
Forks
172
PR merge metrics
No merged PRs in 30d

Description

## 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

```typescript
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:

```typescript
// 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).

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.