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

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

コミット

commit 2f5825d4273d910b0d3c9ee82336cf041e8f02d3
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue May 28 19:17:47 2013 +0400

    runtime: fix heap corruption during GC
    The 'n' variable is used during rescan initiation in GC_END case,
    but it's overwritten with chan capacity in GC_CHAN case.
    As the result rescan is done with the wrong object size.
    Fixes #5554.

    R=golang-dev, khr
    CC=golang-dev
    https://golang.org/cl/9831043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/2f5825d4273d910b0d3c9ee82336cf041e8f02d3

元コミット内容

このコミットは、Goランタイムにおけるガベージコレクション(GC)中のヒープ破損バグを修正するものです。具体的には、GCのスキャン処理において、nという変数がGC終了時(GC_ENDケース)のリスキャン開始時に使用されるべき値を持つにもかかわらず、チャネル(GC_CHANケース)の容量で上書きされてしまう問題がありました。この結果、オブジェクトのサイズが誤って認識され、リスキャンが不正確に行われることでヒープが破損する可能性がありました。この修正は、Goイシュー5554を解決します。

変更の背景

Go言語のランタイム、特にガベージコレクタは、メモリ管理の根幹をなす非常に重要なコンポーネントです。ガベージコレクタは、プログラムが動的に確保したメモリのうち、もはや到達不可能になったオブジェクトを自動的に解放し、メモリリークを防ぎ、開発者が手動でメモリを管理する負担を軽減します。

このコミットが修正する問題は、ガベージコレクションの「リスキャン」フェーズにおけるヒープ破損です。リスキャンとは、GCサイクル中に、既にスキャンされたオブジェクトが指す可能性のある新しいポインタを再スキャンするプロセスです。これは、並行GCにおいて、GCが実行されている間にプログラムが新しいオブジェクトを作成したり、既存のオブジェクトへのポインタを変更したりする可能性があるため、非常に重要です。

コミットメッセージによると、nという変数が、GCの異なるフェーズ(GC_ENDGC_CHAN)で異なる意味を持つように再利用されていました。GC_ENDケースでは、このnがリスキャンを開始するためのオブジェクトサイズとして使われるべきでしたが、GC_CHANケースでチャネルの容量を格納するために上書きされていました。この変数の誤った再利用が、リスキャン時のオブジェクトサイズの誤認識を引き起こし、結果としてヒープ破損という深刻な問題につながっていました。ヒープ破損は、プログラムのクラッシュ、予期せぬ動作、セキュリティ脆弱性など、様々な問題を引き起こす可能性があります。

このバグは、Goイシュー5554として報告されており、その解決がこのコミットの目的です。

前提知識の解説

ガベージコレクション (GC)

ガベージコレクションは、プログラムが使用しなくなったメモリ領域を自動的に解放するプロセスです。Go言語のGCは、並行マーク&スイープ方式を採用しており、プログラムの実行と並行してGCが動作することで、アプリケーションの一時停止時間(ストップ・ザ・ワールド)を最小限に抑えることを目指しています。

  • マークフェーズ: GCのルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトをマークします。
  • スイープフェーズ: マークされなかった(到達不可能な)オブジェクトが占めるメモリ領域を解放します。
  • リスキャンフェーズ: 並行GCにおいて、マークフェーズ中にアプリケーションが変更したポインタを再確認し、マーク漏れがないかを確認します。

Goランタイム

Goランタイムは、Goプログラムの実行を管理する低レベルのシステムです。これには、スケジューラ(ゴルーチンの管理)、メモリ割り当て、ガベージコレクタ、チャネルの実装などが含まれます。src/pkg/runtimeディレクトリには、これらのコアコンポーネントのC言語(またはGoのサブセットであるPlan 9 C)とアセンブリ言語による実装が含まれています。

チャネル (Channels)

Goのチャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは、Goの並行処理モデルの重要な要素であり、同期と通信を安全に行うための手段を提供します。チャネルは内部的にバッファを持つことができ、そのバッファの容量はチャネル作成時に指定されます。

ヒープ (Heap)

ヒープは、プログラムが実行時に動的にメモリを割り当てるために使用されるメモリ領域です。Goプログラムでは、newmakeなどの関数によって作成されたオブジェクトはヒープに割り当てられ、ガベージコレクタによって管理されます。

uintptr

uintptrは、ポインタを保持するのに十分な大きさの符号なし整数型です。Goのランタイムコードでは、メモリアドレスやサイズを扱う際によく使用されます。

技術的詳細

この修正は、src/pkg/runtime/mgc0.cファイル内のscanblock関数に焦点を当てています。scanblock関数は、ガベージコレクタがオブジェクトをスキャンし、到達可能なオブジェクトをマークする主要な関数の一つです。

問題の核心は、scanblock関数内でnという変数が、チャネルの容量を格納するためにも使用されていた点にあります。しかし、このnは、GCの他の部分、特にGC_ENDケースでのリスキャン開始時に、オブジェクトのサイズを示すために再利用されていました。

元のコードでは、チャネルのバッファサイズを取得するためにn = ((uintgo*)chan)[1];という行がありました。このnが、チャネルの容量として使用された後、GCの他の部分でオブジェクトサイズとして誤って解釈される可能性がありました。

修正では、チャネルの容量を格納するためにchancapという新しい変数を導入しました。これにより、n変数がチャネルの容量で上書きされることがなくなり、nが本来の目的(GC_ENDケースでのリスキャン開始時のオブジェクトサイズ)で正しく使用されるようになりました。

