ソフトウェア

とあるアプリで尋常ではないメモリリークが報告され修正されるまで


2025年末に実は非営利団体化していることが明らかになったターミナルエミュレーターの「Ghostty」ですが、メモリリークの問題が発覚したものの、開発チームが迅速に対応し直後のアップデートで修正されました。

Finding and Fixing Ghostty's Largest Memory Leak – Mitchell Hashimoto
https://mitchellh.com/writing/ghostty-memory-leak-fix

◆メモリリークとは
メモリリークとは、プログラムが使用したメモリを解放せずに保持し続ける現象のことを指します。メモリリークが発生するとコンピューターシステムが使用できるメモリがどんどん減少することによりパフォーマンスが低下し、最終的にはシステムのクラッシュやフリーズの原因となることもあります。

なぜメモリリークが発生するのかを考えるうえで、システムが管理するメモリ領域には「スタック領域」と「ヒープ領域」の2種類があることを理解する必要があります。スタック領域は関数の呼び出しやローカル変数の管理に使用され、使用可能なメモリサイズが固定である代わりに、関数が終了すると自動的に解放されます。一方、ヒープ領域は動的にメモリを割り当てるために使用され、実行時にメモリサイズを自由に指示できる利点がある代わりに、プログラムが明示的に解放しない限り保持され続けます。ヒープ領域のメモリを割り当てたり解放したりする際にはシステムコールを使用しますが、当記事中ではmmap()・munmap()がそれに該当します。メモリリークはヒープ領域で見られる現象で、プログラムが動的に割り当てたメモリを解放し忘れることにより発生します。


動的にメモリを確保する場合でもモダンなプログラミング言語ではガベージコレクション(GC)と呼ばれる仕組みが備わっており、不要になったメモリを自動的に解放してくれます。ただGCも万能ではなく、特にオブジェクト同士が相互参照している場合などGCが不要と判断できないケースではメモリリークが発生することがあります。

◆Ghosttyのメモリリーク
Ghosttyのユーザーから開発元に不可解なメモリ消費が報告されるようになりました。とあるユーザーが遭遇したケースでは37GBものメモリが消費されたとのことです。調査を進めるうちに以下の事実が判明しました。

・メモリリークの原因は初期(Ghostty 1.0)の頃から存在していたが発生条件が満たされることはなかった
・人気のあるCLIアプリケーションを使用することでメモリリークの発生条件が満たされるようになった
・特にClaude Codeを使用すると顕著にメモリリークが発生する

発生する条件が限定的だったために原因の特定は困難を極めました。

◆Ghosttyのメモリ管理
どうしてメモリリークが発生したかを理解するには、Ghosttyがターミナルのメモリをどのように管理しているのかを知る必要があります。Ghosttyはターミナルコンテンツを「ページリスト」と呼ばれるデータ構造で保持しています。ページリストは文字・スタイル・ハイパーリンクといったターミナルコンテンツを格納する「メモリページ」を複数持っており、双方向連結リストとして管理しています。


ページリストの基盤となるページ群はmmap()を用いてメモリの割り当てを行っています。ただmmap()はそれほど高速ではないので毎回呼び出さなくて済むように「メモリプール」を併用しています。ページを生成する際にはまずメモリプールから空きページを探し、見つからなければmmap()で新たにページを確保します。ページが不要になった場合はメモリプールに返却され、再利用の機会を待つという仕組みです。メモリプールはページに対して「標準サイズ」を持っています。身近なイメージとしては標準サイズの収納箱を用意しておけば何かと融通が利くのと同じです。

ただし、ターミナルが標準サイズのページよりも多くのメモリを必要とするケースもあります。例えば多くの絵文字・スタイル・ハイパーリンクを含む行がある場合などです。このような場合、Ghosttyは標準サイズよりも多いメモリをmmap()で確保した「非標準ページ」をページリストに追加します。つまりプールを完全にバイパスするわけですが、これはレアケースに該当します。


ページを「解放」する際にはごく簡単なロジックを適用します。

・ページが標準サイズ以下の場合:プールに戻す
・ページが標準サイズより大きい場合:munmap()を呼び出しメモリを解放する

