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

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

このコミットは、Go言語の標準ライブラリである encoding/gob パッケージに対する変更です。具体的には、ファズテストの導入と、非常に長い型名に対するバグ修正が含まれています。

変更されたファイルは以下の通りです。

  • src/pkg/encoding/gob/codec_test.go: ファズテストの追加
  • src/pkg/encoding/gob/decode.go: 型名の長さに関するデコードロジックの修正
  • src/pkg/encoding/gob/error.go: エラーハンドリングの改善

コミット

commit 9440d823a504d581ef82c53f1bf69b4b0f8e2d55
Author: David Symonds <dsymonds@golang.org>
Date:   Mon Feb 6 14:02:12 2012 +1100

    gob: fuzz testing, plus a fix for very large type names.
    
    Fixes #2689.
    
    R=r
    CC=golang-dev
    https://golang.org/cl/5616063

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

https://github.com/golang/go/commit/9440d823a504d581ef82c53f1bf69b4b0f8e2d55

元コミット内容

gob: fuzz testing, plus a fix for very large type names.

Fixes #2689.

R=r
CC=golang-dev
https://golang.org/cl/5616063

変更の背景

このコミットの主な背景は、encoding/gob パッケージの堅牢性を向上させることです。特に、以下の2つの側面が挙げられます。

  1. ファズテストの導入: encoding/gob はGoのデータ構造をバイナリ形式にシリアライズ・デシリアライズするためのパッケージであり、ネットワーク通信や永続化など、外部からの入力に晒される可能性があります。悪意のある、または予期せぬ形式の入力データが与えられた場合、プログラムがクラッシュしたり、セキュリティ上の脆弱性が発生したりするリスクがあります。ファズテストは、このような不正な入力に対するシステムの耐性を検証するために導入されました。
  2. 非常に長い型名に対する修正 (Fixes #2689): Issue #2689は、「harden against invalid input」と題されており、無効な入力に対するencoding/gobの堅牢性に関する問題でした。このコミットでは、特に非常に長い型名が与えられた場合に発生する可能性のある問題を修正しています。これは、デシリアライズ処理中に型名の長さを処理する際の潜在的な脆弱性やバグに対処するためのものです。

これらの変更は、encoding/gobパッケージがより安全で信頼性の高いものとなることを目的としています。

前提知識の解説

encoding/gob パッケージとは

encoding/gob はGo言語の標準ライブラリの一つで、Goのデータ構造をバイナリ形式にエンコード(シリアライズ)およびデコード(デシリアライズ)するためのプロトコルを提供します。主な特徴は以下の通りです。

  • Go言語専用: 主にGoプログラム間でのデータ交換を目的としており、他のプログラミング言語との互換性は考慮されていません。
  • 自己記述型ストリーム: gobストリーム内の各データ項目は、その型情報を含んでいます。これにより、受信側は事前に型を知らなくてもデータをデコードできます。
  • 効率性: 同じ Encoder を使用して複数の値を送信する場合に最も効率的です。これは、各データ型に対してカスタムコーデックをコンパイルし、そのコンパイルコストを償却するためです。
  • RPCでの利用: リモートプロシージャコール (RPC) における引数や結果の転送によく利用されます。

ファズテスト (Fuzz Testing) とは

ファズテスト(Fuzzing または Fuzz Testing)は、ソフトウェアのセキュリティ脆弱性やバグを発見するための自動テスト手法です。ランダムまたは半ランダムな、無効な、予期せぬ、または異常なデータをプログラムの入力として与え、その応答(クラッシュ、アサーション失敗、メモリリークなど)を監視します。

  • 目的: ソフトウェアの堅牢性、安定性、セキュリティを向上させること。特に、パーサー、プロトコル実装、ファイルフォーマット処理など、複雑な入力処理を行う部分で有効です。
  • 動作原理:
    1. 入力生成: テスト対象のプログラムに与える入力データを生成します。これは完全にランダムなデータであることもあれば、既存の有効な入力データ(シードコーパス)を少し変更(ミューテーション)したものであることもあります。
    2. プログラム実行: 生成された入力をプログラムに与えて実行します。
    3. 監視: プログラムの動作を監視し、クラッシュ、ハング、メモリリーク、不正な出力などの異常を検出します。
    4. レポート: 異常が検出された場合、その異常を引き起こした入力データを記録し、開発者に報告します。

Go言語にはGo 1.18以降、標準でファズテストの機能が組み込まれています。

技術的詳細

このコミットでは、主に以下の3つの領域で技術的な変更が行われています。

  1. ファズテストの追加 (src/pkg/encoding/gob/codec_test.go):

    • encFuzzDec 関数が追加されました。この関数は、任意のGoの値を gob でエンコードし、そのエンコードされたバイト列の一部をランダムに改変(ファズ)した後、再度デコードを試みます。このプロセス中にエラーが発生しないか、または予期せぬパニックが発生しないかを検証します。
    • TestFuzz 関数が追加され、encFuzzDec を使用して様々な型の入力データに対してファズテストを実行します。testing.Short() をチェックすることで、通常のテスト実行ではスキップされ、明示的にファズテストを実行する場合にのみ実行されるようになっています。
    • TestFuzzRegressions 関数は、過去に発見された特定のシード値(1328492090837718000)を使用して、特定の回帰バグ(この場合は非常に長い型名が原因で発生する問題)が再発しないことを確認します。
    • testFuzz ヘルパー関数は、指定されたシード値と実行回数でファズテストを繰り返し実行するための共通ロジックを提供します。
  2. デコードロジックの修正 (src/pkg/encoding/gob/decode.go):

    • decodeInterface メソッド内で、型名の長さをデコードする部分にバリデーションが追加されました。
    • 以前は b := make([]byte, state.decodeUint()) のように、デコードされた符号なし整数を直接バイトスライスの長さに使用していました。
    • 修正後には nr := state.decodeUint() で長さを取得した後、if nr < 0 || nr > 1<<31 というチェックが追加されました。これにより、デコードされた型名の長さが負の値であるか、または 1<<31 (約2GB) を超えるような異常に大きな値である場合に errorf を呼び出してエラーを発生させるようになりました。これは、悪意のある入力や破損したデータによって非常に大きなメモリ割り当てが試みられ、サービス拒否 (DoS) 攻撃につながる可能性を防ぐための重要な変更です。1<<31 は、Goのバイトスライスが取りうる最大長(約2GB)を考慮した上限値です。
  3. エラーハンドリングの改善 (src/pkg/encoding/gob/error.go):

    • catchError 関数内で、recover() されたパニックが gobError 型であることを明示的にチェックするようになりました。
    • 以前は *err = e.(gobError).err のように、recover() された値 e が常に gobError 型であると仮定していました。
    • 修正後には ge, ok := e.(gobError) という型アサーションと ok 変数によるチェックが追加されました。これにより、egobError 型でない(例えば、ランタイムエラーなど、gob パッケージ内部で発生したものではないパニック)場合には、panic(e) を呼び出して元のパニックを再発生させるようになりました。これは、gob パッケージが予期しないパニックを適切に処理し、デバッグを容易にするための堅牢性向上策です。

これらの変更は、gob パッケージが不正な入力に対してより耐性を持つようにし、潜在的なセキュリティ問題や安定性の問題を未然に防ぐことを目的としています。

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

src/pkg/encoding/gob/codec_test.go

@@ -1407,3 +1409,60 @@ func TestDebugStruct(t *testing.T) {
 	}
 	debugFunc(debugBuffer)
 }
