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

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

このコミットは、Go言語の encoding/gob パッケージにおける、破損したデータに対するデコード処理中のクラッシュ(パニック)を修正するものです。具体的には、不正な入力データが与えられた際に発生する可能性のある2つのクラッシュを解消し、デコーダの堅牢性を向上させています。

コミット

commit 7a73f32725ff8b13a4cca703972fa76e598f4436
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Thu Jan 30 07:54:57 2014 +0100

    encoding/gob: fix two crashes on corrupted data.
    
    Fixes #6323.
    
    LGTM=r
    R=r
    CC=golang-codereviews
    https://golang.org/cl/56870043

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

https://github.com/golang/go/commit/7a73f32725ff8b13a4cca703972fa76e598f4436

元コミット内容

encoding/gob パッケージにおいて、破損したデータが入力された際に発生する2つのクラッシュを修正します。Issue #6323を解決します。

変更の背景

encoding/gob はGoプログラム間で構造化されたデータを効率的にシリアライズ・デシリアライズするためのパッケージです。しかし、ネットワーク経由でのデータ転送やファイルからの読み込みなど、外部からの入力は常に信頼できるとは限りません。悪意のある、あるいは単に破損したデータが gob デコーダに渡された場合、デコーダが予期せぬ状態に陥り、プログラムがクラッシュ(パニック)する可能性があります。

このコミットは、そのような状況下でのデコーダの脆弱性に対処するために導入されました。特に、型情報の長さが不正であったり、構造体のフィールド情報が欠落している場合に発生するパニックを特定し、それらを安全に処理するように修正することが目的です。コミットメッセージにある "Fixes #6323" は、この問題が特定のバグ報告(Issue 6323)に関連していることを示唆しています。このIssueは、おそらく不正な gob ストリームがデコードされた際に発生するパニックを報告していたものと推測されます。

前提知識の解説

encoding/gob パッケージ

encoding/gob はGo言語に特化したバイナリシリアライゼーションフォーマットです。主な特徴は以下の通りです。

  • Go言語間での利用: 主にGoプログラム間でのデータ交換(RPCなど)や、Goプログラム内での永続化に適しています。他の言語との相互運用性はありません。
  • 自己記述型: gob ストリームは自己記述型であり、各データ項目の前にその型情報が含まれます。これにより、デコーダは事前に型を知らなくてもデータをデコードできます。
  • 効率性: バイナリ形式であるため、JSONやXMLのようなテキストベースのフォーマットと比較して、データサイズが小さく、エンコード・デコードが高速です。特に、同じ Encoder を使って複数の値をストリームとして送信する場合、型情報の送信コストが償却されるため効率的です。
  • リフレクションの利用: Goのリフレクション機能を利用して、任意のGoデータ構造をシリアライズ・デシリアライズします。
  • 型情報の伝達: シリアライズ時に型情報も一緒に送信されるため、エンコーダとデコーダ間で型定義が多少異なっていても(フィールド名が一致していれば)デコードが可能です。

パニックとリカバリ (Panic and Recover)

Go言語における「パニック」は、プログラムの実行を停止させるランタイムエラーの一種です。これは通常、回復不可能なエラー(例: nilポインタ参照、配列の範囲外アクセス)が発生した場合に引き起こされます。パニックが発生すると、現在のゴルーチンは実行を停止し、遅延関数(defer)が実行され、コールスタックを遡っていきます。

recover は、パニックから回復するための組み込み関数です。defer 関数内で recover() を呼び出すことで、パニックの発生を検知し、パニックによって停止したゴルーチンの実行を再開させることができます。これにより、プログラム全体がクラッシュするのを防ぎ、エラーを適切に処理する機会を得られます。このコミットのテストコードでは、recover を使用して、不正な入力に対するデコード処理がパニックを引き起こさないことを検証しています。

データ破損とセキュリティ

シリアライゼーション/デシリアライゼーションの文脈では、データ破損はセキュリティ上の脆弱性につながる可能性があります。不正なデータがデコーダに渡された場合、デコーダがメモリを不正に読み書きしたり、無限ループに陥ったり、クラッシュしたりすることがあります。これはサービス拒否(DoS)攻撃や、場合によっては任意のコード実行につながる可能性もあります。そのため、デコーダは不正な入力に対して堅牢である必要があります。

