
2026/01/11 3:58
**GhostTyの最大メモリリークを発見し修正する**
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
Ghostty の長時間にわたるセッションは、
mmap(スクロールバックバッファに使用される)で割り当てられたページが解放されないため、最大 37 GB の RAM をリークしていました。アプリはターミナルコンテンツを PageList に保存します。これは「標準」(プールから取得したもの)または「非標準」(mmap)のメモリページで構成される双方向リンクリストです。スクロールバックの削減時に、Ghostty は誤って最も古いページを新しいページとして再利用します:そのメタデータだけを「標準サイズ」にリサイズし、大きな mmap 割り当てはそのまま残します。この再利用されたページが後で解放されると、Ghostty はそれを標準とみなし、munmap を呼び出す代わりにプールへ返却してしまい、メモリブロックがリークしたままとなります。このバグは Ghostty 1.0 から存在しましたが、大量のスクロールバックバッファ(例:多くの絵文字とハイパーリンクを含む Claude Code など)を生成する CLI アプリでのみ顕在化し、非標準ページ割り当てをトリガーします。既存のリーク検出器は特定の実行時条件下で発生するため、検知できませんでした。
新しいテストが問題を再現しリークを確認しました。統合された修正では、削減中に 非標準ページを破棄(
self.destroyNode(first))し、プールから新しい標準サイズのページで置き換えるようになっています。この修正は Ghostty 1.3(3 月)に組み込まれます。既に Nightly リリースにはパッチが含まれています。さらに、macOS のメモリタグ付け(
mach.taggedPageAllocator(.application_specific_1))を追加し、PageList 割り当てにタグを付与して修正の検出と確認を簡素化しました。この更新により、長時間ターミナルセッションを実行するユーザー—特に重い CLI ワークロードを扱う開発者は――メモリ使用量が急増する問題が解消され、個人およびプロダクションでアプリに依存している組織の両方に対し、より安定かつ信頼性の高い Ghostty エクスペリエンスを提供します。本文
数か月前、ユーザーから Ghostty が膨大なメモリを消費しているという報告が寄せられました。10 日間稼働した後に 37 GB を使用していたというケースです。
本日、修正が見つかりマージされたことをご報告します。本稿ではリークの原因、Ghostty の内部構造、および追跡方法を簡潔にまとめています。
このリークは Ghostty 1.0 以降に存在していましたが、最近になって特定の CLI アプリ(特に Claude Code)が発生させる条件で大規模に顕在化しました。トリガー条件が限定的だったため診断が難しくなっていた点が特徴です。
修正はマージ済みで nightly / tip リリースに反映されています。3 月のタグ付き 1.3 版にも含まれます。
PageList
バグを理解するにはまず Ghostty が端末メモリをどのように管理しているかを知る必要があります。Ghostty は PageList と呼ばれるデータ構造でターミナル内容(文字、スタイル、ハイパーリンク等)を保持します。
- PageList – 端末コンテンツを格納するメモリページの双方向連結リスト。
Page 1 最も古いスクロールバック Page 2 Page 3 Page 4 最新のアクティブ画面
「ページ」は単一の仮想メモリページではなく、ページ境界に揃えられた連続したメモリブロックです。サイズはシステムページ数の偶数倍で構成され、
mmap で確保します。mmap は呼び出しが遅いため、頻繁なシステムコールを避けるためにメモリプールを利用しています。新しいページが必要になったらプールから取り出し、使い終わったら再度返却します。
プールは標準サイズのページのみを扱います。これは「一般的な配送箱」を購入するようなもので、ほとんどの荷物が同じ箱に収まるため効率化できます。ただし端末では時折標準ページより大きいメモリが必要になる場合があります。行数に絵文字やスタイル、ハイパーリンクが多く含まれると、大きなページを確保する必要があります。このようなケースではプールを経由せず
mmap で直接非標準ページを割り当てます(稀なシナリオ)。
ページの割り当てタイプ
| タイプ | サイズ | 再利用 |
|---|---|---|
| 標準ページ(プールから) | 固定 | 破棄時にプールへ返却 |
非標準ページ ( 直接) | 可変(標準より大きい) | を呼び、再利用不可 |
ページを「解放」するときは次のような単純ロジックが適用されます。
if page <= 標準サイズ → プールへ返却 else → munmap で解放
これが Ghostty の端末メモリ管理の基本設計です。概念自体は正しく、最適化に伴うロジックバグがリークを生み出しました。
スクロールバックの削除
Ghostty にはスクロールバック制限設定があります。この上限に達すると古いページを削除してメモリを解放します。
大量データを高速で出力する際など、頻繁に発生するため
mmap の呼び出しはコストが高くなります。そのため、上限に達したときは「最古のページをそのまま最新の位置へ再利用」する最適化を行っています。
スクロールバック削除:最古ページを再利用 前:上限に到達 → 先頭から削除し末尾へ再配置 後 :再利用されたページが末尾にある Page 2 → 最古になる Page 3 Page 4
この最適化は割り当てを行わず、ポインタ操作だけで済むため高速です。メタデータのクリア処理も行いますが、実際のメモリ領域はそのまま残します。
バグ
スクロールバック削除時にページサイズを標準サイズへ「再設定」していましたが、実際の
mmap 割り当ては変わっていませんでした。つまり、非標準(大きめ)メモリ領域を持つページを標準サイズと誤認し、PageList はそれをプールから取得したものだと考えていました。
非標準ページ確保 スクロールバックで再利用 BUG:メタデータは std_size に戻るが、mmap はそのまま ページ解放時 → std_size とみなされ munmap が呼ばれない 結果:標準→非標準 → メモリリーク
最終的に端末を閉じた際などにページを破棄するとき、メモリは「標準サイズ」に見えるためプールに戻るとみなされ
munmap が呼ばれません。これが典型的なリークです。
非標準ページは設計上稀であり、最適化の目的は標準ページを主流にすることです。そのため非標準ページが発生した場合には破棄して新しい標準ページを確保する方針でした。
Claude Code の影響
Claude Code の CLI は多くのマルチコードポイント文字(絵文字など)を出力し、Ghostty が頻繁に非標準ページを使用します。さらに主画面で大量スクロールバックが発生するため、リークが大きな量で顕在化しました。
このバグは Claude Code の設計上の問題ではなく、Ghostty に対して長年存在した欠陥を露呈させただけです。
修正
概念的には非常にシンプルです。非標準ページは再利用しないことです。
スクロールバック削除時に非標準ページが見つかったら、
munmap で破棄し、プールから新しい標準サイズのページを割り当てます。
if (first.data.memory.len > std_size) { self.destroyNode(first); break :prune; }
非標準ページを再利用して大きいメモリ領域を保持することも可能ですが、現時点では「標準ページが主流」という前提の下でシンプルに標準サイズへ戻す方針です。
より複雑な戦略(非標準ページ使用頻度の統計を取って動的に切り替える等)は検討中ですが、まずは確実にバグを修正することが最優先でした。
追加改善
修正作業と同時に macOS の Mach カーネル提供機能で仮想メモリタグをサポートしました。これにより PageList のメモリアロケーションに特定の識別子を付与でき、ツール上で簡単に可視化できます。
inline fn pageAllocator() Allocator { // テスト時は leak 検出用アロケータを使用 if (builtin.is_test) return std.testing.allocator; // 非 macOS では標準 Zig ページアロケータを利用 if (!builtin.target.os.tag.isDarwin()) return std.heap.page_allocator; // macOS ならメモリにタグ付けして core terminal 用に識別 const mach = @import("../os/mach.zig"); return mach.taggedPageAllocator(.application_specific_1); }
macOS 上でメモリをデバッグすると、Ghostty の PageList メモリが特定のタグ付きとして表示されます。これによりリーク箇所の特定と修正後の動作確認が容易になりました。
Ghostty におけるリーク防止策
Ghostty では以下の手法でメモリリークを検出・回避しています。
- デバッグビルドや単体テストではリーク検出 Zig アロケータを使用
- CI は毎コミットごとに Valgrind をフルユニットテストスイートで実行し、リークだけでなく未定義メモリアクセスも検出
- macOS GUI 版は Instruments で頻繁にリークチェック(特に Swift コードベース)
- GTK 関連 PR は Valgrind(フル GUI)で実行し、単体テスト対象外の GTK コードパスのリークを確認
これまで有効でしたが、本件は非常に限定的な条件下でのみ発生したため既存テストでは検出できませんでした。マージされた PR には再現性のあるテストも追加され、将来の回帰防止になります。
結論
これまで Ghostty に報告された最大規模のメモリリークであり、複数ユーザーから確認された唯一のケースです。今後もメモリ関連レポートを監視し続けますが、再現性のあるテストこそが診断と修正への鍵となります。
@grishy さんには信頼できる再現環境を提供していただき、自身で解析できたことに感謝しています。彼らの分析も私の結論と一致し、再現実験により両者の理解が独立して確認できました。また、詳細な診断情報(フットプリント出力や VM リージョン数)を報告いただいた皆さんにも感謝します。コミュニティから得た手掛かりが PageList を特定する鍵となりました。
本稿は AI の支援なしで執筆されました。図表に関しては AI が一部補助しましたが、すべて人間がレビューし正確性を確認しています。テキスト内容は AI によって生成されたものではありません。