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

[インデックス 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の実行時型情報を表し、typeIdgob 独自の型識別子です。

バグの発生源は、getDecEnginePtr 関数にありました。この関数は、特定の型IDとユーザー型情報 (userTypeInfo) に基づいてデコードエンジンを取得または生成し、キャッシュから取得しようとします。元の実装では、キャッシュのキーとして ut.base (ユーザー型の基底型) を使用していました。

例えば、*map[string]interface{} をデコードする場合、ut.user*map[string]interface{} ですが、ut.basemap[string]interface{} になります。もし、map[string]interface{} をデコードした後に *map[string]interface{} をデコードしようとすると、両者が同じ ut.base を持つため、getDecEnginePtrmap[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 の変更

  1. エラーメッセージの変更:

    -			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) の間接参照レベルが一致しない場合に発生します。

  2. デコードエンジンのキャッシュキーの変更:

    -	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 の変更

  1. TestPtrToMapOfMap テストケースの追加: この新しいテストケースは、*map のようなポインタを介したマップのデコードに関するバグを具体的に再現し、修正が正しく機能することを確認するために追加されました。
    • Register(make(map[string]interface{}))gobmap[string]interface{} 型を登録します。これは、gob が未知の型をエンコード/デコードする際に必要となる場合があります。
    • subdatadata の作成: map[string]interface{} 型のネストされたマップを作成します。data["foo"] = subdata のように、マップの中に別のマップが値として格納されています。
    • エンコード: NewEncoder(b).Encode(data)databytes.Buffer にエンコードします。
    • デコード: NewDecoder(b).Decode(&newData) でエンコードされたデータを newData にデコードします。ここで重要なのは、newDatamap[string]interface{} 型のポインタとして渡されている点です。元のバグは、このようなポインタを介したマップのデコードで発生していました。
    • 検証: !reflect.DeepEqual(data, newData) を使用して、元の data とデコードされた newData が完全に一致するかどうかを検証します。もし一致しない場合はテストが失敗し、バグがまだ存在するか、修正が不完全であることを示します。

このテストケースは、getDecEnginePtrut.base の代わりに ut.user をキャッシュキーとして使用するように変更されたことで、*map のような型が正しくデコードされるようになったことを確認するためのものです。

関連リンク

参考にした情報源リンク