[インデックス 13462] ファイルの概要
このコミットは、Go言語の encoding/gob
パッケージにおけるスライスデコード時の入力サイズチェックの不具合を修正するものです。具体的には、デコード対象のスライスの長さが入力バッファの残りの長さよりも大きい場合に誤ってエラーを報告してしまう問題を解決し、同時に配列デコードのヘルパー関数にも同様の入力枯渇チェックを追加しています。
コミット
commit 1fa32d21a947dbe00217baa5ff22a27dd54fbb9f
Author: Rob Pike <r@golang.org>
Date: Thu Jul 12 10:23:54 2012 -0700
encoding/gob: fix check for short input in slice decode
R=golang-dev, dsymonds, r, nigeltao
CC=golang-dev
https://golang.org/cl/6374059
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1fa32d21a947dbe00217baa5ff22a27dd54fbb9f
元コミット内容
encoding/gob: fix check for short input in slice decode
変更の背景
encoding/gob
パッケージは、Goのデータ構造をバイナリ形式でエンコード・デコードするためのメカニズムを提供します。このコミットが行われた当時、gob
デコーダには、スライスをデコードする際に、入力ストリームの残りのバイト数とデコードしようとしているスライスの要素数(およびその合計バイト数)を比較するロジックに欠陥がありました。
具体的には、gob
ストリームは型定義情報と実際のデータが混在して流れることがあります。デコーダがスライスの長さを読み取った後、すぐにその長さ分のデータが続くとは限りません。その間に、新しい型定義などのメタデータが挿入される可能性があります。
以前の実装では、スライスの長さを読み取った直後に、入力バッファに残っているバイト数とスライスの長さ(要素数)を単純に比較していました。この比較が、もし次に続くのが型定義のようなメタデータであり、そのメタデータがスライスの全要素分のデータよりも短い場合、デコーダは「入力が足りない」と誤判断し、不必要なエラーを発生させていました。これは、実際のデータが後続のチャンクで提供される可能性があるにもかかわらず、デコーダが早まって入力枯渇を検出してしまうというバグでした。
この問題は、特に大きなスライスや、型定義が頻繁に挿入されるような複雑な gob
ストリームを扱う際に顕在化しました。このコミットは、この誤った入力チェックを修正し、デコーダが実際に要素を読み取る直前まで入力枯渇のチェックを遅らせることで、より堅牢なデコード処理を実現することを目的としています。
前提知識の解説
Go言語の encoding/gob
パッケージ
encoding/gob
は、Go言語のプログラム間でGoのデータ構造をシリアライズ(エンコード)およびデシリアライズ(デコード)するための標準パッケージです。ネットワーク経由でのデータ転送や、ファイルへの永続化などに利用されます。
- 自己記述的 (Self-describing):
gob
ストリームは、データだけでなく、そのデータの型情報も含まれています。これにより、受信側は事前に型を知らなくてもデータをデコードできます。 - ストリーム指向:
gob
はストリームとしてデータを扱います。複数のGoの値を連続してエンコード・デコードできます。 - 型定義の伝送:
gob
は、初めて遭遇する型についてはその定義をストリーム内に含めます。これにより、エンコーダとデコーダが異なるプロセスであっても、型の同期が取れます。
gob
のエンコード・デコードプロセス
- 型情報の送信: エンコーダは、初めてエンコードする型の定義をストリームに書き込みます。デコーダはこれを受け取り、内部で型情報を登録します。
- 値の送信: エンコーダは、登録された型情報に基づいて値をバイナリ形式で書き込みます。
- 値の受信: デコーダは、ストリームから値のバイナリデータを読み込み、対応する型情報に基づいてGoのデータ構造に復元します。
このプロセスにおいて、型情報と値のデータがストリーム内で混在するため、デコーダは次に読み込むのが型情報なのか、それとも値のデータなのかを適切に判断する必要があります。
bytes.Buffer
bytes.Buffer
は、Go言語の標準ライブラリ bytes
パッケージに含まれる型で、可変長のバイトシーケンスを扱うためのバッファです。io.Reader
および io.Writer
インターフェースを実装しており、バイトデータの読み書きに便利です。gob
デコーダは、この bytes.Buffer
を内部的に使用して入力ストリームを管理し、残りのバイト数を Len()
メソッドで取得できます。
unsafe.Pointer
と uintptr
Go言語では、ポインタ演算は通常許可されていませんが、unsafe
パッケージを使用することで、低レベルのメモリ操作が可能になります。
unsafe.Pointer
: 任意の型のポインタを保持できる汎用ポインタ型です。C言語のvoid*
に似ています。uintptr
: ポインタの値を整数として表現する型です。ポインタ演算を行う際に一時的にuintptr
にキャストしてアドレスを操作し、再度unsafe.Pointer
にキャストして元の型に戻すといった用途で使われます。gob
デコーダのようなシリアライゼーションライブラリでは、リフレクションと組み合わせて、任意のGoのデータ構造のメモリレイアウトを直接操作するためにこれらの機能が利用されることがあります。
技術的詳細
このコミットの核心は、encoding/gob
デコーダがスライスや配列のデコード時に行う入力バッファの残量チェックのタイミングとロジックの変更です。
変更前(問題点)
decodeSlice
関数には、以下のようなチェックがありました。
func (dec *Decoder) decodeSlice(atyp reflect.Type, state *decoderState, p uintptr, elemOp decOp, elemWid uintptr, indir, elemIndir int, ovfl error) {
nr := state.decodeUint() // スライスの要素数を読み込む
if nr > uint64(state.b.Len()) { // ここで問題のチェックが行われていた
errorf("length of slice exceeds input size (%d elements)", nr)
}
// ... 後続のデコード処理 ...
}
ここで state.b.Len()
は、現在の bytes.Buffer
に残っているバイト数を示します。nr
は gob
ストリームから読み取られたスライスの要素数です。この if
文は、「スライスの要素数 nr
が、現在のバッファに残っているバイト数 state.b.Len()
よりも大きい場合、入力が足りない」と判断していました。
しかし、この判断は誤りでした。gob
ストリームは自己記述的であるため、スライスの要素数を読み取った直後に、そのスライスの全要素分のデータが続くとは限りません。その間に、新しい型定義(gob
の型IDと構造体のフィールド情報など)が挿入される可能性があります。型定義は通常、実際のデータよりもはるかに少ないバイト数で表現されます。
例えば、[100]Z{}
のようなスライスをエンコードし、Z
が初めて登場する型である場合、gob
ストリームは次のような順序でデータを含む可能性があります。
- スライスの長さ (
100
) - 型
Z
の定義 Z
のインスタンスデータ (100個分)
変更前のロジックでは、スライスの長さ 100
を読み取った後、すぐに state.b.Len()
と比較していました。もし state.b.Len()
が型 Z
の定義のバイト数しか含んでおらず、それが 100
要素分のデータよりも少なければ、デコーダは誤って入力枯渇エラーを報告してしまっていました。
変更後(修正)
このコミットでは、decodeSlice
関数から上記の誤ったチェックを削除しました。
func (dec *Decoder) decodeSlice(atyp reflect.Type, state *decoderState, p uintptr, elemOp decOp, elemWid uintptr, indir, elemIndir int, ovfl error) {
nr := state.decodeUint()
// 誤ったチェックが削除された
n := int(nr)
// ... 後続のデコード処理 ...
}
代わりに、より正確な入力枯渇チェックを decodeArrayHelper
関数(スライスと配列の両方の要素をデコードする際に内部的に使用されるヘルパー関数)のループ内に移動しました。
func (dec *Decoder) decodeArrayHelper(state *decoderState, p uintptr, elemOp decOp, elemWid uintptr, length, elemIndir int, ovfl error) {
instr := &decInstr{elemOp, 0, elemIndir, 0, ovfl}
for i := 0; i < length; i++ {
if state.b.Len() == 0 { // 各要素をデコードする直前にチェック
errorf("decoding array or slice: length exceeds input size (%d elements)", length)
}
up := unsafe.Pointer(p)
if elemIndir > 1 {
up = decIndirect(up, elemIndir)
}
// ... 要素のデコード処理 ...
}
}
この変更により、state.b.Len() == 0
のチェックは、各要素を実際にデコードしようとする直前に行われるようになりました。これにより、型定義などのメタデータがストリームに挿入されていても、デコーダはそれらを適切に処理し、実際に要素のデータが必要になったときにのみ入力が枯渇しているかを判断するようになります。これは、gob
ストリームの動的な性質により適したロジックです。
また、encoder_test.go
に追加された Test29ElementSlice
は、このバグを再現し、修正が正しく機能することを確認するためのものです。このテストは、ダミーの型 Z
を登録し、Z
のインスタンスを含む interface{}
のスライスをエンコード・デコードします。これにより、型定義がデータストリームに挿入されるシナリオをシミュレートし、以前のバグが修正されたことを検証します。
コアとなるコードの変更箇所
src/pkg/encoding/gob/decode.go
--- a/src/pkg/encoding/gob/decode.go
+++ b/src/pkg/encoding/gob/decode.go
@@ -562,6 +562,9 @@ func (dec *Decoder) ignoreSingle(engine *decEngine) {
func (dec *Decoder) decodeArrayHelper(state *decoderState, p uintptr, elemOp decOp, elemWid uintptr, length, elemIndir int, ovfl error) {
instr := &decInstr{elemOp, 0, elemIndir, 0, ovfl}
for i := 0; i < length; i++ {
+ if state.b.Len() == 0 {
+ errorf("decoding array or slice: length exceeds input size (%d elements)", length)
+ }
up := unsafe.Pointer(p)
if elemIndir > 1 {
up = decIndirect(up, elemIndir)
@@ -652,9 +655,6 @@ func (dec *Decoder) ignoreMap(state *decoderState, keyOp, elemOp decOp) {
// Slices are encoded as an unsigned length followed by the elements.
func (dec *Decoder) decodeSlice(atyp reflect.Type, state *decoderState, p uintptr, elemOp decOp, elemWid uintptr, indir, elemIndir int, ovfl error) {
nr := state.decodeUint()
- if nr > uint64(state.b.Len()) {
- errorf("length of slice exceeds input size (%d elements)", nr)
- }
n := int(nr)
if indir > 0 {
up := unsafe.Pointer(p)
src/pkg/encoding/gob/encoder_test.go
--- a/src/pkg/encoding/gob/encoder_test.go
+++ b/src/pkg/encoding/gob/encoder_test.go
@@ -813,3 +813,32 @@ func TestMutipleEncodingsOfBadType(t *testing.T) {\n \t\tt.Errorf(\"expected error about no exported fields; got %v\", err)\n \t}\n }\n+\n+// There was an error check comparing the length of the input with the\n+// length of the slice being decoded. It was wrong because the next\n+// thing in the input might be a type definition, which would lead to\n+// an incorrect length check. This test reproduces the corner case.\n+\n+type Z struct {\n+}\n+\n+func Test29ElementSlice(t *testing.T) {\n+\tRegister(Z{})\n+\tsrc := make([]interface{}, 100) // Size needs to be bigger than size of type definition.\n+\tfor i := range src {\n+\t\tsrc[i] = Z{}\n+\t}\n+\tbuf := new(bytes.Buffer)\n+\terr := NewEncoder(buf).Encode(src)\n+\tif err != nil {\n+\t\tt.Fatalf(\"encode: %v\", err)\n+\t\treturn\n+\t}\n+\n+\tvar dst []interface{}\n+\terr = NewDecoder(buf).Decode(&dst)\n+\tif err != nil {\n+\t\tt.Errorf(\"decode: %v\", err)\n+\t\treturn\n+\t\t}\n+}\n```
## コアとなるコードの解説
### `decode.go` の変更
1. **`decodeSlice` 関数からの誤ったチェックの削除**:
`decodeSlice` 関数は、`gob` ストリームからスライスをデコードする主要な関数です。以前は、スライスの要素数 `nr` を読み取った直後に、`state.b.Len()` (入力バッファの残りバイト数) と比較して、入力が足りないかどうかを判断していました。このコミットでは、この `if nr > uint64(state.b.Len())` の行が削除されました。これにより、デコーダはスライスの要素数を読み取った時点では、まだ入力の枯渇を厳密にチェックしなくなりました。
2. **`decodeArrayHelper` 関数への入力枯渇チェックの追加**:
`decodeArrayHelper` 関数は、スライスや配列の個々の要素をループでデコードする際に呼び出されるヘルパー関数です。この関数内のループの先頭に `if state.b.Len() == 0` というチェックが追加されました。
- このチェックは、各要素をデコードする *直前* に行われます。
- `state.b.Len() == 0` は、入力バッファが完全に空であることを意味します。
- もしバッファが空なのに、まだデコードすべき要素が残っている場合(`i < length`)、それは真の入力枯渇であり、`errorf` を呼び出してエラーを報告します。
この変更により、デコーダは、型定義などのメタデータがストリームに挿入されていても、それらを適切に処理し、実際に要素のデータが必要になったときにのみ入力が枯渇しているかを判断するようになります。これは、`gob` ストリームの動的な性質により適した、より正確なエラー検出メカニズムです。
### `encoder_test.go` の変更
1. **`Test29ElementSlice` テストの追加**:
この新しいテストは、以前のバグを再現し、修正が正しく機能することを確認するために追加されました。
- `type Z struct {}`: 空の構造体 `Z` を定義します。これは、`gob` が型定義をストリームに書き込む必要がある新しい型として機能します。
- `Register(Z{})`: `gob` パッケージに型 `Z` を登録します。これにより、`gob` はこの型をエンコード・デコードできるようになります。
- `src := make([]interface{}, 100)`: 100個の要素を持つ `interface{}` のスライスを作成します。`interface{}` を使用することで、異なる型の値を格納でき、`gob` が型情報をストリームに含める必要が生じます。
- `for i := range src { src[i] = Z{} }`: スライスの各要素に `Z{}` のインスタンスを割り当てます。
- `NewEncoder(buf).Encode(src)`: `src` スライスを `bytes.Buffer` にエンコードします。この際、`Z` の型定義がストリームに書き込まれる可能性があります。
- `NewDecoder(buf).Decode(&dst)`: エンコードされたデータを `dst` スライスにデコードします。
このテストの重要な点は、`src` スライスのサイズ (`100`) が、型定義のサイズよりもはるかに大きいことです。以前のバグのあるロジックでは、スライスの長さ (`100`) を読み取った直後に、入力バッファに残っているバイト数(型定義のバイト数のみ)と比較し、誤って入力枯渇エラーを発生させていました。修正後、このテストはエラーなく成功するはずです。
## 関連リンク
- Go言語の `encoding/gob` パッケージのドキュメント: [https://pkg.go.dev/encoding/gob](https://pkg.go.dev/encoding/gob)
- Go言語の `bytes` パッケージのドキュメント: [https://pkg.go.dev/bytes](https://pkg.go.dev/bytes)
- Go言語の `unsafe` パッケージのドキュメント: [https://pkg.go.dev/unsafe](https://pkg.go.dev/unsafe)
- Go言語の `reflect` パッケージのドキュメント: [https://pkg.go.dev/reflect](https://pkg.go.dev/reflect)
## 参考にした情報源リンク
- Goの公式リポジトリ: [https://github.com/golang/go](https://github.com/golang/go)
- Gerrit Code Review (Goプロジェクトのコードレビューシステム): [https://go-review.googlesource.com/](https://go-review.googlesource.com/) (コミットメッセージに記載されている `https://golang.org/cl/6374059` は、このGerritの変更リストへのリンクです。)
- Go言語のブログやドキュメント(`gob` の詳細な動作原理について)