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

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

このコミットは、Go言語のreflectパッケージにおけるマップ型生成のバグ修正に関するものです。具体的には、reflect.Newを用いてマップ変数を生成した際に、ガベージコレクタ(GC)が誤った型情報に基づいてメモリをスキャンしてしまう問題を解決しています。これにより、メモリの過剰スキャンや、場合によってはクラッシュ、あるいは誤ったオブジェクトの保持(false retention)といった問題が発生する可能性がありました。

コミット

commit 5bc1cef869b0c6caea2d680010908cf6871c6c24
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue May 13 09:53:47 2014 +0400

    reflect: fix map type generation
    If a map variable is created with reflect.New it has incorrect type (map[unsafe.Pointer]unsafe.Pointer).
    If GC follows such pointer, it scans Hmap and buckets with incorrect type.
    This can lead to overscan of up to 120 bytes for map[int8]struct{}.
    Which in turn can lead to crash if the memory after a bucket object is unaddressable
    or false retention (buckets are scanned as arrays of unsafe.Pointer).
    I don't see how it can lead to heap corruptions, though.
    
    LGTM=khr
    R=rsc, khr
    CC=golang-codereviews
    https://golang.org/cl/96270044

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

https://github.com/golang/go/commit/5bc1cef869b0c6caea2d680010908cf6871c6c24

元コミット内容

reflect: fix map type generation

reflect.Newを使ってマップ変数が作成された場合、その型が誤って(map[unsafe.Pointer]unsafe.Pointerとして)認識されていました。 GCがこのようなポインタを追跡すると、Hmap(マップの内部構造)とそのバケットを誤った型情報でスキャンしてしまいます。 これにより、map[int8]struct{}のようなマップでは最大120バイトの過剰スキャンが発生する可能性がありました。 この過剰スキャンは、バケットオブジェクトの後のメモリがアドレス不可能である場合にクラッシュを引き起こしたり、あるいは誤ったオブジェクトの保持(バケットがunsafe.Pointerの配列としてスキャンされるため)につながる可能性がありました。 ただし、ヒープ破損につながる可能性は低いとされています。

変更の背景

Go言語のreflectパッケージは、実行時に型情報を検査し、操作するための強力な機能を提供します。reflect.New関数は、指定された型Tの新しい項目へのポインタを返します。これは、*T型の値であり、その要素はT型のゼロ値です。

このコミット以前は、reflect.Newを使ってマップ型の変数を動的に生成する際に、ガベージコレクタがそのマップの内部構造を正しく認識できないという問題がありました。具体的には、生成されたマップの型情報が、GCがポインタを追跡し、メモリをスキャンするために必要な正確な情報を含んでいませんでした。

Goのガベージコレクタは、ヒープ上のオブジェクトを正確に識別し、到達可能なオブジェクトをマークすることで、不要になったメモリを解放します。このプロセスにおいて、オブジェクトの型情報(特にポインタが含まれているかどうか、どこにポインタがあるか)は非常に重要です。マップのような複雑なデータ構造の場合、その内部にはキーと値のペアを格納するためのバケット(配列)が含まれており、これらのバケットがポインタを含む可能性があるため、GCはバケットの内容を正しくスキャンする必要があります。

しかし、reflect.Newで生成されたマップは、GCに対して「map[unsafe.Pointer]unsafe.Pointer」という誤った型情報を提供していました。これは、GCがマップのバケットを、実際にはポインタではないデータ(例えばint8struct{}のような非ポインタ型)が含まれている場合でも、すべてunsafe.Pointerの配列としてスキャンしてしまうことを意味します。

この誤ったスキャンは以下の問題を引き起こす可能性がありました。

  1. 過剰スキャン (Overscan): GCが本来スキャンする必要のないメモリ領域までスキャンしてしまう。例えば、map[int8]struct{}のようなマップでは、キーと値がポインタを含まないため、バケット内のデータはポインタとして扱われるべきではありません。しかし、誤った型情報により、GCはバケットのメモリをunsafe.Pointerの配列として扱い、その結果、バケットの実際のサイズを超えて最大120バイトも余分にスキャンしてしまうことがありました。
  2. クラッシュ (Crash): 過剰スキャンによって、マップのバケットの直後に位置するアドレス不可能なメモリ領域にGCがアクセスしようとすると、プログラムがクラッシュする可能性がありました。
  3. 誤ったオブジェクトの保持 (False Retention): GCが非ポインタデータをポインタとして誤って解釈し、その「ポインタ」がたまたまヒープ上の有効なオブジェクトを指しているように見えてしまう場合、実際には到達不可能であるはずのオブジェクトが到達可能であると誤認され、メモリが解放されない「誤った保持」が発生する可能性がありました。これはメモリリークの一種です。