+
+func encFuzzDec(rng *rand.Rand, in interface{}) error {
+	buf := new(bytes.Buffer)
+	enc := NewEncoder(buf)
+	if err := enc.Encode(&in); err != nil {
+		return err
+	}
+
+	b := buf.Bytes()
+	for i, bi := range b {
+		if rng.Intn(10) < 3 {
+			b[i] = bi + uint8(rng.Intn(256))
+		}
+	}
+
+	dec := NewDecoder(buf)
+	var e interface{}
+	if err := dec.Decode(&e); err != nil {
+		return err
+	}
+	return nil
+}
+
+// This does some "fuzz testing" by attempting to decode a sequence of random bytes.
+func TestFuzz(t *testing.T) {
+	if testing.Short() {
+		return
+	}
+
+	// all possible inputs
+	input := []interface{}{
+		new(int),
+		new(float32),
+		new(float64),
+		new(complex128),
+		&ByteStruct{255},
+		&ArrayStruct{},
+		&StringStruct{"hello"},
+		&GobTest1{0, &StringStruct{"hello"}},
+	}
+	testFuzz(t, time.Now().UnixNano(), 100, input...)
+}
+
+func TestFuzzRegressions(t *testing.T) {
+	// An instance triggering a type name of length ~102 GB.
+	testFuzz(t, 1328492090837718000, 100, new(float32))
+}
+
+func testFuzz(t *testing.T, seed int64, n int, input ...interface{}) {
+	t.Logf("seed=%d n=%d\n", seed, n)
+	for _, e := range input {
+		rng := rand.New(rand.NewSource(seed))
+		for i := 0; i < n; i++ {
+			encFuzzDec(rng, e)
+		}
+	}
+}

