[インデックス 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.godecodeInterface関数内に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.goTestGobEncodePtrErrorという新しいテスト関数が追加されました。このテストは、修正されたバグを再現し、修正が正しく機能することを確認するためのものです。
--- 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