このコミットは、これらの問題を解決し、reflect.Newで生成されたマップがGCによって正しく扱われるようにするために導入されました。

前提知識の解説

このコミットの理解には、以下のGo言語の内部動作に関する知識が不可欠です。

  1. reflectパッケージ:

    • Goのreflectパッケージは、実行時にプログラムの構造を検査・操作するための機能を提供します。これにより、型、フィールド、メソッドなどの情報を動的に取得したり、新しい値を生成したり、既存の値を変更したりすることができます。
    • reflect.Type: Goの型を表すインターフェースです。reflectパッケージを通じて取得できるすべての型情報はreflect.Typeインターフェースを実装しています。
    • reflect.New(typ Type) Value: 指定されたtypの新しい項目へのポインタを返します。返されるreflect.Value*typ型であり、その要素はtyp型のゼロ値で初期化されます。
  2. Goのマップ (map) の内部構造:

    • Goのマップはハッシュテーブルとして実装されており、内部的にはruntime.hmapという構造体で表現されます。
    • hmapは、キーと値のペアを格納するためのバケット(runtime.bmap)の配列を管理します。
    • バケットは、キーと値のデータを保持する固定サイズのメモリブロックです。マップのキーと値の型によっては、バケット内にポインタが含まれることがあります(例: map[string]interface{})。
    • マップのサイズが大きくなると、バケットの再配置(リハッシュ)が行われ、新しいバケットが割り当てられることがあります。
  3. Goのガベージコレクタ (GC):

    • GoのGCは、並行マーク&スイープ方式を採用しています。これは、プログラムの実行と並行してGCが動作し、アプリケーションの停止時間(STW: Stop-The-World)を最小限に抑えることを目指しています。
    • マークフェーズ: GCは、ルート(スタック、グローバル変数など)から到達可能なすべてのオブジェクトをマークします。この際、オブジェクトの型情報に基づいて、そのオブジェクトがポインタを含んでいるかどうか、そしてポインタがどこにあるかを正確に識別する必要があります。
    • スキャン: GCは、マークされたオブジェクトのメモリ領域をスキャンし、そこに含まれるポインタを追跡して、さらに到達可能なオブジェクトをマークします。このスキャンプロセスにおいて、オブジェクトのレイアウト(どのオフセットにポインタがあるか)を記述するGCプログラム(GCプログラム、GCビットマップなどと呼ばれる)が使用されます。
    • GCプログラム/GCビットマップ: Goの各型には、その型がメモリ上でどのように配置され、どの部分がポインタであるかを示す情報が関連付けられています。これはGCプログラムまたはGCビットマップとして知られ、GCがメモリを効率的かつ正確にスキャンするために利用されます。例えば、_GC_PTR_GC_ENDといった定数は、GCプログラムの命令の一部として使用されます。
    • unsafe.Pointer: Goのunsafeパッケージは、型安全性をバイパスして、任意の型へのポインタをuintptrに変換したり、その逆を行ったりすることを可能にします。これは非常に強力ですが、誤用するとメモリ破損やGCの誤動作を引き起こす可能性があるため、慎重に使用する必要があります。GCはunsafe.Pointerを通常のポインタとして扱いますが、その指す先の型情報は失われているため、GCがそのポインタの指す先のメモリをスキャンする際には、追加の注意が必要です。
  4. runtime.ptrGC構造体:

    • Goのランタイム内部で使用される構造体で、GCが特定のメモリ領域をスキャンする方法を記述するGCプログラムの一部を構成します。
    • width: スキャンするメモリ領域の幅(バイト単位)。
    • op: GC操作のタイプ(例: _GC_PTRはポインタをスキャンすることを示す)。
    • off: オフセット。
    • elemgc: 要素のGC情報。配列やマップの要素など、繰り返し構造の内部要素のGC情報を指します。
    • end: GCプログラムの終了を示すマーカー。

これらの概念を理解することで、このコミットがなぜ必要とされ、どのように問題を解決しているのかが明確になります。

技術的詳細

このコミットの核心は、reflect.MapOf関数が生成するマップ型(mapType)に、ガベージコレクタがマップの内部構造(hmapとそのバケット)を正しくスキャンするためのgc情報(GCプログラム)を適切に設定することです。