技術的詳細

このコミットは、encoding/gob デコーダが不正な入力データに遭遇した際に発生する2つの特定のクラッシュを修正します。

  1. 不正な型名長さによるクラッシュ: gob ストリームでは、カスタム型が初めて出現する際に、その型の名前が送信されます。この名前の長さは、データストリーム内にエンコードされています。もしこの長さが不正な値(例えば、非常に大きな値や負の値)であった場合、デコーダは不正なメモリ領域を読み込もうとしたり、make([]byte, nr) のような操作で巨大なスライスを確保しようとしてメモリ不足に陥ったり、あるいは単に範囲外アクセスでパニックを引き起こす可能性がありました。 修正前は、decodeInterface 関数内で型名の長さを表す nruint64(state.b.Len()) (入力バッファの残りサイズ) を超える場合に、state.b.Read(b) がパニックを引き起こす可能性がありました。これは、make([]byte, nr) で確保されたスライス b のサイズが、実際に読み込めるデータ量よりもはるかに大きくなるためです。

  2. 構造体デコーダのコンパイル失敗によるクラッシュ: gob デコーダは、効率的なデコードのために、受信する型に基づいて内部的に「デコードエンジン」をコンパイルします。このエンジンは、特定の型をデコードするための命令のシーケンスです。構造体をデコードする際、デコーダは受信した gob ストリーム内の構造体のフィールド情報と、Goプログラム側の構造体のフィールドを比較し、マッピングを行います。 修正前は、decodeValue 関数内で、受信した gob ストリームの型情報(dec.wireType[wireId])が nil であるにもかかわらず、デコーダがそのフィールドにアクセスしようとするとパニックが発生する可能性がありました。これは、dec.wireType[wireId].StructT.Field にアクセスする前に dec.wireType[wireId] が有効であるかどうかのチェックが不足していたためです。このような状況は、特に不正な gob ストリームが与えられた場合に発生しやすくなります。

これらの問題は、デコーダが入力データの整合性を十分に検証せずに処理を進めてしまうことに起因していました。

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

src/pkg/encoding/gob/codec_test.go

  • newDT() 関数が追加され、テスト用の DT 構造体のインスタンスを生成するロジックが分離されました。これにより、テストコードの重複が減り、可読性が向上しています。
  • TestFuzzOneByte という新しいテスト関数が追加されました。このテストは、エンコードされた gob データストリームの各バイトをランダムに(または特定のパターンで)変更し、その破損したデータをデコードしようとすることで、デコーダがパニックを起こさないことを検証します。deferrecover を使用して、デコード中にパニックが発生した場合にそれを捕捉し、テストを失敗させることなくエラーを報告します。このテストは、デコーダの堅牢性を確認するための重要な追加です。

src/pkg/encoding/gob/decode.go

  • decodeInterface 関数に以下のチェックが追加されました。

    	if nr < 0 || nr > 1<<31 { // zero is permissible for anonymous types
    		errorf("invalid type name length %d", nr)
    	}
    	if nr > uint64(state.b.Len()) {
    		errorf("invalid type name length %d: exceeds input size", nr)
    	}
    

    ここで nr は型名の長さを表します。最初のチェックは nr が負の値でないこと、および非常に大きな値でないことを確認します。2番目のチェックは、nr が入力バッファに残っているバイト数 state.b.Len() を超えていないことを確認します。これにより、デコーダが実際に存在しない量のデータを読み込もうとしてパニックを起こすのを防ぎます。

  • decodeValue 関数内の条件式が変更されました。

    	if engine.numInstr == 0 && st.NumField() > 0 &&
    		dec.wireType[wireId] != nil && len(dec.wireType[wireId].StructT.Field) > 0 {
    		name := base.Name()
    		errorf("type mismatch: no fields matched compiling decoder for %s", name)
    	}
    

    dec.wireType[wireId] != nil という条件が追加されました。これにより、dec.wireType[wireId]nil である場合に、その後の len(dec.wireType[wireId].StructT.Field) へのアクセスがパニックを引き起こすのを防ぎます。

