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

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

このコミットは、Go言語のランタイムにおけるガベージコレクション(GC)のスライススキャンに関するバグ修正を扱っています。具体的には、スライスが構造体に埋め込まれた配列を指している場合に、GCが誤って構造体全体をスライスバッファとしてスキャンしてしまう問題を解決します。

コミット

commit c6293d2106515b1150b4765fa61b12cea76442ae
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed May 15 23:50:32 2013 +0400

    runtime: fix GC scanning of slices
    If a slice points to an array embedded in a struct,
    the whole struct can be incorrectly scanned as the slice buffer.
    Fixes #5443.
    
    R=cshapiro, iant, r, cshapiro, minux.ma
    CC=bradfitz, gobot, golang-dev
    https://golang.org/cl/9372044

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

https://github.com/golang/go/commit/c6293d2106515b1150b4765fa61b12cea76442ae

元コミット内容

runtime: fix GC scanning of slices
If a slice points to an array embedded in a struct,
the whole struct can be incorrectly scanned as the slice buffer.
Fixes #5443.

変更の背景

このコミットは、Go言語のガベージコレクタがスライスをスキャンする際の特定のコーナーケースにおけるバグを修正するために行われました。問題は、スライスが構造体(struct)内に埋め込まれた配列を指している場合に発生しました。このような状況で、ガベージコレクタがスライスの指すメモリ領域を正確に特定できず、誤ってスライスが参照している配列を含む構造体全体をスライスのバッファとしてスキャンしてしまう可能性がありました。

この誤ったスキャンは、ガベージコレクタが不要なメモリを解放する際に、まだ参照されているオブジェクトの一部を誤って解放してしまう、あるいは逆に、解放すべきメモリを解放しないといった、メモリ破損やメモリリークに繋がる深刻な問題を引き起こす可能性があります。特に、GCがポインタを追跡し、到達可能なオブジェクトをマークするプロセスにおいて、誤った領域をスキャンすることは、プログラムの安定性と正確性に直接影響します。

この問題は、Go issue #5443として報告されており、このコミットはその報告された問題を解決するためのものです。ガベージコレクタの正確性は、Goプログラムの安全性とパフォーマンスの基盤であるため、このようなバグの修正は非常に重要です。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびガベージコレクションに関する前提知識が必要です。

1. Go言語のガベージコレクション (GC)

Go言語は、自動メモリ管理のためにトレース型ガベージコレクタを採用しています。これは、プログラムが動的に割り当てたメモリのうち、もはや到達不可能(参照されていない)なオブジェクトを自動的に識別し、解放する仕組みです。GoのGCは、並行(concurrent)かつ低遅延(low-latency)であることを目指して設計されており、プログラムの実行と並行して動作します。

GCの基本的なプロセスは以下の通りです。

  • マークフェーズ (Mark Phase): プログラムのルート(グローバル変数、スタック上の変数など)から到達可能なすべてのオブジェクトをマークします。
  • スキャンフェーズ (Scan Phase): マークされたオブジェクト内部のポインタをたどり、さらに到達可能なオブジェクトをマークします。このプロセスを再帰的に行い、すべての到達可能なオブジェクトを特定します。
  • スイープフェーズ (Sweep Phase): マークされなかった(到達不可能な)オブジェクトが占めるメモリ領域を解放し、再利用可能にします。

このコミットが修正する問題は、特に「スキャンフェーズ」におけるポインタの追跡とメモリ領域の識別に関するものです。

2. Go言語のスライス (Slice)

Goのスライスは、配列(Array)の上に構築された、より柔軟なデータ構造です。スライスは、以下の3つの要素から構成されます。

  • ポインタ (Pointer): スライスが参照する基底配列の先頭要素へのポインタ。
  • 長さ (Length): スライスに含まれる要素の数。
  • 容量 (Capacity): スライスの基底配列の先頭から、基底配列の末尾までの要素の数。