Goのランタイムでは、各型は_type構造体(reflect.rtypeの内部表現)を持っており、この構造体にはGCがその型のインスタンスをスキャンするために必要な情報が含まれています。特に、_type構造体にはgcフィールドがあり、これはGCプログラムへのポインタです。このGCプログラムは、その型のメモリレイアウトにおけるポインタの位置を記述します。

マップ型の場合、そのインスタンス(hmap)自体がポインタ(例えば、バケットへのポインタ)を含んでいます。さらに、マップのキーと値の型によっては、バケットの内部にユーザーデータとしてポインタが含まれることもあります。したがって、GCがマップを正しくスキャンするためには、マップインスタンス自体のポインタ情報と、バケット内のキー・値のポインタ情報の両方を考慮に入れる必要があります。

コミット前の問題は、reflect.Newを通じて動的に作成されたマップ型が、このgcフィールドを正しく設定していなかったことにあります。具体的には、MapOf関数がmapTypeを構築する際に、mapType.gcが適切に初期化されていませんでした。これにより、GCはデフォルトの、あるいは誤ったGCプログラムを使用してマップをスキャンしてしまい、前述の過剰スキャンや誤った保持といった問題を引き起こしていました。

このコミットでは、MapOf関数内でmapTypegcフィールドを明示的に設定することでこの問題を解決しています。

追加されたコードは以下の通りです。

	mt.gc = unsafe.Pointer(&ptrGC{
		width:  unsafe.Sizeof(uintptr(0)),
		op:     _GC_PTR,
		off:    0,
		elemgc: mt.hmap.gc,
		end:    _GC_END,
	})

このコードは、mapTypegcフィールドに、ptrGC構造体へのポインタを設定しています。このptrGC構造体は、GCに対して以下のような指示を与えます。

  • width: unsafe.Sizeof(uintptr(0)): これは、mapTypeのインスタンス(つまりhmap構造体)の先頭にあるポインタ(通常はhmap構造体自体へのポインタ、またはその一部)をスキャンすることを示唆しています。uintptr(0)のサイズは、システムにおけるポインタのサイズ(32ビットシステムでは4バイト、64ビットシステムでは8バイト)です。
  • op: _GC_PTR: これは、GCがポインタをスキャンする操作であることを示します。
  • off: 0: スキャンを開始するオフセットが0であることを示します。つまり、mapTypeのインスタンスの先頭からスキャンを開始します。
  • elemgc: mt.hmap.gc: ここが最も重要な部分です。elemgcフィールドは、要素のGC情報、つまりマップの内部構造であるhmapのGC情報を参照するように設定されています。mt.hmap.gcは、hmap構造体自体が持つGCプログラムを指します。このhmap.gcは、hmap構造体内のポインタ(例えば、バケットへのポインタ)や、バケット内のキーと値のポインタを正確にスキャンするための情報を含んでいます。これにより、GCはマップの内部構造を再帰的に正しくスキャンできるようになります。
  • end: _GC_END: GCプログラムの終了を示します。

この変更により、reflect.Newで作成されたマップ変数は、GCがそのメモリレイアウトを正確に理解し、過剰スキャンや誤った保持を防ぐための正しいGCプログラムを持つことになります。特に、elemgc: mt.hmap.gcの設定は、マップのバケット内の実際のキーと値の型に応じた正確なGCスキャンを保証するために不可欠です。

コミットメッセージにある「map[unsafe.Pointer]unsafe.Pointer」という誤った型は、おそらくmapTypegcフィールドが未設定だった場合に、GCがフォールバックとして使用する汎用的なポインタスキャンロジックが適用されていたことを示唆しています。この汎用ロジックは、すべてのデータをポインタとして扱うため、非ポインタ型のデータが含まれるマップで問題を引き起こしていました。

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

変更はsrc/pkg/reflect/type.goファイル内のMapOf関数に集中しています。

