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

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

このコミットは、Go言語のreflectパッケージを使用してnilマップにアクセスする際の挙動を修正し、関連するテストを追加するものです。具体的には、nilマップに対してreflect.Value.MapKeys()reflect.Value.MapIndex()を呼び出した際に、予期せぬパニックや誤った結果が生じる可能性があった問題を解決します。これにより、reflectパッケージを通じたnilマップの操作が、Go言語の組み込みマップのnil値のセマンティクス(存在しないキーへのアクセスはゼロ値を返す)と一貫するようになります。

コミット

commit d54b67df0cc2f9cfa7785919e20d152305bd72e8
Author: Russ Cox <rsc@golang.org>
Date:   Mon May 19 09:36:47 2014 -0400

    reflect: test, fix access to nil maps
    
    Fixes #8010.
    
    LGTM=bradfitz, khr
    R=khr, bradfitz, dvyukov
    CC=golang-codereviews
    https://golang.org/cl/91450048

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

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

元コミット内容

reflect: test, fix access to nil maps

Fixes #8010.

LGTM=bradfitz, khr
R=khr, bradfitz, dvyukov
CC=golang-codereviews
https://golang.org/cl/91450048

変更の背景

Go言語において、マップは非常に強力なデータ構造ですが、そのnil値の扱いは重要です。Goの組み込みマップでは、nilマップからキーを読み取ろうとすると、そのキーの型のゼロ値が返され、パニックは発生しません。しかし、このコミット以前は、reflectパッケージを通じてnilマップにアクセスしようとすると、この期待される挙動が保証されず、場合によってはパニックを引き起こす可能性がありました。

具体的には、reflect.Value型で表現されたnilマップに対してMapKeys()(マップのキーのリストを取得する)やMapIndex()(特定のキーに対応する値を取得する)といったメソッドを呼び出した際に、ランタイムレベルでの適切なnilチェックが不足していたため、内部的なハッシュマップ操作が不正なポインタアクセスを引き起こし、プログラムがクラッシュする可能性がありました。

この問題はIssue #8010として報告されたと考えられます。reflectパッケージはGoの強力な機能であり、リフレクションを利用するライブラリやフレームワークにとって、nil値の安全な取り扱いは不可欠です。このコミットは、reflectパッケージの堅牢性を高め、nilマップの操作がGoのセマンティクスに沿って安全に行われるようにするために導入されました。

前提知識の解説

Goのmap

Goにおけるmapは、キーと値のペアを格納するハッシュテーブルの実装です。mapは参照型であり、変数を宣言しただけではnil値になります。

  • nilマップ: var m map[string]intのように宣言されただけのマップはnilです。nilマップは要素を持たず、読み取り操作(キーが存在しない場合はゼロ値を返す)は可能ですが、書き込み操作(要素の追加や変更)はパニックを引き起こします。
  • 空マップ: m := make(map[string]int)のようにmake関数で初期化されたマップは空マップです。要素は持っていませんが、読み書き操作が可能です。

reflectパッケージ

reflectパッケージは、Goプログラムの実行時に型情報(reflect.Type)や値(reflect.Value)を検査・操作するための機能を提供します。これにより、ジェネリックなコードの記述や、構造体のフィールドへの動的なアクセス、関数の動的な呼び出しなどが可能になります。

  • reflect.Value: Goのあらゆる値(変数、定数、関数の戻り値など)を抽象的に表現する型です。reflect.ValueOf()関数を使ってGoの値をreflect.Valueに変換できます。
  • MapKeys(): reflect.Valueがマップを表す場合、このメソッドはマップ内のすべてのキーをreflect.Valueのスライスとして返します。
  • MapIndex(key reflect.Value): reflect.Valueがマップを表す場合、このメソッドは指定されたkeyに対応する値をreflect.Valueとして返します。キーが存在しない場合、その型のゼロ値を表すreflect.ValueIsValid()falseを返す)を返します。

nil値の扱い

Goでは、ポインタ、スライス、マップ、チャネル、関数、インターフェースなどの参照型は、初期状態ではnilという特殊な値を取ります。nilは「値がない」ことを示し、これらの型がまだ有効なメモリ領域を指していないことを意味します。nil値に対する操作は、型によって許容されるものとパニックを引き起こすものがあります。マップの場合、nilマップからの読み取りはゼロ値を返しますが、書き込みはパニックになります。

runtimeパッケージ

runtimeパッケージは、Goプログラムの実行環境(ガベージコレクション、スケジューラ、メモリ管理など)を制御する低レベルな機能を提供します。Goの組み込み型(マップ、スライスなど)の多くの操作は、このruntimeパッケージ内のCGoコード(.gocファイル)によって実装されています。

