[インデックス 12492] ファイルの概要
このコミットは、Go言語の標準ライブラリである encoding/gob
パッケージにおけるメモリ破損のバグを修正するものです。具体的には、インターフェースのデコード処理において、特定の条件下でポインタの参照が不正になり、メモリが破損する問題に対処しています。この修正により、gob
エンコーディング/デコーディングの堅牢性が向上し、予期せぬランタイムパニック(例: "call of nil func value" や "invalid memory address or nil pointer dereference")が防止されます。
コミット
commit c8b1f85493f9d1d141dd33cb88dfd435e17222b5
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Mar 8 08:53:08 2012 +1100
encoding/gob: fix memory corruption
Fixes #3175.
R=golang-dev, iant, rsc, r
CC=golang-dev
https://golang.org/cl/5758069
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c8b1f85493f9d1d141dd33cb88dfd435e17222b5
元コミット内容
encoding/gob: fix memory corruption
Fixes #3175.
R=golang-dev, iant, rsc, r
CC=golang-dev
https://golang.org/cl/5758069
変更の背景
この変更は、Go言語のIssue #3175「encoding/gob: memory corruption decoding interface」を修正するために行われました。このIssueは、encoding/gob
パッケージがインターフェース型をデコードする際に、特定の状況下でメモリ破損を引き起こす可能性があることを報告していました。具体的には、nil
インターフェース値をデコードしようとした際に、ポインタの参照が適切に処理されず、結果としてランタイムパニック(例えば "call of nil func value" や "invalid memory address or nil pointer dereference")が発生するという問題でした。
gob
はGo言語のデータ構造をシリアライズ/デシリアライズするための形式であり、ネットワーク経由でのデータ転送や永続化によく利用されます。そのため、gob
のデコード処理におけるメモリ破損は、アプリケーションの安定性やセキュリティに深刻な影響を与える可能性があります。このバグは、特にインターフェース型を扱う際に顕在化し、開発者が意図しない動作やクラッシュに直面する原因となっていました。
このコミットは、この根本的なメモリ破損の問題を解決し、encoding/gob
パッケージの信頼性と堅牢性を向上させることを目的としています。
前提知識の解説
このコミットの理解には、以下のGo言語の概念と encoding/gob
パッケージの基本的な知識が必要です。
-
encoding/gob
パッケージ:- Go言語のデータ構造をバイナリ形式にエンコード(シリアライズ)およびデコード(デシリアライズ)するためのパッケージです。
gob
は、Goの型システムと密接に連携しており、構造体、スライス、マップ、インターフェースなど、Goのあらゆるデータ型を扱うことができます。- エンコードされたデータは自己記述的であり、受信側は送信側がどのような型を送信したかを知らなくても、データを正しくデコードできます。
Encoder
とDecoder
という主要な型があり、それぞれデータの書き込みと読み込みを行います。
-
インターフェース (Interface):
- Go言語におけるインターフェースは、メソッドのシグネチャの集合を定義する型です。
- 任意の型がインターフェースで定義されたすべてのメソッドを実装していれば、そのインターフェース型を満たすと見なされます。
- Goのインターフェース値は、内部的に「型 (type)」と「値 (value)」の2つのポインタから構成されます。型ポインタはインターフェースが保持する具体的な型の情報(メソッドセットなど)を指し、値ポインタは具体的なデータ(インスタンス)を指します。
nil
インターフェースは、型ポインタも値ポインタもnil
である状態を指します。
-
reflect
パッケージ:- Goのランタイムリフレクション機能を提供するパッケージです。
- プログラムの実行中に、変数や型の情報を動的に検査・操作することができます。
reflect.Type
はGoの型の情報を表し、reflect.Value
はGoの値を表します。reflect.Type.Kind()
やreflect.Value.InterfaceData()
などのメソッドが利用されます。
-
unsafe
パッケージ:- Goの型安全性をバイパスし、低レベルのメモリ操作を可能にするパッケージです。
unsafe.Pointer
は、任意の型のポインタを保持できる特殊なポインタ型で、C言語のvoid*
に似ています。unsafe.Pointer
を使用することで、異なる型のポインタ間で変換を行ったり、ポインタ演算を行ったりすることが可能になります。ただし、誤用するとメモリ破損やクラッシュを引き起こす非常に危険なパッケージです。*(*[2]uintptr)(unsafe.Pointer(p))
のような記述は、p
が指すメモリ領域を[2]uintptr
型のスライスとして解釈し、その内容を操作することを意味します。これはインターフェースの内部表現(型と値のポインタ)を直接操作するために用いられます。
-
ポインタと間接参照 (Pointers and Indirection):
- Goでは、変数のアドレスを指すポインタを使用します。
*T
は型T
のポインタを表します。 - 間接参照(dereferencing)は、ポインタが指す値にアクセスすることです。
indir
は、decode.go
の文脈では、デコード対象の型がどれだけポインタを介して間接参照されているかを示す深度を表す変数です。例えば、int
はindir=0
、*int
はindir=1
、**int
はindir=2
となります。
- Goでは、変数のアドレスを指すポインタを使用します。
これらの概念を理解することで、decodeInterface
関数がどのようにインターフェース値を処理し、なぜ特定の条件下でメモリ破損が発生したのか、そして今回の修正がどのようにその問題を解決しているのかを深く把握することができます。
技術的詳細
このメモリ破損は、encoding/gob
パッケージの decode.go
ファイルにある decodeInterface
関数内で発生していました。この関数は、gob
ストリームからインターフェース値をデコードする役割を担っています。
問題の核心は、nil
インターフェース値をデコードする際の処理にありました。gob
ストリームで nil
インターフェースが表現されている場合、decodeInterface
関数は、その nil
表現をターゲットのインターフェース値にコピーしようとします。このコピーは、Goのインターフェースの内部表現(型と値の2つのポインタ)を直接操作するために unsafe.Pointer
を使用して行われます。
元のコードでは、if name == ""
のブロック内で nil
インターフェースの処理が行われていました。このブロックの内部では、*(*[2]uintptr)(unsafe.Pointer(p)) = ivalue.InterfaceData()
という行で、nil
インターフェースの内部データ(両方とも nil
ポインタ)が、デコードターゲットのメモリ領域 p
に直接コピーされていました。
しかし、ここで問題となるのが、デコードターゲット p
が指すメモリ領域が、実際にインターフェース値を格納するのに十分なメモリが確保されているかどうか、という点でした。特に、デコード対象のインターフェースがポインタのポインタ(例: **interface{}
)のような多重間接参照を伴う場合、dec.Indirect
関数が既に一部のポインタレベルのメモリを確保している可能性がありますが、最終的なインターフェース値自体が格納されるべきメモリ領域が適切に確保されていないケースがありました。
indir > 0
の条件は、デコード対象の型が少なくとも1つ以上のポインタを介して間接参照されていることを示します。このような場合、p
は最終的なインターフェース値が格納されるべきアドレスを指していますが、そのアドレス自体が有効なメモリを指しているとは限りませんでした。もし p
が指す先にメモリが確保されていなければ、unsafe.Pointer(p)
を介した直接書き込みは、未定義の動作、すなわちメモリ破損を引き起こします。これが、"call of nil func value" や "invalid memory address or nil pointer dereference" といったランタイムパニックの原因でした。
修正は、この indir > 0
の条件下で、nil
インターフェース値をコピーする前に、allocate
関数を呼び出して適切なメモリが確保されていることを保証することです。allocate
関数は、指定された型 ityp
とポインタ p
に基づいて、必要なメモリを確保し、そのポインタを返します。これにより、unsafe.Pointer(p)
を介した書き込みが常に有効なメモリ領域に対して行われるようになり、メモリ破損が防止されます。
この修正は、gob
デコーダが nil
インターフェース値を安全に処理できるようにすることで、Goアプリケーションの安定性を高める重要な変更です。
コアとなるコードの変更箇所
このコミットによるコードの変更は、以下の2つのファイルにわたります。
-
src/pkg/encoding/gob/decode.go
decodeInterface
関数内に3行のコードが追加されました。
--- a/src/pkg/encoding/gob/decode.go +++ b/src/pkg/encoding/gob/decode.go @@ -707,6 +707,9 @@ func (dec *Decoder) decodeInterface(ityp reflect.Type, state *decoderState, p ui if name == "" { // Copy the representation of the nil interface value to the target. // This is horribly unsafe and special. + if indir > 0 { + p = allocate(ityp, p, 1) // All but the last level has been allocated by dec.Indirect + } *(*[2]uintptr)(unsafe.Pointer(p)) = ivalue.InterfaceData() return }
-
src/pkg/encoding/gob/gobencdec_test.go
TestGobEncodePtrError
という新しいテスト関数が追加されました。このテストは、修正されたバグを再現し、修正が正しく機能することを確認するためのものです。
--- a/src/pkg/encoding/gob/gobencdec_test.go +++ b/src/pkg/encoding/gob/gobencdec_test.go @@ -573,3 +573,22 @@ func TestGobEncodeIsZero(t *testing.T) { t.Fatalf("%v != %v", x, y) } } + +func TestGobEncodePtrError(t *testing.T) { + var err error + b := new(bytes.Buffer) + enc := NewEncoder(b) + err = enc.Encode(&err) + if err != nil { + t.Fatal("encode:", err) + } + dec := NewDecoder(b) + err2 := fmt.Errorf("foo") + err = dec.Decode(&err2) + if err != nil { + t.Fatal("decode:", err) + } + if err2 != nil { + t.Fatalf("expected nil, got %v", err2) + } +}
コアとなるコードの解説
src/pkg/encoding/gob/decode.go
の変更
追加されたコードは以下の3行です。
if indir > 0 {
p = allocate(ityp, p, 1) // All but the last level has been allocated by dec.Indirect
}
if indir > 0
: この条件は、デコード対象のインターフェースがポインタを介して間接参照されている場合に真となります。indir
は、decodeInterface
関数が処理している現在のポインタの深さを示します。indir > 0
は、例えば*interface{}
や**interface{}
のような型をデコードしていることを意味します。p = allocate(ityp, p, 1)
:allocate
関数は、Goのランタイムが値を格納するために必要なメモリを確保する内部ヘルパー関数です。ityp
は、デコードしようとしているインターフェースの具体的な型(例えばerror
インターフェースが保持する*bytes.Buffer
の型など)を表します。p
は、現在デコード中の値が格納されるべきメモリ位置へのポインタです。1
は、確保する要素の数を示します。ここでは、インターフェース値自体を格納するための単一の要素を確保します。- この行の目的は、
nil
インターフェース値をコピーする前に、p
が指すメモリ領域が実際に有効なメモリを指していることを保証することです。多重間接参照の場合、dec.Indirect
が既に上位のポインタレベルのメモリを確保している可能性がありますが、最終的なインターフェース値が格納される場所が未確保である可能性がありました。このallocate
の呼び出しにより、その最終的な格納場所が確実に確保されます。
- コメント
// All but the last level has been allocated by dec.Indirect
: このコメントは、dec.Indirect
関数が既にポインタチェーンの大部分のメモリを確保しているが、最後のレベル(つまり、インターフェース値自体が格納される場所)はまだ確保されていない可能性があることを説明しています。したがって、allocate
を呼び出してこの最後のレベルのメモリを確保する必要があります。
この修正により、nil
インターフェース値がデコードされる際に、unsafe.Pointer(p)
を介したメモリへの直接書き込みが、常に適切に確保されたメモリ領域に対して行われるようになります。これにより、不正なメモリアクセスやメモリ破損が防止され、ランタイムパニックの発生が回避されます。
src/pkg/encoding/gob/gobencdec_test.go
の変更
TestGobEncodePtrError
テスト関数は、このメモリ破損バグを具体的に再現し、修正が正しく機能することを確認するために追加されました。
テストの主な流れは以下の通りです。
var err error
:nil
のerror
インターフェース変数を宣言します。enc.Encode(&err)
: このnil
のerror
インターフェース変数のアドレスをgob
エンコーダに渡してエンコードします。これにより、gob
ストリームにはnil
インターフェースの表現が書き込まれます。err2 := fmt.Errorf("foo")
: デコードのターゲットとして、nil
ではないerror
インターフェース変数err2
を初期化します。dec.Decode(&err2)
:gob
ストリームからデコードし、その結果をerr2
のアドレスに書き込みます。if err2 != nil { t.Fatalf("expected nil, got %v", err2) }
: デコード後、err2
がnil
になっていることを検証します。
このテストのポイントは、nil
インターフェースをエンコードし、それを nil
ではないインターフェース変数(err2
)にデコードしようとすることです。修正前は、この操作がメモリ破損を引き起こし、err2
が正しく nil
に設定されなかったり、ランタイムパニックが発生したりする可能性がありました。修正後は、allocate
の呼び出しによって適切なメモリが確保され、err2
が正しく nil
にデコードされることが期待されます。このテストは、その期待される動作を検証しています。
関連リンク
- Go Issue #3175: https://github.com/golang/go/issues/3175
- Go CL 5758069: https://golang.org/cl/5758069