--- a/src/pkg/reflect/type.go
+++ b/src/pkg/reflect/type.go
@@ -1541,6 +1541,13 @@ func MapOf(key, elem Type) Type {
 	mt.uncommonType = nil
 	mt.ptrToThis = nil
 	mt.zero = unsafe.Pointer(&make([]byte, mt.size)[0])
+	mt.gc = unsafe.Pointer(&ptrGC{
+		width:  unsafe.Sizeof(uintptr(0)),
+		op:     _GC_PTR,
+		off:    0,
+		elemgc: mt.hmap.gc,
+		end:    _GC_END,
+	})
 
 	// INCORRECT. Uncomment to check that TestMapOfGC and TestMapOfGCValues
 	// fail when mt.gc is wrong.

コアとなるコードの解説

このコミットで追加された7行のコードは、reflectパッケージのMapOf関数内で、新しく生成されるマップ型(mapType、コード内ではmtとして参照)のガベージコレクション(GC)情報を設定しています。

MapOf関数は、指定されたキーと要素の型に基づいて新しいマップ型を構築し、返します。この関数は、Goの内部でマップ型がどのように表現されるかを定義するmapType構造体のインスタンスを作成し、そのフィールドを初期化します。

追加された行は、mt.gcフィールドに値を割り当てています。 mt.gcは、このマップ型のインスタンスがメモリ上でどのように配置され、GCがどの部分をポインタとしてスキャンすべきかを記述するGCプログラムへのポインタです。

具体的には、unsafe.Pointer(&ptrGC{...})という形で、ptrGC構造体のインスタンスを生成し、そのアドレスをunsafe.Pointerにキャストしてmt.gcに設定しています。

ptrGC構造体の各フィールドの意味は以下の通りです。

  • width: unsafe.Sizeof(uintptr(0)):
    • これは、GCがスキャンを開始するメモリブロックの幅を示します。uintptr(0)のサイズは、現在のアーキテクチャにおけるポインタのサイズ(例: 64ビットシステムでは8バイト)です。
    • これは、hmap構造体自体が持つポインタ(例えば、バケットへのポインタ)をスキャンするための初期幅を設定していると考えられます。
  • op: _GC_PTR:
    • これはGC操作のタイプを指定します。_GC_PTRは、GCがこのメモリ領域をポインタとしてスキャンすべきであることを示します。
    • つまり、このmapTypeのインスタンス(hmap)の先頭にポインタが存在し、それを追跡する必要があることをGCに伝えます。
  • off: 0:
    • スキャンを開始するオフセットをバイト単位で指定します。0は、mapTypeのインスタンスの先頭からスキャンを開始することを意味します。
  • elemgc: mt.hmap.gc:
    • このフィールドは、要素のGC情報、つまりマップの内部構造であるhmapのGC情報を参照するように設定されています。
    • mt.hmapは、mapType構造体の一部として定義されているhmap構造体の埋め込みフィールドです。
    • mt.hmap.gcは、hmap構造体自体が持つGCプログラムを指します。このhmap.gcは、hmap構造体内のポインタ(例えば、バケットへのポインタ)や、バケット内のキーと値のポインタを正確にスキャンするための情報を含んでいます。
    • この設定により、GCはマップのトップレベルの構造だけでなく、その内部にあるバケットや、バケット内のキーと値のデータ(これらがポインタを含む場合)も再帰的に正しくスキャンできるようになります。これが、過剰スキャンや誤った保持を防ぐための最も重要な部分です。
  • end: _GC_END:
    • GCプログラムの終了を示すマーカーです。

この変更により、reflect.Newで動的に生成されたマップ型は、GCがそのメモリレイアウトを正確に理解し、マップの内部構造(hmapとバケット)を効率的かつ安全にスキャンするための適切なGCプログラムを持つことになります。これにより、以前発生していた過剰スキャン、クラッシュ、誤った保持といった問題が解消されます。

コメントアウトされた行は、この修正がなければTestMapOfGCTestMapOfGCValuesが失敗することを示すテストコードの一部であり、この修正の重要性を強調しています。

関連リンク

  • Go言語のreflectパッケージのドキュメント: https://pkg.go.dev/reflect
  • Go言語のガベージコレクションに関する公式ブログ記事やドキュメント(一般的な情報源として):
    • The Go Blog: Go's new GC: https://go.dev/blog/go15gc (これはコミット後の情報ですが、GCの基本的な理解に役立ちます)
  • Goのマップの内部実装に関する記事(非公式なものが多いですが、理解を深めるのに役立ちます):

参考にした情報源リンク

  • Go言語の公式ソースコード(特にsrc/pkg/reflect/type.goおよびsrc/runtime/ディレクトリ内のGC関連ファイル)
  • Go言語のガベージコレクションに関する技術文書やブログ記事(一般的なGCの仕組みを理解するため)
  • unsafeパッケージのドキュメント: https://pkg.go.dev/unsafe
  • Goの型システムとGCの連携に関する議論(GoコミュニティのメーリングリストやIssueトラッカーなど)

(注: 特定の外部記事やブログへの直接リンクは、時間の経過とともにリンク切れになる可能性があるため、一般的な情報源のカテゴリとして記載しています。)