つまりGhosttyのメモリ管理は合理的な仕組みで動作しているといえます。

◆スクロールバック制限の最適化
不具合の背景を理解するにはもう一つ、「スクロールバック」について知る必要があります。スクロールバックとは、ターミナルで過去の出力履歴をさかのぼって閲覧できる機能のことです。Ghosttyには履歴の上限を設けることでスクロールバックを制限する設定があり、制限に達すると古いページを削除する仕組みを備えています。しかしながら大量のデータを高速で処理しようとすると頻繁にスクロールバックの制限に到達し、しかもページの削除と追加はメモリプールを活用してなおコストの高い処理となっています。よって、制限に達した場合はページリスト内で「最も古いページ」を「最新のページ」に付け替えて再利用するという手法で処理の最適化を図っています。


スクロールバック制限の最適化はメモリ割り当てを一切使用せず、またページリストの先頭から末尾へページを移動させるだけ、プログラミング的な表現をすれば双方向連結リストのポインタの指す先を変更するだけなので非常に効果的です。ページの内容をクリアするためにメタデータのクリーンアップを実行する以外はメモリをそのまま流用します。

◆メモリリークの原因
スクロールバック制限に関する最適化において、ページは常に「標準サイズ」にリサイズされました。ただしこの時、メモリ割り当てしたサイズはそのままでメタデータに含まれるサイズを変更しただけでした。そのため割り当てられたメモリサイズが標準サイズより大きかった場合でも、再利用されたページの持つサイズは標準サイズで上書きされるため、ページリストは標準サイズであると認識するようになったのです。


ページのメモリを解放する状況には様々なものがあり、最も一般的なものとしてはユーザーがターミナルを閉じたときです。その際にメモリサイズを確認するといずれも「標準サイズ」となっているため、ヒープ領域の解放を行うmunmap()を呼び出していませんでした。これは典型的なメモリリークのパターンであり、Ghosttyで発生したメモリリークの根本原因はここにありました。

問題は「非標準ページ」が発生することが設計上レアケースであったことです。ページリストの設計および最適化は標準サイズのページを前提としていたため、非標準ページが存在するケースはごく特殊なシナリオに限定されており、非標準ページの生成数は少量でした。しかしClaude Codeが登場したことで状況が一変します。Claude CodeのCLIは大量のマルチコードポイント文字を出力するためGhosttyは頻繁に非標準ページを使用するようになりました。さらにClaude Codeは標準出力に大量の出力を行うためスクロールバック制限に達する頻度も高くなり、結果として非標準ページが再利用されるケースが増加しました。これによりメモリリークが顕在化し多くのユーザーが異常なメモリ消費に気付く事態となったのです。

◆解決
解決策はとてもシンプルで「非標準ページを絶対に使い回さないこと」です。スクロールバック制限に達してページの再利用を行う場合、非標準ページは必ずmunmap()を呼び出してメモリを解放し、新たに標準サイズのページを割り当てるように修正されました。これによりGhosttyのメモリリークは完全に解消されました。非標準ページを再利用して大きなメモリサイズを維持する手もありましたが、標準ページを使用するという当初の設計思想がある以上、対処できないケースを突き付けられない限り現行の運用を継続し、ページは必ず標準サイズに戻すのが合理的であるという判断となりました。

この記事のタイトルとURLをコピーする

・関連記事
ロジクールのアプリ「Logi Options+」「Logicool G HUB」のmacOS版で不具合発生、復旧方法はコレ - GIGAZINE

テスラがワイパーの不具合でほぼすべてのサイバートラックをリコール - GIGAZINE

5カ月にわたって解読不能なデータを送り続けたボイジャー1号の不具合の原因が判明 - GIGAZINE

マツダ車で「特定のラジオを受信するとシステムが利用不能になる不具合」が発生、その原因とは? - GIGAZINE

無料でFirefoxのメモリリークを一発であっという間に除去する「Firemin」 - GIGAZINE

in ソフトウェア, Posted by log1c_sh

You can read the machine translated English article A certain app reported an unusual memory….