スライスは、基底配列の一部または全体を参照できます。複数のスライスが同じ基底配列を参照することも可能です。スライスは動的にサイズを変更できるように見えますが、実際には新しい基底配列が割り当てられ、要素がコピーされることで実現されます。

3. 構造体 (Struct) と埋め込み (Embedding)

Goの構造体は、異なる型のフィールドをまとめた複合データ型です。Goは、他の構造体やインターフェースを構造体内に「埋め込む」ことができます。これは、埋め込まれた型が、それを埋め込んだ構造体のフィールドとしてではなく、その構造体自身のメソッドやフィールドであるかのように振る舞うことを可能にする、一種の継承のようなメカニズムです。

このコミットの文脈では、構造体内に配列が埋め込まれているケースが問題となります。例えば、type X struct { buf [1]byte; ... } のように、構造体 X の中に buf という名前の配列が直接定義されている場合です。

4. ポインタスキャンと型情報

Goのガベージコレクタは、メモリ領域をスキャンする際に、その領域にポインタが含まれているかどうかを判断する必要があります。これは、ポインタが他のオブジェクトへの参照であり、GCが到達可能性を判断するために追跡する必要があるためです。Goのランタイムは、各データ型に関する型情報(type information)を持っており、これを利用して、どのメモリ領域がポインタを含み、どの領域がポインタを含まないかを識別します。

問題は、スライスが構造体内の埋め込み配列を指している場合、GCがスライスの型情報だけに基づいてスキャンを行うと、そのスライスが指す「要素の型」の情報しか得られず、それが実際にはより大きな構造体の一部であるという文脈を見落としてしまうことにありました。結果として、スライスが指す配列の範囲を超えて、構造体全体をポインタスキャンしてしまうという誤動作が発生していました。

技術的詳細

このバグは、Goランタイムのガベージコレクタがスライスをスキャンするロジック、特に scanblock 関数(src/pkg/runtime/mgc0.c に存在)内で発生していました。

通常、GCはスライスをスキャンする際に、スライスのポインタ(array フィールド)、長さ(len フィールド)、容量(cap フィールド)を利用して、基底配列のどの部分が有効なデータを含んでいるかを判断します。そして、その有効な範囲内の要素を、スライスの要素型に基づいてスキャンします。

しかし、問題のシナリオは以下の通りです。

  1. ある構造体 S があり、その中に配列 A が埋め込まれている。
  2. スライス s が、この構造体 S の中の配列 A を指している。例えば、s = S.A[:] のように作成された場合。

このとき、GCが s をスキャンする際に、s の型情報からはその要素型(例: byte)しか得られません。GCは、スライスの array フィールドが指すアドレスから、スライスの長さ分のメモリを、その要素型に基づいてスキャンしようとします。

しかし、s.array が指すアドレスは、実際には構造体 S の先頭アドレス、またはその非常に近いオフセットに位置する配列 A の先頭アドレスです。もしGCがスライスの要素型(例えば byte はポインタを含まない)に基づいてスキャンを続行し、かつ objti = pc[2] | PRECISE | LOOP; のように、スライスの要素型から得られる型情報(objti)をそのまま使用してしまうと、以下の問題が発生します。

  • 誤ったスキャン範囲: スライスの要素型がポインタを含まない型(例: byte, int など)であっても、それが構造体の一部である場合、構造体には他のポインタフィールドが含まれている可能性があります。GCがスライスの要素型に基づいて「ポインタを含まない」と判断し、スキャンをスキップしたり、あるいはスライスの範囲を超えて構造体全体をスキャンしてしまったりする可能性があります。
  • 型情報の不一致: pc[2] は、スライスの要素型に関する型情報を含んでいます。しかし、スライスが構造体内の埋め込み配列を指している場合、pc[2] が提供する型情報は、実際にメモリ上に存在するオブジェクト(構造体 S)の真の型情報とは異なります。GCは、スライスの要素型ではなく、sliceptr->array が指す実際のメモリ領域の型情報(ヒープから取得できる)に基づいてスキャンを行うべきです。