src/pkg/encoding/gob/encoder_test.go

  • TestBadData 関数に新しいテストケースが追加されました。
    	// issue 6323.
    	corruptDataCheck("\x04\x24foo", errRange, t)
    
    これは、特定の不正なバイトシーケンス "\x04\x24foo" が与えられた場合に、errRange エラー(範囲エラー)が返されることを期待するテストです。これはIssue 6323で報告された具体的なクラッシュケースの一つを再現し、その修正を検証するためのものです。

コアとなるコードの解説

decode.go の変更点

  • decodeInterface 関数の修正: この関数は、gob ストリームからインターフェース型をデコードする際に、そのインターフェースが保持する具体的な型の名前を読み込みます。型名の長さ nr は、ストリームから読み込まれる最初の情報の一つです。 追加された if nr > uint64(state.b.Len()) のチェックは非常に重要です。これは、デコードしようとしている型名の長さが、現在デコード可能なバッファの残りのサイズを超えていないかを検証します。もし nr がバッファの残りサイズよりも大きい場合、それは不正なデータであり、state.b.Read(b) が要求されたバイト数を読み込めずにパニックを引き起こす可能性があります。このチェックにより、デコーダは不正な長さを検出した時点で安全にエラーを返し、クラッシュを防ぎます。

  • decodeValue 関数の修正: この関数は、gob ストリームから具体的な値をデコードする際に、その値の型情報(wireId で識別される)に基づいてデコードエンジンを適用します。 dec.wireType[wireId] != nil の追加は、デコードしようとしている wireId に対応する型情報が dec.wireType マップに存在するかどうかを確認します。もし存在しない(nil である)場合、それは不正な gob ストリームを示しており、その後の dec.wireType[wireId].StructT.Field へのアクセスは nil ポインタ参照によるパニックを引き起こします。このチェックにより、デコーダは nil アクセスを未然に防ぎ、エラーを適切に処理できるようになります。

これらの変更は、デコーダが入力データの整合性をより厳密にチェックし、不正なデータパターンに対して早期にエラーを返すことで、パニックを回避し、堅牢性を高めることを目的としています。

codec_test.goTestFuzzOneByte

この新しいテストは、ファジング(fuzzing)テストの一種です。ファジングとは、プログラムにランダムまたは半ランダムなデータを大量に与え、予期せぬ動作(クラッシュ、メモリリークなど)を検出するテスト手法です。

TestFuzzOneByte は、まず正常な gob データをエンコードし、そのバイナリ表現を取得します。次に、そのバイナリデータの各バイトを1つずつ変更(フリップ)し、その破損したデータを gob デコーダに渡してデコードを試みます。

			func() {
				defer func() {
					if p := recover(); p != nil {
						t.Errorf("crash for b[%d] ^= 0x%x", i, j)
						panic(p) // 再パニックさせてテストを失敗させる
					}
				}()
				err := NewDecoder(bytes.NewReader(b)).Decode(&e)
				_ = err
			}()

この deferrecover を使ったブロックは、デコード処理中にパニックが発生した場合にそれを捕捉します。パニックが捕捉された場合、t.Errorf でエラーメッセージを記録し、その後 panic(p) で再度パニックを発生させます。これにより、テストフレームワークはパニックを検出し、テストを失敗としてマークすることができます。このテストの目的は、デコーダがどのような不正な入力に対してもパニックを起こさず、エラーを返すか、あるいは安全に処理を終了することを確認することです。

TestFuzzOneByte は、特に「1バイトの変更」という限定的なファジングを行いますが、これにより、デコーダが入力データのわずかな破損に対しても堅牢であることを保証します。

関連リンク

参考にした情報源リンク

  • Go言語 encoding/gob パッケージ公式ドキュメント: https://pkg.go.dev/encoding/gob
  • Go言語におけるパニックとリカバリに関するドキュメント(Go公式ブログなど)
  • Go言語のテストに関するドキュメント(ファジングテストの概念など)
  • encoding/gob の内部実装に関する情報(Goのソースコードや関連する設計ドキュメント)
  • Web search results for "Go encoding/gob" (provided by the tool)