[インデックス 18622] ファイルの概要
このコミットは、Goランタイムにおけるヒープメモリ破損のバグを修正するものです。具体的には、並行スイープ中にfinc
(ファイナライザキュー)がrunfinq
とqueuefinalizer
によって同時に変更されることで発生する競合状態を解消します。
コミット
commit ea8750175020f162a15c827225328f8ba9e1a118
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Feb 24 20:53:50 2014 +0400
runtime: fix heap memory corruption
With concurrent sweeping finc if modified by runfinq and queuefinalizer concurrently.
Fixes crashes like this one:
http://build.golang.org/log/6ad7b59ef2e93e3c9347eabfb4c4bd66df58fd5a
Fixes #7324.
Update #7396
LGTM=rsc
R=golang-codereviews, minux.ma, rsc
CC=golang-codereviews, khr
https://golang.org/cl/67980043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ea8750175020f162a15c827225328f8ba9e1a118
元コミット内容
runtime: fix heap memory corruption
With concurrent sweeping finc if modified by runfinq and queuefinalizer concurrently.
Fixes crashes like this one:
http://build.golang.org/log/6ad7b59ef2e93e3c9347eabfb4c4bd66df58fd5a
Fixes #7324.
Update #7396
変更の背景
このコミットは、Goランタイムにおけるヒープメモリ破損の問題を解決するために導入されました。具体的には、Goのガベージコレクタ(GC)が並行して動作している際に、ファイナライザに関連するデータ構造であるfinc
が複数のゴルーチンによって同時にアクセス・変更されることによって発生する競合状態が原因でした。
コミットメッセージに記載されているように、http://build.golang.org/log/6ad7b59ef2e93e3c9347eabfb4c4bd66df58fd5a
のようなクラッシュログが報告されており、これはGoプログラムが予期せず終了する深刻な問題を示していました。このクラッシュは、ヒープメモリの整合性が損なわれた結果として発生し、プログラムの信頼性と安定性を著しく低下させます。
Goのガベージコレクションは、プログラムの実行と並行して動作するように設計されており、これによりGCの一時停止時間を最小限に抑え、アプリケーションの応答性を向上させています。しかし、並行処理は常に競合状態のリスクを伴います。このケースでは、ファイナライザの処理に関連するrunfinq
関数とqueuefinalizer
関数が、finc
という共有リソースに対して非同期にアクセスし、適切な同期メカニズムが欠如していたために問題が発生しました。
この問題は、Goのランタイムの安定性にとって非常に重要であり、修正が急務とされていました。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムとガベージコレクションに関する前提知識が必要です。
-
Goのガベージコレクション (GC): GoのGCは、不要になったメモリを自動的に解放する仕組みです。Go 1.x系(このコミットが作成された時期)では、主にマーク&スイープ方式が採用されており、並行(concurrent)および並行(parallel)なフェーズを含んでいます。
- マークフェーズ: GCが到達可能なオブジェクトをマークします。
- スイープフェーズ: マークされていない(到達不可能な)オブジェクトが占有していたメモリを解放し、再利用可能にします。このスイープフェーズは、プログラムの実行と並行して行われることがあります(concurrent sweeping)。
-
ファイナライザ (Finalizers): Goでは、
runtime.SetFinalizer
関数を使って、オブジェクトがガベージコレクションによってメモリから解放される直前に実行される関数(ファイナライザ)を登録できます。これは、ファイルハンドルやネットワーク接続などの外部リソースをクリーンアップするのに役立ちます。- ファイナライザは、GCによってオブジェクトが「死んでいる」と判断された後、実際にメモリが解放される前に実行されます。
- ファイナライザが登録されたオブジェクトは、GCの通常のサイクルとは異なる特別なキューに入れられ、ファイナライザが実行されるまでメモリが保持されます。
-
finc
(Finalizer Queue):finc
は、Goランタイム内部で使用されるデータ構造で、ファイナライザが登録されたオブジェクト(正確には、そのファイナライザ情報)を保持するキューです。GCがオブジェクトを回収する準備ができたとき、そのオブジェクトにファイナライザが設定されていれば、そのファイナライザはfinc
キューに追加されます。 -
runfinq
関数:runfinq
は、finc
キューからファイナライザを取り出し、それらを実行するGoランタイムの内部関数です。これは通常、専用のゴルーチン(ファイナライザゴルーチン)によって実行されます。 -
queuefinalizer
関数:queuefinalizer
は、新しいファイナライザをfinc
キューに追加するGoランタイムの内部関数です。これは、runtime.SetFinalizer
が呼び出された際や、GCがオブジェクトをマークし終えた後に、ファイナライザを持つオブジェクトを処理する際に呼び出されます。 -
競合状態 (Race Condition): 複数のゴルーチン(またはスレッド)が共有リソース(この場合は
finc
)に同時にアクセスし、少なくとも1つがそのリソースを変更する操作を行う場合に発生する問題です。適切な同期メカニズムがないと、操作の順序によって結果が非決定論的になり、データ破損やクラッシュにつながる可能性があります。 -
ミューテックス (Mutex) と
runtime·lock
/runtime·unlock
: 競合状態を防ぐための一般的な同期プリミティブです。ミューテックスは、共有リソースへのアクセスを一度に1つのゴルーチンに制限します。Goランタイム内部では、runtime·lock
とruntime·unlock
という低レベルのロックプリミティブが使用されます。これらは、Goのsync.Mutex
の基盤となるものです。gclock
は、ガベージコレクションに関連する操作を同期するためのグローバルなロックです。
技術的詳細
このバグは、Goランタイムのガベージコレクタが並行スイープを実行している最中に、finc
(ファイナライザキュー)が複数の異なるコンテキストから同時に変更されることによって引き起こされました。具体的には、以下の2つの操作が競合していました。
-
runfinq
関数によるfinc
の読み取りと変更:runfinq
は、ファイナライザを実行するためにfinc
キューからファイナライザ情報を取得します。この処理中、finc
のリスト構造が一時的に変更される可能性があります。 -
queuefinalizer
関数によるfinc
への追加: GCのスイープフェーズ中、またはruntime.SetFinalizer
が呼び出された際に、新しいファイナライザ情報がqueuefinalizer
によってfinc
キューの先頭に追加されることがあります。
問題は、これらの操作が同時に発生した場合に、finc
というリンクリスト構造の整合性が保たれないことにありました。例えば、runfinq
がfinc
の先頭要素を処理している最中に、queuefinalizer
が新しい要素をfinc
の先頭に挿入しようとすると、ポインタの更新が非アトミックに行われるため、リストが破損し、不正なメモリアクセスやヒープメモリ破損につながる可能性がありました。コミットメッセージにあるクラッシュログは、このようなメモリ破損が実際に発生していたことを示しています。
この修正は、finc
へのアクセスをgclock
というグローバルなGCロックで保護することで、この競合状態を解消します。gclock
は、Goランタイムのガベージコレクションに関連する重要なデータ構造へのアクセスを同期するために使用されるミューテックスです。runfinq
関数内でfinc
リストのnext
ポインタを更新する直前と直後にgclock
をロック・アンロックすることで、finc
へのアクセスが排他的になり、複数のゴルーチンによる同時変更が防止されます。
これにより、finc
の整合性が保証され、ヒープメモリ破損やそれに伴うクラッシュが回避されます。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/mgc0.c
ファイル内のrunfinq
関数にあります。
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -2551,7 +2551,7 @@ runfinq(void)\n if(framecap < framesz) {\n runtime·free(frame);\n // The frame does not contain pointers interesting for GC,\n- // all not yet finalized objects are stored in finc.\n+ // all not yet finalized objects are stored in finq.\n // If we do not mark it as FlagNoScan,\n // the last finalized object is not collected.\n frame = runtime·mallocgc(framesz, 0, FlagNoScan|FlagNoInvokeGC);\n@@ -2580,8 +2580,10 @@ runfinq(void)\n f->ot = nil;\n }\n fb->cnt = 0;\n+\t\truntime·lock(&gclock);\n \t\tfb->next = finc;\n \t\tfinc = fb;\n+\t\truntime·unlock(&gclock);\n }\n runtime·gc(1);\t// trigger another gc to clean up the finalized objects, if possible\n }\n```
## コアとなるコードの解説
このコミットの主要な変更は、`runfinq`関数内の以下の3行です。
```c
runtime·lock(&gclock);
fb->next = finc;
finc = fb;
runtime·unlock(&gclock);
-
runtime·lock(&gclock);
: これは、gclock
というグローバルなミューテックスをロックする操作です。このロックが取得されると、他のゴルーチンはgclock
が解放されるまで、gclock
によって保護されている共有リソース(この場合はfinc
)にアクセスできません。これにより、finc
リストの変更が排他的に行われることが保証されます。 -
fb->next = finc;
: これは、現在処理中のファイナライザブロックfb
のnext
ポインタを、現在のfinc
(ファイナライザキューの先頭)に設定しています。これは、fb
をfinc
リストの新しい先頭として挿入する準備です。 -
finc = fb;
: これは、finc
ポインタ自体を、新しく追加されたファイナライザブロックfb
を指すように更新しています。これにより、fb
がfinc
リストの新しい先頭となります。 -
runtime·unlock(&gclock);
: これは、gclock
ミューテックスを解放する操作です。これにより、他のゴルーチンがgclock
によって保護されている共有リソースにアクセスできるようになります。
これらの変更により、finc
リストの先頭への挿入操作(fb->next = finc; finc = fb;
)がアトミックに実行されるようになります。つまり、この2つのポインタ更新操作の間に、他のゴルーチンがfinc
にアクセスしてリストの整合性を破壊する可能性がなくなります。
また、コメントの修正も行われています。
- // all not yet finalized objects are stored in finc.
+ // all not yet finalized objects are stored in finq.
これは、finc
がfinq
(finalizer queue)の略であることを明確にするための修正であり、コードの動作には影響しませんが、可読性を向上させます。
関連リンク
- Go Issue #7324:
runtime: heap corruption with concurrent sweeping
- このコミットが修正した具体的なバグ報告。 - Go Issue #7396:
runtime: finalizer queue race
- 関連する別のバグ報告。このコミットによって更新された可能性があります。 - Gerrit Code Review for this commit:
https://golang.org/cl/67980043
- https://go-review.googlesource.com/c/go/+/67980043 (GerritのURLは変更されている可能性がありますが、元のコミットメッセージに記載されているものです)
参考にした情報源リンク
- Goのガベージコレクションに関する公式ドキュメントやブログ記事 (当時のGo 1.xのGCの仕組みについて)
- Goランタイムのソースコード (特に
src/runtime/mgc.go
,src/runtime/mgc0.c
,src/runtime/proc.go
など、GCとスケジューラに関連するファイル) - Goのミューテックスと同期プリミティブに関するドキュメント
- 競合状態と同期メカニズムに関する一般的なコンピュータサイエンスの知識
- GoのIssueトラッカーとコミットログ
- Dmitriy Vyukov氏のGoランタイムに関する他の貢献や解説 (もしあれば)
- Goのファイナライザに関する解説記事