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

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

このコミットは、Go言語の標準ライブラリであるreflectパッケージ内のvalue.goファイルに対する変更です。reflectパッケージは、実行時にプログラムの構造を検査し、変更するための機能を提供します。具体的には、型情報の取得、構造体のフィールドへのアクセス、関数の動的な呼び出しなどが行えます。value.goファイルは、reflect.Value型の実装を含んでおり、Goのあらゆる値(変数、関数、構造体など)を抽象化して操作するための基盤を提供します。このファイル内のcallメソッドは、リフレクションを通じて関数を呼び出す際の引数処理を担当しています。

コミット

reflect: expose reflect.call argument slice to the garbage collector

The argument slice was kept hidden from the garbage collector
by destroying its referent in an unsafe.Pointer to uintptr
conversion. This change preserves the unsafe.Pointer referent
and only performs an unsafe.Pointer to uintptr conversions
within expressions that construct new unsafe.Pointer values.

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

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

https://github.com/golang/go/commit/0ab8f2d287159de00dfa64793a64285223f5661e

元コミット内容

reflect: expose reflect.call argument slice to the garbage collector

The argument slice was kept hidden from the garbage collector
by destroying its referent in an unsafe.Pointer to uintptr
conversion. This change preserves the unsafe.Pointer referent
and only performs an unsafe.Pointer to uintptr conversions
within expressions that construct new unsafe.Pointer values.

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

変更の背景

このコミットの背景には、Go言語のガベージコレクタ(GC)が、reflect.callメソッド内で動的に確保された引数スライスを正しく追跡できていなかったという問題があります。

reflect.callメソッドは、リフレクションを用いて関数を呼び出す際に、その関数の引数を格納するための一時的なメモリ領域(スライス)を確保します。このメモリ領域は、unsafe.PointeruintptrというGoのunsafeパッケージの機能を使って操作されていました。

問題は、この引数スライスへのポインタがunsafe.Pointerからuintptrに変換された後、元のunsafe.Pointer参照が失われていた点にあります。GoのGCは、unsafe.Pointer型の値が指すメモリ領域は追跡しますが、uintptr型の値が指すメモリ領域は追跡しません。uintptrは単なる整数であり、GCにとってはメモリアドレスを示すものとは認識されないためです。

その結果、reflect.callが使用する引数スライスがGCから「隠れて」しまい、GCがそのメモリがまだ使用中であることを認識できず、誤って解放してしまう可能性がありました。これは、メモリ破損やクラッシュ、あるいはメモリリーク(GCが解放すべきメモリを解放しない)につながる深刻なバグの原因となります。

このコミットは、この問題を解決し、reflect.callが使用する引数スライスが常にGCによって正しく追跡されるようにするためのものです。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念について理解しておく必要があります。

  1. Goのreflectパッケージ:

    • Goは静的型付け言語ですが、reflectパッケージを使用することで、実行時に型情報を検査したり、変数の値を動的に操作したり、関数を動的に呼び出したりすることができます。
    • reflect.Value型は、Goのあらゆる値(変数、関数、構造体など)を抽象化して表現し、それらに対するリフレクション操作を可能にします。
    • reflect.Value.Callメソッドは、reflect.Valueが表す関数を、reflect.Valueのスライスとして渡された引数で呼び出すために使用されます。
  2. Goのガベージコレクション (GC):

    • Goは自動メモリ管理を採用しており、開発者が手動でメモリを解放する必要はありません。GCが不要になったメモリ領域を自動的に特定し、解放します。
    • GCは、プログラムがアクセス可能なメモリ領域(「到達可能」なオブジェクト)を追跡し、到達不能になったメモリ領域を解放します。
    • GCがメモリを正しく管理するためには、プログラム内のすべてのポインタ参照を正確に把握している必要があります。
  3. unsafeパッケージ:

    • Goは通常、厳格な型安全性を提供しますが、unsafeパッケージは、その型安全性の制約を意図的にバイパスするための機能を提供します。
    • unsafeパッケージは、低レベルのメモリ操作や、C言語とのインターフェースなど、特定の高度なユースケースでのみ使用されるべきです。誤用すると、メモリ破損やセキュリティ上の脆弱性を引き起こす可能性があります。
  4. unsafe.Pointer:

    • unsafe.Pointerは、Goの任意の型のポインタを保持できる特殊なポインタ型です。
    • *T(任意の型Tへのポインタ)とunsafe.Pointerの間、およびuintptrunsafe.Pointerの間で相互に変換可能です。
    • 重要な点: unsafe.PointerはGCによって追跡されます。つまり、unsafe.Pointerが指すメモリ領域は、そのunsafe.Pointerが到達可能である限り、GCによって解放されません。
  5. uintptr:

    • uintptrは、ポインタを符号なし整数として表現する型です。
    • 重要な点: uintptrはGCによって追跡されません。uintptrは単なる数値であり、GCはそれがメモリアドレスを指しているとは認識しません。したがって、uintptr型の変数だけがメモリ領域を指している場合、GCはそのメモリ領域が到達不能であると判断し、誤って解放してしまう可能性があります。
  6. unsafe.Pointerからuintptrへの変換とGCの関係:

    • unsafe.Pointeruintptrに変換すると、GCはそのポインタが指していたメモリ領域への参照を失います。
    • もし、そのメモリ領域への唯一の参照がunsafe.Pointerからuintptrへの変換によって失われた場合、GCはそのメモリ領域を解放してしまいます。
    • しかし、uintptrを介してそのメモリ領域にアクセスしようとすると、既に解放されたメモリにアクセスすることになり、プログラムがクラッシュしたり、予期せぬ動作を引き起こしたりします(Use-After-Freeバグ)。
    • このため、unsafe.Pointerからuintptrへの変換は、非常に慎重に行う必要があり、GCが追跡すべきメモリへの参照が常にunsafe.Pointerとして保持されていることを保証しなければなりません。

