Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 18622] ファイルの概要

このコミットは、Goランタイムにおけるヒープメモリ破損のバグを修正するものです。具体的には、並行スイープ中にfinc(ファイナライザキュー)がrunfinqqueuefinalizerによって同時に変更されることで発生する競合状態を解消します。

コミット

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ランタイムとガベージコレクションに関する前提知識が必要です。

  1. Goのガベージコレクション (GC): GoのGCは、不要になったメモリを自動的に解放する仕組みです。Go 1.x系(このコミットが作成された時期)では、主にマーク&スイープ方式が採用されており、並行(concurrent)および並行(parallel)なフェーズを含んでいます。

    • マークフェーズ: GCが到達可能なオブジェクトをマークします。
    • スイープフェーズ: マークされていない(到達不可能な)オブジェクトが占有していたメモリを解放し、再利用可能にします。このスイープフェーズは、プログラムの実行と並行して行われることがあります(concurrent sweeping)。
  2. ファイナライザ (Finalizers): Goでは、runtime.SetFinalizer関数を使って、オブジェクトがガベージコレクションによってメモリから解放される直前に実行される関数(ファイナライザ)を登録できます。これは、ファイルハンドルやネットワーク接続などの外部リソースをクリーンアップするのに役立ちます。

    • ファイナライザは、GCによってオブジェクトが「死んでいる」と判断された後、実際にメモリが解放される前に実行されます。
    • ファイナライザが登録されたオブジェクトは、GCの通常のサイクルとは異なる特別なキューに入れられ、ファイナライザが実行されるまでメモリが保持されます。
  3. finc (Finalizer Queue): fincは、Goランタイム内部で使用されるデータ構造で、ファイナライザが登録されたオブジェクト(正確には、そのファイナライザ情報)を保持するキューです。GCがオブジェクトを回収する準備ができたとき、そのオブジェクトにファイナライザが設定されていれば、そのファイナライザはfincキューに追加されます。

  4. runfinq関数: runfinqは、fincキューからファイナライザを取り出し、それらを実行するGoランタイムの内部関数です。これは通常、専用のゴルーチン(ファイナライザゴルーチン)によって実行されます。

  5. queuefinalizer関数: queuefinalizerは、新しいファイナライザをfincキューに追加するGoランタイムの内部関数です。これは、runtime.SetFinalizerが呼び出された際や、GCがオブジェクトをマークし終えた後に、ファイナライザを持つオブジェクトを処理する際に呼び出されます。

  6. 競合状態 (Race Condition): 複数のゴルーチン(またはスレッド)が共有リソース(この場合はfinc)に同時にアクセスし、少なくとも1つがそのリソースを変更する操作を行う場合に発生する問題です。適切な同期メカニズムがないと、操作の順序によって結果が非決定論的になり、データ破損やクラッシュにつながる可能性があります。

  7. ミューテックス (Mutex) と runtime·lock/runtime·unlock: 競合状態を防ぐための一般的な同期プリミティブです。ミューテックスは、共有リソースへのアクセスを一度に1つのゴルーチンに制限します。Goランタイム内部では、runtime·lockruntime·unlockという低レベルのロックプリミティブが使用されます。これらは、Goのsync.Mutexの基盤となるものです。gclockは、ガベージコレクションに関連する操作を同期するためのグローバルなロックです。

技術的詳細

このバグは、Goランタイムのガベージコレクタが並行スイープを実行している最中に、finc(ファイナライザキュー)が複数の異なるコンテキストから同時に変更されることによって引き起こされました。具体的には、以下の2つの操作が競合していました。

  1. runfinq関数によるfincの読み取りと変更: runfinqは、ファイナライザを実行するためにfincキューからファイナライザ情報を取得します。この処理中、fincのリスト構造が一時的に変更される可能性があります。

  2. queuefinalizer関数によるfincへの追加: GCのスイープフェーズ中、またはruntime.SetFinalizerが呼び出された際に、新しいファイナライザ情報がqueuefinalizerによってfincキューの先頭に追加されることがあります。

問題は、これらの操作が同時に発生した場合に、fincというリンクリスト構造の整合性が保たれないことにありました。例えば、runfinqfincの先頭要素を処理している最中に、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;: これは、現在処理中のファイナライザブロックfbnextポインタを、現在のfinc(ファイナライザキューの先頭)に設定しています。これは、fbfincリストの新しい先頭として挿入する準備です。

  • finc = fb;: これは、fincポインタ自体を、新しく追加されたファイナライザブロックfbを指すように更新しています。これにより、fbfincリストの新しい先頭となります。

  • 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. これは、fincfinq(finalizer queue)の略であることを明確にするための修正であり、コードの動作には影響しませんが、可読性を向上させます。

関連リンク

参考にした情報源リンク

  • 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のファイナライザに関する解説記事