src/pkg/encoding/gob/decode.go

@@ -690,7 +690,11 @@ func (dec *Decoder) decodeInterface(ityp reflect.Type, state *decoderState, p ui
 	// Create a writable interface reflect.Value.  We need one even for the nil case.
 	ivalue := allocValue(ityp)
 	// Read the name of the concrete type.
-	b := make([]byte, state.decodeUint())
+	nr := state.decodeUint()
+	if nr < 0 || nr > 1<<31 { // zero is permissible for anonymous types
+		errorf("invalid type name length %d", nr)
+	}
+	b := make([]byte, nr)
 	state.b.Read(b)
 	name := string(b)
 	if name == "" {

src/pkg/encoding/gob/error.go

@@ -33,7 +33,11 @@ func error_(err error) {
 // plain error.  It overwrites the error return of the function that deferred its call.\n func catchError(err *error) {
  	if e := recover(); e != nil {
-\t\t*err = e.(gobError).err // Will re-panic if not one of our errors, such as a runtime error.
+\t\tge, ok := e.(gobError)\n+\t\tif !ok {\n+\t\t\tpanic(e)\n+\t\t}\n+\t\t*err = ge.err
  	}\n  	return
  }\n```

## コアとなるコードの解説

### `src/pkg/encoding/gob/codec_test.go` の変更

*   **`encFuzzDec(rng *rand.Rand, in interface{}) error`**:
    *   この関数は、ファズテストの核となるロジックを実装しています。
    *   入力 `in` を `gob` でエンコードし、その結果を `buf` に書き込みます。
    *   エンコードされたバイト列 `b` を取得し、`rng.Intn(10) < 3` の確率で各バイトをランダムに改変します。これにより、不正な `gob` データが生成されます。
    *   改変されたバイト列を `NewDecoder` に渡し、デコードを試みます。
    *   エンコードまたはデコード中にエラーが発生した場合、それを返します。
*   **`TestFuzz(t *testing.T)`**:
    *   `testing.Short()` チェックにより、`go test -short` 実行時にはスキップされます。これは、ファズテストが長時間実行される可能性があるためです。
    *   `input` スライスには、様々な組み込み型やカスタム構造体のインスタンスが含まれており、これらの型に対してファズテストが実行されます。
    *   `testFuzz` ヘルパー関数を呼び出し、現在の時刻をシードとして、100回のファズテストを実行します。
*   **`TestFuzzRegressions(t *testing.T)`**:
    *   特定のシード値 `1328492090837718000` を使用して `testFuzz` を呼び出します。このシード値は、過去に発見された「長さが約102GBの型名」という回帰バグを再現するために使用されます。これにより、修正が正しく適用され、将来的に同じ問題が再発しないことを保証します。
*   **`testFuzz(t *testing.T, seed int64, n int, input ...interface{})`**:
    *   ファズテストの共通ロジックをカプセル化したヘルパー関数です。
    *   与えられた `seed` と `n` (実行回数) を使用して、各入力 `e` に対して `encFuzzDec` を `n` 回呼び出します。
    *   `rand.New(rand.NewSource(seed))` をループ内で呼び出すことで、各テストケースの実行が同じシードから開始され、再現性が保証されます。

### `src/pkg/encoding/gob/decode.go` の変更

*   **`func (dec *Decoder) decodeInterface(...)` 内の変更**:
    *   `nr := state.decodeUint()`: `gob` ストリームから型名の長さを符号なし整数としてデコードします。
    *   `if nr < 0 || nr > 1<<31`: ここが重要な変更点です。
        *   `nr < 0`: `decodeUint()` は符号なし整数を返すため、通常は負の値にはなりませんが、Goの整数型のオーバーフローや、将来的な変更に備えた防御的なチェックと考えられます。
        *   `nr > 1<<31`: デコードされた型名の長さが `1<<31` (約2GB) を超える場合にエラーを発生させます。これは、Goのバイトスライスの最大長が通常2GB程度であることと関連しており、これを超える長さのメモリを確保しようとすると、システムリソースを枯渇させたり、パニックを引き起こしたりする可能性があるためです。
    *   `errorf("invalid type name length %d", nr)`: 上記の条件に合致した場合、無効な型名の長さとしてエラーを報告します。
    *   `b := make([]byte, nr)`: バリデーション後、安全な長さ `nr` でバイトスライスを割り当てます。

### `src/pkg/encoding/gob/error.go` の変更

*   **`func catchError(err *error)` 内の変更**:
    *   `if e := recover(); e != nil`: パニックが発生した場合に `recover()` で捕捉します。
    *   `ge, ok := e.(gobError)`: 捕捉したパニックの値 `e` が `gobError` 型であるかを型アサーションでチェックします。`ok` は型アサーションが成功したかどうかを示すブール値です。
    *   `if !ok`: `e` が `gobError` 型でなかった場合(つまり、`gob` パッケージ自身が意図的に発生させたエラーではない場合)、
        *   `panic(e)`: 元のパニックを再発生させます。これにより、`gob` パッケージの内部エラーではない、より深刻なランタイムエラーなどが適切に伝播され、デバッグが容易になります。
    *   `*err = ge.err`: `e` が `gobError` 型であった場合、その内部のエラーを `err` ポインタに設定します。

これらの変更は、`gob` パッケージの堅牢性とセキュリティを大幅に向上させ、不正な入力に対する耐性を高めることを目的としています。

## 関連リンク

*   Go CL: [https://golang.org/cl/5616063](https://golang.org/cl/5616063)
*   Go Issue #2689: [https://goissues.org/issue/2689](https://goissues.org/issue/2689)

## 参考にした情報源リンク

*   `encoding/gob` パッケージの目的:
    *   [https://go.dev/blog/gob-and-rpc](https://go.dev/blog/gob-and-rpc)
    *   [https://go.dev/src/encoding/gob/doc.go](https://go.dev/src/encoding/gob/doc.go)
*   Go言語におけるファズテスト:
    *   [https://go.dev/doc/fuzz/](https://go.dev/doc/fuzz/)
    *   [https://codingexplorations.com/go/fuzz-testing-in-go/](https://codingexplorations.com/go/fuzz-testing-in-go/)
*   Go Issue #2689:
    *   [https://goissues.org/issue/2689](https://goissues.org/issue/2689)
*   その他、`encoding/gob`に関する一般的な情報:
    *   [https://medium.com/@vertexaisearch/grounding-api-redirect-auziyqe1e9c5baljljcjfrloicgkxejrdh-ct0gzr8amtm60bro1gvramecml1kpzv2joi5ljjkcon58gxq24dhcvtcjckxay-mr7vxain8ngixq64w4uo1nkckyr0uhrrsqnopkdatwc_0lqbxgzgmfloa5ffiqigrqn2clgrtww_ae43efdmfhvundnb-ll7nsg3vp75_lugtvjj8ovcp8fsaj9hnvbmbmpku0ojva4gdhcdnmri65dqr-dfsgso_jvqgn](https://medium.com/@vertexaisearch/grounding-api-redirect-auziyqe1e9c5baljljcjfrloicgkxejrdh-ct0gzr8amtm60bro1gvramecml1kpzv2joi5ljjkcon58gxq24dhcvtcjckxay-mr7vxain8ngixq64w4uo1nkckyr0uhrrsqnopkdatwc_0lqbxgzgmfloa5ffiqigrqn2clgrtww_ae43efdmfhvundnb-ll7nsg3vp75_lugtvjj8ovcp8fsaj9hnvbmbmpku0ojva4gdhcdnmri65dqr-dfsgso_jvqgn)
    *   [https://shaadi.com/vertexaisearch/grounding-api-redirect/auziyqgsebblb7n1jtuzyysojbdq8h2dqufgrjrm-0cmudbvokrcknle4s6pdhyxqpjzl8rutyswwtos5mzwatuwtvj3whp2zfv7xddgz_ljqyxuph0zerfhg5jgavwn-q3eug9ukclnegjrajyplkkrydoz_4isxhkylgoe6qkrqqp](https://shaadi.com/vertexaisearch/grounding-api-redirect/auziyqgsebblb7n1jtuzyysojbdq8h2dqufgrjrm-0cmudbvokrcknle4s6pdhyxqpjzl8ruyyswwtos5mzwatuwtvj3whp2zfv7xddgz_ljqyxuph0zerfhg5jgavwn-q3eug9ukclnegjrajyplkkrydoz_4isxhkylgoe6qkrqqp)
    *   [https://ubc.ca/vertexaisearch/grounding-api-redirect/auziyqebvfjyhchl67foq_uhragh1ccvs4h5pvkhu1uamzfkppf0zyggge9uy-eqz6hsynlebsccbr9sg2zvy7i7od69wu46boui1oved6_6qxw9sxtrxuhjslsesqjismndsl_arcp-apqqjo2f9zrqyak81gtj7x5nxwlm3dyd9gus1qg4hdarl31ksdwwmljeaewcl_8ufj2z988](https://ubc.ca/vertexaisearch/grounding-api-redirect/auziyqebvfjyhchl67foq_uhragh1ccvs4h5pvkhu1uamzfkppf0zyggge9uy-eqz6hsynlebsccbr9sg2zvy7i7od69wu46boui1oved6_6qxw9sxtrxuhjslsesqjismndsl_arcp-apqqjo2f9zrqyak81gtj7x5nxwlm3dyd9gus1qg4hdarl31ksdwwmljeaewcl_8ufj2z988)
    *   [https://mingrammer.com/vertexaisearch/grounding-api-redirect/auziyqgnk_0pd3viey3y0x-yawkfdhp5buzjd9nxuzg5chgyz0cn1jq1up0u4oyyxh87vjhvu5jodlfxsvsgquyaaslgkqggpizaojiry7u7xpx_svoljmit9ju3tkiungd3zivz_hwwdcm5hotobgz8uv5w896clse-twflhbcayq](https://mingrammer.com/vertexaisearch/grounding-api-redirect/auziyqgnk_0pd3viey3y0x-yawkfdhp5buzjd9nxuzg5chgyz0cn1jq1up0u4oyyxh87vjhvu5jodlfxsvsgquyaaslgkqggpizaojiry7u7xpx_svoljmit9ju3tkiungd3zivz_hwwdcm5hotobgz8uv5w896clse-twflhbcayq)