技術的詳細

このコミットの技術的な核心は、reflect.Value.callメソッド内で引数スライスargsがGCによって正しく追跡されるように、unsafe.Pointeruintptrの利用方法を変更した点にあります。

変更前は、argsスライスの先頭アドレスを取得する際に、unsafe.Pointer(&args[0])を直接uintptrに変換していました。

ptr := uintptr(unsafe.Pointer(&args[0]))

この行が実行されると、argsスライスへの唯一のGCが追跡可能な参照(unsafe.Pointer(&args[0]))が、GCが追跡しないuintptr型のptrに変換され、元のunsafe.Pointerは一時的な値としてすぐに破棄されていました。これにより、argsスライス自体がGCから見えなくなり、GCがそのメモリを解放してしまう可能性がありました。

変更後は、ptr変数をunsafe.Pointer型として保持するようにしました。

ptr := unsafe.Pointer(&args[0])

これにより、ptrargsスライスへのGCが追跡可能な参照を保持し続けるため、argsスライスはGCによって到達可能であると認識され、誤って解放されることがなくなります。

しかし、メモリ内のオフセット計算(例えば、スライス内の特定の引数のアドレスを計算する)には、ポインタ演算が必要です。Goではunsafe.Pointerに対する直接的なポインタ演算は許可されていません。ポインタ演算を行うためには、一時的にuintptrに変換する必要があります。

このコミットでは、この要件を満たすために、ポインタ演算が必要な場合にのみ、unsafe.Pointeruintptrに変換し、その結果をすぐにunsafe.Pointerに戻すというパターンを採用しています。

addr := unsafe.Pointer(uintptr(ptr) + off)

このパターンでは、ptrunsafe.Pointer)が指すメモリ領域へのGCが追跡可能な参照はptr変数によって保持され続けます。uintptr(ptr)への変換は、+ offという算術演算のための一時的なものであり、その結果はすぐにunsafe.Pointerに再変換されてaddrに格納されます。これにより、addrもまたGCが追跡可能なポインタとなります。

この変更により、reflect.callメソッドが使用する引数スライスは、そのライフサイクル全体を通じてGCによって正しく追跡されるようになり、メモリリークやメモリ破損のリスクが排除されました。これは、Goのランタイムにおけるメモリ安全性を向上させる重要な修正です。

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