hashmap.gocreflect·mapaccess

  • hashmap.goc: Goランタイムのハッシュマップ(マップの実装)に関連するCGoコードが含まれるファイルです。Goのマップ操作の効率性と安全性を保証するための低レベルなロジックがここに記述されています。
  • reflect·mapaccess: reflectパッケージがマップの要素にアクセスする際に、Goランタイムが内部的に呼び出す関数の一つです。この関数は、reflect.Valueでラップされたマップとキーを受け取り、対応する値を返します。

raceenabled

Goのデータ競合検出機能が有効になっているかどうかを示すフラグです。go run -racego build -raceでビルドされたプログラムでは、複数のゴルーチンが共有データに同時にアクセスし、少なくとも一方が書き込み操作を行う場合に競合を検出します。

hash_lookup

ハッシュマップ内で特定のキーを検索するための内部関数です。この関数は、マップの内部構造を直接操作し、キーに対応する値のポインタを返します。

技術的詳細

このコミットは、reflectパッケージとGoランタイムのマップ実装の2つの主要な部分にわたる変更を含んでいます。

src/pkg/reflect/all_test.goの変更

このファイルには、reflectパッケージのテストスイートが含まれています。今回のコミットでは、TestNilMapという新しいテスト関数が追加されました。このテストの目的は、nilマップに対するreflectパッケージの挙動がGoのセマンティクスに沿っていることを確認することです。

  • var m map[string]int: まず、nilmap[string]intが宣言されます。
  • mv := ValueOf(m): このnilマップがreflect.Valueに変換されます。
  • keys := mv.MapKeys(): nilマップに対してMapKeys()が呼び出されます。期待される挙動は、キーが一つも存在しないため、長さが0のスライスが返されることです。テストではlen(keys) != 0の場合にエラーを報告します。
  • x := mv.MapIndex(ValueOf("hello")): nilマップに対して、存在しないキー"hello"MapIndex()が呼び出されます。Goの組み込みマップのセマンティクスでは、nilマップから存在しないキーを読み取ると、その型のゼロ値が返されます。reflectパッケージでは、これはInvalidreflect.Valueとして表現されます。テストではx.Kind() != Invalidの場合にエラーを報告します。
  • var mbig map[string][10 << 20]byte: さらに、値が非常に大きい(10MBのバイト配列)nilマップmbigに対しても同様のテストが行われます。これは、値のサイズに関わらずnilマップの挙動が一貫していることを確認するためです。

これらのテストは、reflectパッケージがnilマップを適切に処理し、パニックを起こさずに期待されるゼロ値や空のスライスを返すことを保証するためのものです。

src/pkg/runtime/hashmap.gocの変更

このファイルは、Goランタイムにおけるハッシュマップの低レベルな実装を含んでいます。reflectパッケージからマップにアクセスする際に呼び出される内部関数reflect·mapaccessに重要な変更が加えられました。

変更前は、reflect·mapaccess関数内でh != nil(ハッシュマップのポインタがnilでない)というチェックがraceenabled(データ競合検出が有効な場合)のブロック内にのみ存在していました。このため、raceenabledが有効でない環境でnilマップがreflect·mapaccessに渡された場合、hnilであるにもかかわらず、その後のhash_lookup関数が呼び出されてしまい、不正なメモリアクセス(パニック)を引き起こす可能性がありました。

変更後のコードでは、reflect·mapaccess関数の冒頭にh == nilのチェックが追加されました。

  • if(h == nil): ハッシュマップのポインタhnilであるかを最初にチェックします。
  • val = nil;: もしhnilであれば、val(戻り値のポインタ)をnilに設定し、関数を即座に終了します。これにより、nilマップに対するhash_lookupの呼び出しが完全に回避されます。
  • else { ... }: hnilでない場合にのみ、従来のraceenabledチェックとhash_lookupの呼び出しが実行されます。

この変更により、reflect·mapaccessnilマップが渡された場合でも安全に処理を終了し、reflectパッケージがInvalidreflect.Valueを返すための基盤を提供します。これは、ランタイムレベルでの堅牢性を高め、reflectパッケージのnilマップ処理におけるパニックを根本的に防ぐための重要な修正です。

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

diff --git a/src/pkg/reflect/all_test.go b/src/pkg/reflect/all_test.go
index 1e6341bd0b..9c5eb4e554 100644
--- a/src/pkg/reflect/all_test.go
+++ b/src/pkg/reflect/all_test.go
@@ -973,6 +973,28 @@ func TestMap(t *testing.T) {
 	}
 }
 