このコミットの修正は、この問題を解決するために、スライスが指すメモリ領域の型情報を、スライスの要素型から直接取得するのではなく、ヒープから実際のオブジェクトの型情報を取得するように変更しています。これにより、GCはスライスが指すメモリが、実際にはより大きな構造体の一部である場合でも、その構造体全体の正しい型情報に基づいてポインタスキャンを行うことができるようになります。

具体的には、objti = pc[2] | PRECISE | LOOP; の行が削除され、コメントで「スライスの要素型をスキャンに使用できない。なぜなら、それが構造体の先頭に埋め込まれた配列を指している場合、構造体全体をスライスとしてスキャンしてしまうからだ。だから、ヒープから型情報を取得する。」と説明されています。これは、obj (スライスが指す基底配列のポインタ) の実際の型情報を、GCがヒープメタデータから動的に取得することを意味します。これにより、GCはスライスの見かけ上の型ではなく、実際のメモリレイアウトに基づいて正確なスキャンを実行できるようになります。

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

このコミットによる主要なコード変更は以下の2つのファイルにあります。

  1. src/pkg/runtime/gc_test.go: 問題を再現し、修正を検証するための新しいテストケース TestGcArraySlice が追加されました。
  2. src/pkg/runtime/mgc0.c: ガベージコレクタのコアロジックが含まれるCファイルで、スライスのスキャン方法が修正されました。

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

--- a/src/pkg/runtime/gc_test.go
+++ b/src/pkg/runtime/gc_test.go
@@ -97,3 +97,27 @@ func TestGcHashmapIndirection(t *testing.T) {
 		m[a] = T{}
 	}
 }
+
+func TestGcArraySlice(t *testing.T) {
+	type X struct {
+		buf     [1]byte
+		nextbuf []byte
+		next    *X
+	}
+	var head *X
+	for i := 0; i < 10; i++ {
+		p := &X{}
+		p.buf[0] = 42
+		p.next = head
+		if head != nil {
+			p.nextbuf = head.buf[:]
+		}
+		head = p
+		runtime.GC()
+	}
+	for p := head; p != nil; p = p.next {
+		if p.buf[0] != 42 {
+			t.Fatal("corrupted heap")
+		}
+	}
+}

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

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -799,7 +799,11 @@ scanblock(Workbuf *wbuf, Obj *wp, uintptr nobj, bool keepworking)
 			sliceptr = (Slice*)(stack_top.b + pc[1]);
 			if(sliceptr->cap != 0) {
 				obj = sliceptr->array;
-				objti = pc[2] | PRECISE | LOOP;
+				// Can't use slice element type for scanning,
+				// because if it points to an array embedded
+				// in the beginning of a struct,
+				// we will scan the whole struct as the slice.
+				// So just obtain type info from heap.
 			}
 			pc += 3;
 			break;

コアとなるコードの解説

src/pkg/runtime/gc_test.goTestGcArraySlice