diff --git a/src/pkg/reflect/value.go b/src/pkg/reflect/value.go
index 5acb69efa6..df549f5e16 100644
--- a/src/pkg/reflect/value.go
+++ b/src/pkg/reflect/value.go
@@ -446,11 +446,11 @@ func (v Value) call(op string, in []Value) []Value {
 	// For now make everything look like a pointer by allocating
 	// a []unsafe.Pointer.
 	args := make([]unsafe.Pointer, size/ptrSize)
-	ptr := uintptr(unsafe.Pointer(&args[0]))
+	ptr := unsafe.Pointer(&args[0])
 	off := uintptr(0)
 	if v.flag&flagMethod != 0 {
 		// Hard-wired first argument.
-		*(*iword)(unsafe.Pointer(ptr)) = rcvr
+		*(*iword)(ptr) = rcvr
 		off = ptrSize
 	}
 	for i, v := range in {
@@ -459,7 +459,7 @@ func (v Value) call(op string, in []Value) []Value {
 		a := uintptr(targ.align)
 		off = (off + a - 1) &^ (a - 1)
 		n := targ.size
-		addr := unsafe.Pointer(ptr + off)
+		addr := unsafe.Pointer(uintptr(ptr) + off)
 		v = v.assignTo("reflect.Value.Call", targ, (*interface{})(addr))
 		if v.flag&flagIndir == 0 {
 			storeIword(addr, iword(v.val), n)
@@ -471,7 +471,7 @@ func (v Value) call(op string, in []Value) []Value {
 	off = (off + ptrSize - 1) &^ (ptrSize - 1)
 
 	// Call.
-	call(fn, unsafe.Pointer(ptr), uint32(size))
+	call(fn, ptr, uint32(size))
 
 	// Copy return values out of args.
 	//
@@ -482,7 +482,7 @@ func (v Value) call(op string, in []Value) []Value {
 		a := uintptr(tv.Align())
 		off = (off + a - 1) &^ (a - 1)
 		fl := flagIndir | flag(tv.Kind())<<flagKindShift
-		ret[i] = Value{tv.common(), unsafe.Pointer(ptr + off), fl}
+		ret[i] = Value{tv.common(), unsafe.Pointer(uintptr(ptr) + off), fl}
 		off += tv.Size()
 	}
 

コアとなるコードの解説

このコミットにおける主要な変更点は、src/pkg/reflect/value.goファイル内のValue.callメソッドにおけるunsafe.Pointeruintptrの取り扱い方です。

  1. ptr変数の型変更:

    -	ptr := uintptr(unsafe.Pointer(&args[0]))
    +	ptr := unsafe.Pointer(&args[0])
    
    • 変更前: argsスライスの先頭アドレスをunsafe.Pointerとして取得した後、すぐにuintptrに変換してptrに代入していました。これにより、argsスライスへのGCが追跡可能な参照が失われ、GCがそのメモリを解放してしまう可能性がありました。
    • 変更後: ptr変数をunsafe.Pointer型として宣言し、argsスライスの先頭アドレスを直接unsafe.Pointerとして保持するようにしました。これにより、ptrargsスライスへのGCが追跡可能な参照を保持し続け、GCがargsスライスを正しく追跡できるようになります。
  2. rcvrの格納箇所の変更:

    -		*(*iword)(unsafe.Pointer(ptr)) = rcvr
    +		*(*iword)(ptr) = rcvr
    
    • 変更前: ptruintptr型であったため、ポインタとして使用する際にunsafe.Pointer(ptr)と明示的に変換する必要がありました。
    • 変更後: ptrが既にunsafe.Pointer型であるため、追加の変換なしに直接使用できるようになりました。
  3. 引数アドレスaddrの計算方法の変更:

    -		addr := unsafe.Pointer(ptr + off)
    +		addr := unsafe.Pointer(uintptr(ptr) + off)
    
    • 変更前: ptruintptr型であったため、ptr + offというポインタ演算が直接可能でした。その結果をunsafe.Pointerに変換してaddrに代入していました。
    • 変更後: ptrunsafe.Pointer型になったため、直接ポインタ演算を行うことはできません。そのため、一時的にuintptr(ptr)に変換してoffとの加算を行い、その結果を再度unsafe.Pointerに変換してaddrに代入しています。このパターンにより、ptrargsスライスへのGCが追跡可能な参照を保持しつつ、必要なポインタ演算も安全に行えるようになります。
  4. call関数の引数変更:

    -	call(fn, unsafe.Pointer(ptr), uint32(size))
    +	call(fn, ptr, uint32(size))
    
    • 変更前: ptruintptr型であったため、call関数に渡す際にunsafe.Pointer(ptr)と明示的に変換する必要がありました。
    • 変更後: ptrが既にunsafe.Pointer型であるため、追加の変換なしに直接call関数に渡せるようになりました。
  5. 戻り値ret[i]の計算方法の変更:

    -		ret[i] = Value{tv.common(), unsafe.Pointer(ptr + off), fl}
    +		ret[i] = Value{tv.common(), unsafe.Pointer(uintptr(ptr) + off), fl}
    
    • 変更前: ptruintptr型であったため、ptr + offというポインタ演算が直接可能でした。その結果をunsafe.Pointerに変換してret[i]ptrフィールドに代入していました。
    • 変更後: ptrunsafe.Pointer型になったため、引数アドレスaddrの計算と同様に、一時的にuintptr(ptr)に変換してoffとの加算を行い、その結果を再度unsafe.Pointerに変換してret[i]ptrフィールドに代入しています。

これらの変更は、unsafe.Pointerが指すメモリ領域がGCによって常に追跡されるというGoの保証を遵守し、uintptrへの変換はポインタ演算のための一時的なものに限定することで、reflect.callメソッドのメモリ安全性を大幅に向上させています。

関連リンク

参考にした情報源リンク

  • Go言語のunsafeパッケージに関する公式ドキュメント: https://pkg.go.dev/unsafe
  • Go言語のガベージコレクションに関する情報(Goのドキュメントやブログ記事など)