具体的には、以下の変更が行われました。

  1. scanblock関数の変数宣言にchancapが追加されました。
    -	uintptr *map_ret, mapkey_size, mapval_size, mapkey_ti, mapval_ti, *chan_ret;
    +	uintptr *map_ret, mapkey_size, mapval_size, mapkey_ti, mapval_ti, *chan_ret, chancap;
    
  2. チャネルの容量を取得する際に、nの代わりにchancapが使用されるようになりました。
    -			n = ((uintgo*)chan)[1];
    -			if(n > 0) {
    +			chancap = ((uintgo*)chan)[1];
    +			if(chancap > 0) {
    
  3. チャネルのバッファサイズを計算する際にも、nの代わりにchancapが使用されるようになりました。
    -				*objbufpos++ = (Obj){(byte*)chan+runtime·Hchansize, n*chantype->elem->size,
    +				*objbufpos++ = (Obj){(byte*)chan+runtime·Hchansize, chancap*chantype->elem->size,
    

この変更により、n変数の意味が混同されることがなくなり、GCのリスキャン処理が正しいオブジェクトサイズで行われるようになり、ヒープ破損が防止されます。

また、src/pkg/runtime/gc_test.goには、この修正を検証するための新しいテストケースTestGcRescanが追加されました。このテストは、チャネル、構造体、ポインタが複雑に絡み合ったデータ構造を作成し、GCを繰り返し実行した後に、ヒープが破損していないか(具体的には、ポインタが指す値が期待通りであるか)を検証します。これにより、将来的に同様の回帰バグが発生するのを防ぐための安全網が提供されます。

コアとなるコードの変更箇所

src/pkg/runtime/mgc0.c

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -623,7 +623,7 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
 	byte *b, *arena_start, *arena_used;
 	uintptr n, i, end_b, elemsize, size, ti, objti, count, type;
 	uintptr *pc, precise_type, nominal_size;
-	uintptr *map_ret, mapkey_size, mapval_size, mapkey_ti, mapval_ti, *chan_ret;
+	uintptr *map_ret, mapkey_size, mapval_size, mapkey_ti, mapval_ti, *chan_ret, chancap;
 	void *obj;
 	Type *t;
 	Slice *sliceptr;
@@ -1062,13 +1062,13 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
 			if(!(chantype->elem->kind & KindNoPointers)) {
 				// Channel's buffer follows Hchan immediately in memory.
 				// Size of buffer (cap(c)) is second int in the chan struct.
-				n = ((uintgo*)chan)[1];
-				if(n > 0) {
+				chancap = ((uintgo*)chan)[1];
+				if(chancap > 0) {
 					// TODO(atom): split into two chunks so that only the
 					// in-use part of the circular buffer is scanned.
 					// (Channel routines zero the unused part, so the current
 					// code does not lead to leaks, it's just a little inefficient.)
-					*objbufpos++ = (Obj){(byte*)chan+runtime·Hchansize, n*chantype->elem->size,
+					*objbufpos++ = (Obj){(byte*)chan+runtime·Hchansize, chancap*chantype->elem->size,
 						(uintptr)chantype->elem->gc | PRECISE | LOOP};
 					if(objbufpos == objbuf_end)
 						flushobjbuf(objbuf, &objbufpos, &wp, &wbuf, &nobj);

src/pkg/runtime/gc_test.go

--- a/src/pkg/runtime/gc_test.go
+++ b/src/pkg/runtime/gc_test.go
@@ -121,3 +121,31 @@ func TestGcArraySlice(t *testing.T) {
 		}
 	}
 }
+
+func TestGcRescan(t *testing.T) {
+	type X struct {
+		c     chan error
+		nextx *X
+	}
+	type Y struct {
+		X
+		nexty *Y
+		p     *int
+	}
+	var head *Y
+	for i := 0; i < 10; i++ {
+		p := &Y{}
+		p.c = make(chan error)
+		p.nextx = &head.X
+		p.nexty = head
+		p.p = new(int)
+		*p.p = 42
+		head = p
+		runtime.GC()
+	}
+	for p := head; p != nil; p = p.nexty {
+		if *p.p != 42 {
+			t.Fatal("corrupted heap")
+		}
+	}
+}

コアとなるコードの解説

src/pkg/runtime/mgc0.cの変更点

このファイルはGoランタイムのガベージコレクタのC言語部分を実装しています。scanblock関数は、GCがオブジェクトをスキャンする際の中心的なロジックを含んでいます。

  • 変数chancapの導入: uintptr *map_ret, ..., *chan_ret, chancap; この行は、scanblock関数内でchancapという新しいuintptr型の変数を宣言しています。これは、チャネルの容量を格納するためだけに特化された変数であり、既存のn変数との混同を避けるための重要な変更です。

  • チャネル容量の取得と使用:

    chancap = ((uintgo*)chan)[1];
    if(chancap > 0) {
        // ...
        *objbufpos++ = (Obj){(byte*)chan+runtime·Hchansize, chancap*chantype->elem->size,
            (uintptr)chantype->elem->gc | PRECISE | LOOP};
        // ...
    }
    

    以前はnがチャネルの容量を保持していましたが、この修正によりchancapがその役割を担います。((uintgo*)chan)[1]は、チャネル構造体の2番目のフィールド(インデックス1)がチャネルの容量を格納していることを示しています。この容量が0より大きい場合、チャネルのバッファ部分もGCスキャンの対象としてobjbufposに追加されます。ここで、チャネルのバッファサイズを計算する際にもchancapが使用され、chancap * chantype->elem->sizeによってバッファの実際のバイトサイズが正確に計算されます。これにより、GCがチャネルのバッファを正しいサイズでスキャンできるようになり、ヒープ破損の原因となっていたオブジェクトサイズの誤認識が解消されます。

src/pkg/runtime/gc_test.goの変更点

このファイルはGoランタイムのガベージコレクタのテストケースを含んでいます。

  • TestGcRescan関数の追加: この新しいテスト関数は、GCリスキャン中のヒープ破損を検出するために設計されています。
    • 構造体XYの定義: Xはチャネルと自己参照ポインタを持つ構造体、YXを埋め込み、さらに別の自己参照ポインタと整数へのポインタを持つ構造体です。これらの構造体は、GCが複雑なポインタグラフを正しく処理できるかを検証するために意図的に複雑に設計されています。
    • ループによるオブジェクトの作成とGCの実行: for i := 0; i < 10; i++ループ内で、Y型のオブジェクトが繰り返し作成され、headポインタを通じて連結リストのような構造を形成します。各Yオブジェクトは、新しいチャネルと整数へのポインタを持ち、その整数には値42が設定されます。オブジェクトが作成されるたびにruntime.GC()が呼び出され、GCが頻繁に実行される状況をシミュレートします。これにより、GCのリスキャンフェーズが繰り返しトリガーされ、バグが顕在化しやすい状況を作り出します。
    • ヒープの検証: オブジェクト作成ループの後、for p := head; p != nil; p = p.nextyループで、作成された連結リストを走査します。各Yオブジェクトのpフィールド(整数へのポインタ)が指す値が42であるかを検証します。もしヒープが破損していれば、この値が42以外になっている可能性があり、その場合はt.Fatal("corrupted heap")が呼び出されてテストが失敗します。この検証により、GCが正しく動作し、ヒープの整合性が保たれていることが確認されます。

関連リンク

参考にした情報源リンク

  • Go言語のガベージコレクションに関する公式ドキュメントやブログ記事 (GoのGCの仕組みを理解するために)
  • Goランタイムのソースコード (特にsrc/pkg/runtimeディレクトリ内のGC関連ファイル)
  • Go言語のチャネルに関するドキュメント (チャネルの内部構造とGCとの関連性を理解するために)
  • Go言語のテストフレームワークに関するドキュメント (テストコードの理解のために)
  • ヒープ破損に関する一般的な情報 (ヒープ破損がなぜ問題なのかを理解するために)
  • Go Issue 5554の議論 (バグの詳細と修正の背景を理解するために)```markdown

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

コミット

commit 2f5825d4273d910b0d3c9ee82336cf041e8f02d3
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue May 28 19:17:47 2013 +0400

    runtime: fix heap corruption during GC
    The 'n' variable is used during rescan initiation in GC_END case,
    but it's overwritten with chan capacity in GC_CHAN case.
    As the result rescan is done with the wrong object size.
    Fixes #5554.

    R=golang-dev, khr
    CC=golang-dev
    https://golang.org/cl/9831043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/2f5825d4273d910b0d3c9ee82336cf041e8f02d3

元コミット内容

このコミットは、Goランタイムにおけるガベージコレクション(GC)中のヒープ破損バグを修正するものです。具体的には、GCのスキャン処理において、nという変数がGC終了時(GC_ENDケース)のリスキャン開始時に使用されるべき値を持つにもかかわらず、チャネル(GC_CHANケース)の容量で上書きされてしまう問題がありました。この結果、オブジェクトのサイズが誤って認識され、リスキャンが不正確に行われることでヒープが破損する可能性がありました。この修正は、Goイシュー5554を解決します。

変更の背景

Go言語のランタイム、特にガベージコレクタは、メモリ管理の根幹をなす非常に重要なコンポーネントです。ガベージコレクタは、プログラムが動的に確保したメモリのうち、もはや到達不可能になったオブジェクトを自動的に解放し、メモリリークを防ぎ、開発者が手動でメモリを管理する負担を軽減します。

このコミットが修正する問題は、ガベージコレクションの「リスキャン」フェーズにおけるヒープ破損です。リスキャンとは、GCサイクル中に、既にスキャンされたオブジェクトが指す可能性のある新しいポインタを再スキャンするプロセスです。これは、並行GCにおいて、GCが実行されている間にプログラムが新しいオブジェクトを作成したり、既存のオブジェクトへのポインタを変更したりする可能性があるため、非常に重要です。

コミットメッセージによると、nという変数が、GCの異なるフェーズ(GC_ENDGC_CHAN)で異なる意味を持つように再利用されていました。GC_ENDケースでは、このnがリスキャンを開始するためのオブジェクトサイズとして使われるべきでしたが、GC_CHANケースでチャネルの容量を格納するために上書きされていました。この変数の誤った再利用が、リスキャン時のオブジェクトサイズの誤認識を引き起こし、結果としてヒープ破損という深刻な問題につながっていました。ヒープ破損は、プログラムのクラッシュ、予期せぬ動作、セキュリティ脆弱性など、様々な問題を引き起こす可能性があります。

このバグは、Goイシュー5554として報告されており、その解決がこのコミットの目的です。

前提知識の解説

ガベージコレクション (GC)

ガベージコレクションは、プログラムが使用しなくなったメモリ領域を自動的に解放するプロセスです。Go言語のGCは、並行マーク&スイープ方式を採用しており、プログラムの実行と並行してGCが動作することで、アプリケーションの一時停止時間(ストップ・ザ・ワールド)を最小限に抑えることを目指しています。

  • マークフェーズ: GCのルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトをマークします。
  • スイープフェーズ: マークされなかった(到達不可能な)オブジェクトが占めるメモリ領域を解放します。
  • リスキャンフェーズ: 並行GCにおいて、マークフェーズ中にアプリケーションが変更したポインタを再確認し、マーク漏れがないかを確認します。

Goランタイム

Goランタイムは、Goプログラムの実行を管理する低レベルのシステムです。これには、スケジューラ(ゴルーチンの管理)、メモリ割り当て、ガベージコレクタ、チャネルの実装などが含まれます。src/pkg/runtimeディレクトリには、これらのコアコンポーネントのC言語(またはGoのサブセットであるPlan 9 C)とアセンブリ言語による実装が含まれています。

チャネル (Channels)

Goのチャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは、Goの並行処理モデルの重要な要素であり、同期と通信を安全に行うための手段を提供します。チャネルは内部的にバッファを持つことができ、そのバッファの容量はチャネル作成時に指定されます。

ヒープ (Heap)

ヒープは、プログラムが実行時に動的にメモリを割り当てるために使用されるメモリ領域です。Goプログラムでは、newmakeなどの関数によって作成されたオブジェクトはヒープに割り当てられ、ガベージコレクタによって管理されます。

uintptr

uintptrは、ポインタを保持するのに十分な大きさの符号なし整数型です。Goのランタイムコードでは、メモリアドレスやサイズを扱う際によく使用されます。

技術的詳細

この修正は、src/pkg/runtime/mgc0.cファイル内のscanblock関数に焦点を当てています。scanblock関数は、ガベージコレクタがオブジェクトをスキャンし、到達可能なオブジェクトをマークする主要な関数の一つです。

問題の核心は、scanblock関数内でnという変数が、チャネルの容量を格納するためにも使用されていた点にあります。しかし、このnは、GCの他の部分、特にGC_ENDケースでのリスキャン開始時に、オブジェクトのサイズを示すために再利用されていました。

元のコードでは、チャネルのバッファサイズを取得するためにn = ((uintgo*)chan)[1];という行がありました。このnが、チャネルの容量として使用された後、GCの他の部分でオブジェクトサイズとして誤って解釈される可能性がありました。

修正では、チャネルの容量を格納するためにchancapという新しい変数を導入しました。これにより、n変数がチャネルの容量で上書きされることがなくなり、nが本来の目的(GC_ENDケースでのリスキャン開始時のオブジェクトサイズ)で正しく使用されるようになりました。

具体的には、以下の変更が行われました。

  1. scanblock関数の変数宣言にchancapが追加されました。
    -	uintptr *map_ret, mapkey_size, mapval_size, mapkey_ti, mapval_ti, *chan_ret;
    +	uintptr *map_ret, mapkey_size, mapval_size, mapkey_ti, mapval_ti, *chan_ret, chancap;
    
  2. チャネルの容量を取得する際に、nの代わりにchancapが使用されるようになりました。
    -			n = ((uintgo*)chan)[1];
    -			if(n > 0) {
    +			chancap = ((uintgo*)chan)[1];
    +			if(chancap > 0) {
    
  3. チャネルのバッファサイズを計算する際にも、nの代わりにchancapが使用されるようになりました。
    -				*objbufpos++ = (Obj){(byte*)chan+runtime·Hchansize, n*chantype->elem->size,
    +				*objbufpos++ = (Obj){(byte*)chan+runtime·Hchansize, chancap*chantype->elem->size,
    

この変更により、n変数の意味が混同されることがなくなり、GCのリスキャン処理が正しいオブジェクトサイズで行われるようになり、ヒープ破損が防止されます。

また、src/pkg/runtime/gc_test.goには、この修正を検証するための新しいテストケースTestGcRescanが追加されました。このテストは、チャネル、構造体、ポインタが複雑に絡み合ったデータ構造を作成し、GCを繰り返し実行した後に、ヒープが破損していないか(具体的には、ポインタが指す値が期待通りであるか)を検証します。これにより、将来的に同様の回帰バグが発生するのを防ぐための安全網が提供されます。

コアとなるコードの変更箇所

src/pkg/runtime/mgc0.c

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -623,7 +623,7 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
 	byte *b, *arena_start, *arena_used;
 	uintptr n, i, end_b, elemsize, size, ti, objti, count, type;
 	uintptr *pc, precise_type, nominal_size;
-	uintptr *map_ret, mapkey_size, mapval_size, mapkey_ti, mapval_ti, *chan_ret;
+	uintptr *map_ret, mapkey_size, mapval_size, mapkey_ti, mapval_ti, *chan_ret, chancap;
 	void *obj;
 	Type *t;
 	Slice *sliceptr;
@@ -1062,13 +1062,13 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
 			if(!(chantype->elem->kind & KindNoPointers)) {
 				// Channel's buffer follows Hchan immediately in memory.
 				// Size of buffer (cap(c)) is second int in the chan struct.
-				n = ((uintgo*)chan)[1];
-				if(n > 0) {
+				chancap = ((uintgo*)chan)[1];
+				if(chancap > 0) {
 					// TODO(atom): split into two chunks so that only the
 					// in-use part of the circular buffer is scanned.
 					// (Channel routines zero the unused part, so the current
 					// code does not lead to leaks, it's just a little inefficient.)
-					*objbufpos++ = (Obj){(byte*)chan+runtime·Hchansize, n*chantype->elem->size,
+					*objbufpos++ = (Obj){(byte*)chan+runtime·Hchansize, chancap*chantype->elem->size,
 						(uintptr)chantype->elem->gc | PRECISE | LOOP};
 					if(objbufpos == objbuf_end)
 						flushobjbuf(objbuf, &objbufpos, &wp, &wbuf, &nobj);

src/pkg/runtime/gc_test.go

--- a/src/pkg/runtime/gc_test.go
+++ b/src/pkg/runtime/gc_test.go
@@ -121,3 +121,31 @@ func TestGcArraySlice(t *testing.T) {
 		}
 	}
 }
+
+func TestGcRescan(t *testing.T) {
+	type X struct {
+		c     chan error
+		nextx *X
+	}
+	type Y struct {
+		X
+		nexty *Y
+		p     *int
+	}
+	var head *Y
+	for i := 0; i < 10; i++ {
+		p := &Y{}
+		p.c = make(chan error)
+		p.nextx = &head.X
+		p.nexty = head
+		p.p = new(int)
+		*p.p = 42
+		head = p
+		runtime.GC()
+	}
+	for p := head; p != nil; p = p.nexty {
+		if *p.p != 42 {
+			t.Fatal("corrupted heap")
+		}
+	}
+}

コアとなるコードの解説

src/pkg/runtime/mgc0.cの変更点

このファイルはGoランタイムのガベージコレクタのC言語部分を実装しています。scanblock関数は、GCがオブジェクトをスキャンする際の中心的なロジックを含んでいます。

  • 変数chancapの導入: uintptr *map_ret, ..., *chan_ret, chancap; この行は、scanblock関数内でchancapという新しいuintptr型の変数を宣言しています。これは、チャネルの容量を格納するためだけに特化された変数であり、既存のn変数との混同を避けるための重要な変更です。

  • チャネル容量の取得と使用:

    chancap = ((uintgo*)chan)[1];
    if(chancap > 0) {
        // ...
        *objbufpos++ = (Obj){(byte*)chan+runtime·Hchansize, chancap*chantype->elem->size,
            (uintptr)chantype->elem->gc | PRECISE | LOOP};
        // ...
    }
    

    以前はnがチャネルの容量を保持していましたが、この修正によりchancapがその役割を担います。((uintgo*)chan)[1]は、チャネル構造体の2番目のフィールド(インデックス1)がチャネルの容量を格納していることを示しています。この容量が0より大きい場合、チャネルのバッファ部分もGCスキャンの対象としてobjbufposに追加されます。ここで、チャネルのバッファサイズを計算する際にもchancapが使用され、chancap * chantype->elem->sizeによってバッファの実際のバイトサイズが正確に計算されます。これにより、GCがチャネルのバッファを正しいサイズでスキャンできるようになり、ヒープ破損の原因となっていたオブジェクトサイズの誤認識が解消されます。

src/pkg/runtime/gc_test.goの変更点

このファイルはGoランタイムのガベージコレクタのテストケースを含んでいます。

  • TestGcRescan関数の追加: この新しいテスト関数は、GCリスキャン中のヒープ破損を検出するために設計されています。
    • 構造体XYの定義: Xはチャネルと自己参照ポインタを持つ構造体、YXを埋め込み、さらに別の自己参照ポインタと整数へのポインタを持つ構造体です。これらの構造体は、GCが複雑なポインタグラフを正しく処理できるかを検証するために意図的に複雑に設計されています。
    • ループによるオブジェクトの作成とGCの実行: for i := 0; i < 10; i++ループ内で、Y型のオブジェクトが繰り返し作成され、headポインタを通じて連結リストのような構造を形成します。各Yオブジェクトは、新しいチャネルと整数へのポインタを持ち、その整数には値42が設定されます。オブジェクトが作成されるたびにruntime.GC()が呼び出され、GCが頻繁に実行される状況をシミュレートします。これにより、GCのリスキャンフェーズが繰り返しトリガーされ、バグが顕在化しやすい状況を作り出します。
    • ヒープの検証: オブジェクト作成ループの後、for p := head; p != nil; p = p.nextyループで、作成された連結リストを走査します。各Yオブジェクトのpフィールド(整数へのポインタ)が指す値が42であるかを検証します。もしヒープが破損していれば、この値が42以外になっている可能性があり、その場合はt.Fatal("corrupted heap")が呼び出されてテストが失敗します。この検証により、GCが正しく動作し、ヒープの整合性が保たれていることが確認されます。

関連リンク

参考にした情報源リンク

  • Go言語のガベージコレクションに関する公式ドキュメントやブログ記事 (GoのGCの仕組みを理解するために)
  • Goランタイムのソースコード (特にsrc/pkg/runtimeディレクトリ内のGC関連ファイル)
  • Go言語のチャネルに関するドキュメント (チャネルの内部構造とGCとの関連性を理解するために)
  • Go言語のテストフレームワークに関するドキュメント (テストコードの理解のために)
  • ヒープ破損に関する一般的な情報 (ヒープ破損がなぜ問題なのかを理解するために)
  • Go Issue 5554の議論 (バグの詳細と修正の背景を理解するために)