このテストケースは、問題のシナリオを具体的に再現し、GCが正しく動作することを確認するために設計されています。

  • type X struct { buf [1]byte; nextbuf []byte; next *X }:

    • buf [1]byte: 構造体 X の中に埋め込まれた1バイトの配列です。これが、スライスが指す可能性のある「構造体内の配列」の例となります。
    • nextbuf []byte: 別のスライスフィールドです。
    • next *X: X 型のポインタで、リンクリストを形成するために使用されます。
  • リンクリストの構築とGCの実行:

    • for i := 0; i < 10; i++ ループ内で、X 型のオブジェクトが10個作成され、リンクリストとして連結されます。
    • p.buf[0] = 42: 各オブジェクトの buf 配列の最初のバイトに 42 という値が設定されます。これは、GC後にこの値が保持されているかを確認するためのマーカーとなります。
    • if head != nil { p.nextbuf = head.buf[:] }: ここが重要な部分です。新しく作成されたオブジェクト pnextbuf スライスが、前のオブジェクト headbuf 配列全体を指すように設定されます。これにより、「スライスが構造体内に埋め込まれた配列を指す」という問題のシナリオが再現されます。
    • runtime.GC(): 各イテレーションで明示的にGCが実行されます。これにより、GCが問題のシナリオで正しく動作するかどうかを頻繁にチェックできます。
  • 検証:

    • for p := head; p != nil; p = p.next ループで、リンクリストの各要素をたどります。
    • if p.buf[0] != 42 { t.Fatal("corrupted heap") }: 各オブジェクトの buf[0] の値が 42 のままであることを確認します。もしGCが誤って buf 配列を含む構造体 X の一部を解放してしまったり、破損させてしまったりした場合、この値は変更され、「corrupted heap」エラーが発生します。このチェックにより、GCがメモリを正しく保持していることが保証されます。

src/pkg/runtime/mgc0.cscanblock 関数内の変更

scanblock 関数は、Goのガベージコレクタの重要な部分であり、メモリブロックをスキャンしてポインタを識別し、到達可能なオブジェクトをマークする役割を担っています。

変更された箇所は、スライス(Slice 型)を処理する部分です。

			sliceptr = (Slice*)(stack_top.b + pc[1]);
			if(sliceptr->cap != 0) {
				obj = sliceptr->array;
-				objti = pc[2] | PRECISE | LOOP; // 削除された行
+				// Can't use slice element type for scanning,
+				// because if it points to an array embedded
+				// in the beginning of a struct,
+				// we will scan the whole struct as the slice.
+				// So just obtain type info from heap. // 追加されたコメント
			}
			pc += 3;
			break;
  • sliceptr = (Slice*)(stack_top.b + pc[1]);: スタック上のポインタ情報から Slice 構造体へのポインタを取得します。
  • if(sliceptr->cap != 0): スライスの容量が0でない場合(つまり、有効な基底配列を持っている場合)に処理を進めます。
  • obj = sliceptr->array;: スライスの array フィールド(基底配列の先頭へのポインタ)を obj に代入します。これが、GCがスキャンを開始する実際のメモリアドレスです。
  • - objti = pc[2] | PRECISE | LOOP;: この行が削除されました。
    • pc[2] は、スライスの要素型に関する型情報(type info)を含んでいました。
    • 以前のロジックでは、GCはスライスの要素型から得られるこの型情報 objti を使用して、obj が指すメモリ領域をスキャンしていました。
    • しかし、前述の通り、スライスが構造体内の埋め込み配列を指している場合、pc[2] の型情報は、実際のメモリ上のオブジェクト(構造体全体)の型情報と一致しませんでした。これにより、GCが誤った範囲をスキャンしたり、ポインタを見落としたりする可能性がありました。
  • 新しいコメント: 削除された行の代わりに、新しいコメントが追加されました。
    • // Can't use slice element type for scanning,
    • // because if it points to an array embedded
    • // in the beginning of a struct,
    • // we will scan the whole struct as the slice.
    • // So just obtain type info from heap. このコメントは、なぜスライスの要素型情報を使用できないのか、そして代わりに何をするべきか(ヒープから型情報を取得する)を明確に説明しています。この変更により、GCは obj が指す実際のメモリ領域のメタデータ(ヒープに格納されている型情報)を動的に参照し、それに基づいて正確なポインタスキャンを実行するようになります。これにより、スライスが構造体内の埋め込み配列を指している場合でも、構造体全体が正しくスキャンされ、メモリ破損が防止されます。

この修正は、Goのガベージコレクタの堅牢性を高め、特定の複雑なメモリレイアウトにおける誤ったスキャン動作を防ぐ上で非常に重要です。

関連リンク

参考にした情報源リンク