+func TestNilMap(t *testing.T) {
+	var m map[string]int
+	mv := ValueOf(m)
+	keys := mv.MapKeys()
+	if len(keys) != 0 {
+		t.Errorf(">0 keys for nil map: %v", keys)
+	}
+
+	// Check that value for missing key is zero.
+	x := mv.MapIndex(ValueOf("hello"))
+	if x.Kind() != Invalid {
+		t.Errorf("m.MapIndex(\"hello\") for nil map = %v, want Invalid Value", x)
+	}
+
+	// Check big value too.
+	var mbig map[string][10 << 20]byte
+	x = ValueOf(mbig).MapIndex(ValueOf("hello"))
+	if x.Kind() != Invalid {
+		t.Errorf("mbig.MapIndex(\"hello\") for nil map = %v, want Invalid Value", x)
+	}
+}
+
 func TestChan(t *testing.T) {
 	for loop := 0; loop < 2; loop++ {
 		var c chan int
diff --git a/src/pkg/runtime/hashmap.goc b/src/pkg/runtime/hashmap.goc
index 4f5e78897b..36707c6ede 100644
--- a/src/pkg/runtime/hashmap.goc
+++ b/src/pkg/runtime/hashmap.goc
@@ -908,11 +908,15 @@ func mapaccess2(t *MapType, h *Hmap, key *byte) (val *byte, pres bool) {
 
 #pragma textflag NOSPLIT
 func reflect·mapaccess(t *MapType, h *Hmap, key *byte) (val *byte) {
-\tif(raceenabled && h != nil) {\n-\t\truntime·racereadpc(h, runtime·getcallerpc(&t), reflect·mapaccess);\n-\t\truntime·racereadobjectpc(key, t->key, runtime·getcallerpc(&t), reflect·mapaccess);\n+\tif(h == nil)\n+\t\tval = nil;\n+\telse {\n+\t\tif(raceenabled) {\n+\t\t\truntime·racereadpc(h, runtime·getcallerpc(&t), reflect·mapaccess);\n+\t\t\truntime·racereadobjectpc(key, t->key, runtime·getcallerpc(&t), reflect·mapaccess);\n+\t\t}\n+\t\tval = hash_lookup(t, h, &key);\n \t}\n-\tval = hash_lookup(t, h, &key);\n }\n 
 #pragma textflag NOSPLIT

コアとなるコードの解説

src/pkg/reflect/all_test.goの変更点

新しく追加されたTestNilMap関数は、reflectパッケージがnilマップを正しく処理するかどうかを検証します。

  • var m map[string]intnilマップを宣言し、mv := ValueOf(m)reflect.Valueに変換します。
  • mv.MapKeys()を呼び出し、nilマップのキーリストが空であることを確認します。nilマップにはキーが存在しないため、これは期待される挙動です。
  • mv.MapIndex(ValueOf("hello"))を呼び出し、nilマップから存在しないキーを検索した場合に、返されるreflect.ValueInvalidであることを確認します。これは、Goの組み込みマップがnilの場合にキーが存在しないものとして扱われ、その型のゼロ値が返される動作をreflectパッケージでも再現するためのものです。
  • さらに、map[string][10 << 20]byteのような大きな値を持つマップに対しても同様のテストを行い、値のサイズに関わらずnilマップの挙動が一貫していることを保証します。

これらのテストは、reflectパッケージがnilマップを操作する際にパニックを起こさず、Goのセマンティクスに沿った結果を返すことを保証するためのものです。

src/pkg/runtime/hashmap.gocの変更点

reflect·mapaccess関数は、reflectパッケージからマップにアクセスする際にGoランタイムが呼び出す内部関数です。この関数への変更は、nilマップの安全な処理を保証するためのものです。

変更前は、raceenabled(データ競合検出)が有効な場合にのみh != nilのチェックが行われていました。このため、raceenabledが無効な環境でnilマップが渡されると、hnilであるにもかかわらず、その後のhash_lookup関数が呼び出され、不正なメモリアクセス(パニック)を引き起こす可能性がありました。

変更後のコードでは、reflect·mapaccess関数の冒頭にif(h == nil)というチェックが追加されました。

  • もしh(ハッシュマップのポインタ)がnilであれば、val(戻り値のポインタ)をnilに設定し、関数を即座に終了します。これにより、nilマップに対するhash_lookupの呼び出しが完全に回避され、ランタイムレベルでの安全性が確保されます。
  • raceenabledのチェックとhash_lookupの呼び出しは、hnilでない場合にのみ実行されるように変更されました。

この修正により、reflectパッケージがnilマップを操作する際に、ランタイムが常に安全な方法で処理を終了し、予期せぬパニックを防ぐことができるようになりました。

関連リンク

参考にした情報源リンク

  • Go reflectパッケージ公式ドキュメント (一般的な知識)
  • Go map型公式ドキュメント (一般的な知識)
  • "Go reflect nil map access issue"に関するWeb検索結果 (reflect.Valueによるnilマップアクセスがパニックを引き起こす可能性についての情報)