[インデックス 12024] ファイルの概要
このコミットは、Go言語の標準ライブラリである encoding/gob
パッケージにおけるデコードエンジンのキャッシュメカニズムに関するバグ修正です。具体的には、再帰的な型(例えば、マップのマップなど)をエンコード/デコードする際に、ポインタの多重間接参照レベルが正しく扱われず、誤ったエンジンが再利用される可能性があった問題に対処しています。この修正により、gob
エンコーダ/デコーダがユーザー定義型をより堅牢に処理できるようになります。
コミット
- コミットハッシュ:
420f713b7aa3b85995ded01d13cdeee520dbe38a
- Author: Rob Pike r@golang.org
- Date: Sat Feb 18 14:38:37 2012 +1100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/420f713b7aa3b85995ded01d13cdeee520dbe38a
元コミット内容
encoding/gob: cache engine for user type, not base type
When we build the encode engine for a recursive type, we
mustn't disregard the indirections or we can try to reuse an
engine at the wrong indirection level.
Fixes #3026.
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/5675087
変更の背景
このコミットは、Go言語の encoding/gob
パッケージにおける特定のバグ(Issue #3026)を修正するために行われました。gob
はGoのデータ構造をシリアライズ/デシリアライズするためのメカニズムを提供しますが、複雑な型、特にポインタを含む再帰的なデータ構造(例: *map[string]interface{}
のようなポインタを介したマップのマップ)を扱う際に問題が発生していました。
元の実装では、デコードエンジンをキャッシュする際に、型の「基底型 (base type)」に基づいてキャッシュキーを生成していました。しかし、ポインタの多重間接参照(*T
、**T
など)を持つ型の場合、同じ基底型を持つ異なる間接参照レベルの型が存在し得ます。例えば、map[string]interface{}
と *map[string]interface{}
は異なる型ですが、基底型は同じ map[string]interface{}
と見なされる可能性があります。
この問題により、gob
デコーダが誤った間接参照レベルのキャッシュされたエンジンを再利用しようとし、結果としてデコードエラーやデータ破損を引き起こす可能性がありました。特に、*map
のようなポインタを介したマップをデコードしようとした際に、内部のマップのデコードに失敗するという具体的なシナリオが報告されていました。
このコミットの目的は、デコードエンジンのキャッシュキーを「ユーザー型 (user type)」に基づいて生成するように変更することで、この問題を解決し、gob
の堅牢性を向上させることです。これにより、異なる間接参照レベルを持つ型がそれぞれ適切なデコードエンジンを持つことが保証されます。
前提知識の解説
encoding/gob
パッケージ
encoding/gob
は、Go言語のデータ構造をバイナリ形式でエンコード(シリアライズ)およびデコード(デシリアライズ)するためのGo標準ライブラリです。Goプログラム間でGoの値を効率的に転送したり、永続化したりするのに使用されます。gob
は、エンコードされるデータの型情報を自動的に含めるため、デコード側で事前に型を知っている必要がありません。
エンジン (Engine)
gob
パッケージ内部では、特定のGoの型をエンコードまたはデコードするための「エンジン」と呼ばれる内部構造が生成されます。これらのエンジンは、型の構造を解析し、どのようにデータをバイナリ形式に変換するか(またはその逆)の命令セットを含んでいます。効率化のため、一度生成されたエンジンはキャッシュされ、同じ型が再度現れたときに再利用されます。
間接参照 (Indirection)
プログラミングにおける間接参照とは、値そのものではなく、その値が格納されているメモリのアドレス(ポインタ)を介して値にアクセスすることを指します。Go言語では、ポインタ *T
は型 T
の値へのポインタを表します。**T
は *T
へのポインタ、つまり T
の値への二重間接参照を表します。gob
の文脈では、この間接参照のレベル(ポインタの数)がデコードの挙動に影響を与えることがあります。
ユーザー型 (User Type) と 基底型 (Base Type)
Goの型システムにおいて、type MyInt int
のように既存の型から新しい型を宣言した場合、MyInt
は「ユーザー型」であり、その「基底型」は int
です。ポインタ型の場合、*MyStruct
のユーザー型は *MyStruct
そのものであり、その基底型は MyStruct
となります。gob
のデコードエンジンをキャッシュする際には、このユーザー型と基底型の区別が重要になります。異なる間接参照レベルを持つ型(例: *T
と **T
)は、異なるユーザー型ですが、基底型は同じ T
である可能性があります。
キャッシュ (Cache)
キャッシュは、計算コストの高い操作の結果を一時的に保存し、同じ操作が再度要求されたときに保存された結果を再利用することで、パフォーマンスを向上させるための一般的な最適化手法です。gob
では、デコードエンジンを一度構築すると、それをキャッシュして後続のデコード操作で再利用します。
技術的詳細
encoding/gob
パッケージは、Goの値をシリアライズ/デシリアライズする際に、内部的に型情報を管理し、効率的な処理のために「デコードエンジン」を生成・キャッシュします。
デコードプロセスでは、Decoder
構造体が decoderCache
というマップを保持しており、これは map[reflect.Type]map[typeId]**decEngine
のような構造をしています。ここで reflect.Type
はGoの実行時型情報を表し、typeId
は gob
独自の型識別子です。
バグの発生源は、getDecEnginePtr
関数にありました。この関数は、特定の型IDとユーザー型情報 (userTypeInfo
) に基づいてデコードエンジンを取得または生成し、キャッシュから取得しようとします。元の実装では、キャッシュのキーとして ut.base
(ユーザー型の基底型) を使用していました。
例えば、*map[string]interface{}
をデコードする場合、ut.user
は *map[string]interface{}
ですが、ut.base
は map[string]interface{}
になります。もし、map[string]interface{}
をデコードした後に *map[string]interface{}
をデコードしようとすると、両者が同じ ut.base
を持つため、getDecEnginePtr
は map[string]interface{}
用にキャッシュされたエンジンを *map[string]interface{}
のデコードに再利用しようとする可能性がありました。
しかし、ポインタの有無や多重間接参照のレベルによって、デコードエンジンが実行すべき命令(特にポインタのデリファレンスに関する命令)は異なります。誤った間接参照レベルのエンジンを再利用すると、decodeSingle
関数内で instr.indir != ut.indir
というチェックに引っかかり、「inconsistent indirection」エラーが発生するか、あるいはより深刻なデータ破損を引き起こす可能性がありました。
このコミットは、getDecEnginePtr
関数がキャッシュキーとして ut.base
の代わりに ut.user
を使用するように変更することで、この問題を解決します。これにより、*map[string]interface{}
と map[string]interface{}
は異なるユーザー型として扱われ、それぞれに独立したデコードエンジンがキャッシュされるようになります。これにより、ポインタの多重間接参照レベルが正しく考慮され、適切なエンジンが常に使用されることが保証されます。
また、decodeSingle
関数内のエラーメッセージも修正され、より簡潔になっています。
テストケース TestPtrToMapOfMap
は、このバグを再現し、修正が正しく機能することを確認するために追加されました。このテストは、*map[string]interface{}
のような複雑なポインタ型を含むマップをエンコードし、その後デコードして、元のデータとデコードされたデータが完全に一致することを確認します。
コアとなるコードの変更箇所
src/pkg/encoding/gob/decode.go
--- a/src/pkg/encoding/gob/decode.go
+++ b/src/pkg/encoding/gob/decode.go
@@ -473,7 +473,7 @@ func (dec *Decoder) decodeSingle(engine *decEngine, ut *userTypeInfo, basep uint
}
instr := &engine.instr[singletonField]
if instr.indir != ut.indir {
- errorf("gob: internal error: inconsistent indirection instr %d ut %d", instr.indir, ut.indir)
+ errorf("internal error: inconsistent indirection instr %d ut %d", instr.indir, ut.indir)
}
ptr := unsafe.Pointer(basep) // offset will be zero
if instr.indir > 1 {
@@ -1149,7 +1149,7 @@ func (dec *Decoder) compileDec(remoteId typeId, ut *userTypeInfo) (engine *decEn
// getDecEnginePtr returns the engine for the specified type.
func (dec *Decoder) getDecEnginePtr(remoteId typeId, ut *userTypeInfo) (enginePtr **decEngine, err error) {
- rt := ut.base
+ rt := ut.user
decoderMap, ok := dec.decoderCache[rt]
if !ok {
decoderMap = make(map[typeId]**decEngine)
src/pkg/encoding/gob/encoder_test.go
--- a/src/pkg/encoding/gob/encoder_test.go
+++ b/src/pkg/encoding/gob/encoder_test.go
@@ -712,3 +712,27 @@ func TestGobPtrSlices(t *testing.T) {
t.Fatal("got %v; wanted %v", out, in)
}
}
+
+// getDecEnginePtr cached engine for ut.base instead of ut.user so we passed
+// a *map and then tried to reuse its engine to decode the inner map.
+func TestPtrToMapOfMap(t *testing.T) {
+ Register(make(map[string]interface{}))
+ subdata := make(map[string]interface{})
+ subdata["bar"] = "baz"
+ data := make(map[string]interface{})
+ data["foo"] = subdata
+
+ b := new(bytes.Buffer)
+ err := NewEncoder(b).Encode(data)
+ if err != nil {
+ t.Fatal("encode:", err)
+ }
+ var newData map[string]interface{}
+ err = NewDecoder(b).Decode(&newData)
+ if err != nil {
+ t.Fatal("decode:", err)
+ }
+ if !reflect.DeepEqual(data, newData) {
+ t.Fatalf("expected %v got %v", data, newData)
+ }
+}
コアとなるコードの解説
src/pkg/encoding/gob/decode.go
の変更
-
エラーメッセージの変更:
- errorf("gob: internal error: inconsistent indirection instr %d ut %d", instr.indir, ut.indir) + errorf("internal error: inconsistent indirection instr %d ut %d", instr.indir, ut.indir)
decodeSingle
関数内のエラーメッセージから"gob: "
プレフィックスが削除されました。これは機能的な変更ではなく、単にエラーメッセージのフォーマットを統一するためのクリーンアップです。このエラーは、デコードエンジンが期待する間接参照レベル (instr.indir
) と、現在デコードしようとしているユーザー型 (ut.indir
) の間接参照レベルが一致しない場合に発生します。 -
デコードエンジンのキャッシュキーの変更:
- rt := ut.base + rt := ut.user
getDecEnginePtr
関数は、デコードエンジンをキャッシュから取得する際に使用するキーを決定します。この変更がこのコミットの核心です。- 変更前:
rt := ut.base
は、ユーザー型の「基底型」をキャッシュキーとして使用していました。これにより、*map[string]interface{}
とmap[string]interface{}
のように、ポインタの有無は異なるが基底型が同じである型が、同じキャッシュエントリを共有してしまう可能性がありました。 - 変更後:
rt := ut.user
は、ユーザー型そのもの(ポインタの有無や多重間接参照レベルを含む)をキャッシュキーとして使用します。これにより、*map[string]interface{}
とmap[string]interface{}
は異なるキーとして扱われ、それぞれに独立したデコードエンジンがキャッシュされるようになります。これにより、異なる間接参照レベルを持つ型が誤って同じエンジンを再利用することがなくなり、inconsistent indirection
エラーや不正なデコードを防ぎます。
- 変更前:
src/pkg/encoding/gob/encoder_test.go
の変更
TestPtrToMapOfMap
テストケースの追加: この新しいテストケースは、*map
のようなポインタを介したマップのデコードに関するバグを具体的に再現し、修正が正しく機能することを確認するために追加されました。Register(make(map[string]interface{}))
:gob
にmap[string]interface{}
型を登録します。これは、gob
が未知の型をエンコード/デコードする際に必要となる場合があります。subdata
とdata
の作成:map[string]interface{}
型のネストされたマップを作成します。data["foo"] = subdata
のように、マップの中に別のマップが値として格納されています。- エンコード:
NewEncoder(b).Encode(data)
でdata
をbytes.Buffer
にエンコードします。 - デコード:
NewDecoder(b).Decode(&newData)
でエンコードされたデータをnewData
にデコードします。ここで重要なのは、newData
がmap[string]interface{}
型のポインタとして渡されている点です。元のバグは、このようなポインタを介したマップのデコードで発生していました。 - 検証:
!reflect.DeepEqual(data, newData)
を使用して、元のdata
とデコードされたnewData
が完全に一致するかどうかを検証します。もし一致しない場合はテストが失敗し、バグがまだ存在するか、修正が不完全であることを示します。
このテストケースは、getDecEnginePtr
が ut.base
の代わりに ut.user
をキャッシュキーとして使用するように変更されたことで、*map
のような型が正しくデコードされるようになったことを確認するためのものです。
関連リンク
- GitHub Commit: https://github.com/golang/go/commit/420f713b7aa3b85995ded01d13cdeee520dbe38a
- Go CL (Code Review): https://golang.org/cl/5675087
- Go Issue #3026: https://github.com/golang/go/issues/3026
参考にした情報源リンク
- Go Issue 3026: encoding/gob: cache engine for user type, not base type
- Go CL 5675087: encoding/gob: cache engine for user type, not base type
- GoDoc: encoding/gob package
- GoDoc: reflect package
- Go言語のポインタについて (一般的なポインタの概念理解のため)
- Go言語の型システム (一般的なGoの型システム理解のため)