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

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

このコミットは、Go言語の標準ライブラリである encoding/gob パッケージ内の decode.go ファイルに対する変更です。encoding/gob は、Goのデータ構造をバイナリ形式でエンコードおよびデコードするためのメカニズムを提供します。これは、RPC(リモートプロシージャコール)や永続化、ネットワーク経由でのデータ転送など、Goプログラム間でデータをやり取りする際によく利用されます。

decode.go ファイルは、gob ストリームからデータを読み取り、Goの型システムに基づいて適切なGoのデータ構造にデコードするロジックを実装しています。特に、スライスやマップなどの動的なデータ構造のメモリ管理とデコード処理がこのファイルで行われます。

コミット

encoding/gob パッケージにおいて、ガベージコレクタ(GC)がポインタ引数を正しく追跡できるように、decodeSlice 関数内でポインタが uintptr に変換されることで「隠蔽」されるのを防ぐ修正。これにより、GCが誤って参照されているメモリを解放してしまう可能性のあるバグが回避されます。

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

https://github.com/golang/go/commit/6ffd70e7f04e90301ddd8224a90859acb44aed83

元コミット内容

commit 6ffd70e7f04e90301ddd8224a90859acb44aed83
Author: Carl Shapiro <cshapiro@google.com>
Date:   Mon Sep 30 15:54:21 2013 -0700

    encoding/gob: do not hide pointer argument for the garbage collector
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/14154043

変更の背景

このコミットの背景には、Go言語のガベージコレクタ(GC)がポインタをどのように追跡するかという重要な側面があります。GoのGCは、プログラムが使用しなくなったメモリを自動的に解放することで、メモリリークを防ぎ、開発者の負担を軽減します。GCが正しく機能するためには、プログラム内のすべての「到達可能な」オブジェクトを正確に識別し、それらが指すメモリ領域を解放しないようにする必要があります。

問題は、ポインタがGCから「隠蔽」される場合に発生します。Goには unsafe.Pointeruintptr という2つの特殊な型があります。

  • unsafe.Pointer: 任意の型のポインタを保持できる特殊なポインタ型です。Goの型安全性をバイパスしますが、GCは unsafe.Pointer を認識し、それが指すメモリを追跡することができます。
  • uintptr: ポインタのビットパターンを保持するのに十分な大きさの符号なし整数型です。これはポインタ型ではなく、単なる整数です。GCは uintptr の値をポインタとして認識しないため、uintptr に変換されたポインタが指すメモリはGCの追跡対象外となります。

以前の decodeSlice の実装では、unsafe.Pointer 型の引数 p を受け取った後、内部で uintptr(p) のように uintptr に変換し、その uintptr を介してポインタ操作を行っていました。この uintptr への変換が、GCからポインタを「隠蔽」する原因となっていました。もし、この uintptr に変換されたポインタが、その時点でプログラムの他の部分から unsafe.Pointer として参照されていない場合、GCはそれが指すメモリが不要であると判断し、誤って解放してしまう可能性がありました。これは、いわゆる「use-after-free」バグや、プログラムのクラッシュにつながる深刻なメモリ安全性の問題を引き起こす可能性があります。

このコミットは、decodeSlice 関数内でポインタが uintptr に変換されることなく、unsafe.Pointer のまま扱われるようにすることで、GCが常にポインタを追跡できるようにし、この潜在的なメモリ安全性の問題を解決することを目的としています。

前提知識の解説

Goのガベージコレクション (GC) の基本

Goのガベージコレクタは、並行マーク&スイープ方式を採用しています。これは、プログラムの実行と並行してGCが動作し、アプリケーションの停止時間(STW: Stop-The-World)を最小限に抑えることを目指しています。GCの基本的なプロセスは以下の通りです。

  1. マークフェーズ: GCは、プログラムのルート(グローバル変数、スタック上の変数など)から開始し、到達可能なすべてのオブジェクトをマークします。ポインタをたどって、参照されているオブジェクトを次々とマークしていきます。
  2. スイープフェーズ: マークされなかったオブジェクト(つまり、到達不可能なオブジェクト)は、もはや使用されていないと判断され、そのメモリが解放され、再利用可能な状態になります。

GCが正しく機能するためには、プログラム内のすべてのポインタを正確に識別し、それらが指すメモリ領域を追跡できる必要があります。

unsafe.Pointeruintptr の厳密な違いと用途

  • unsafe.Pointer:

    • Goの型システムをバイパスし、任意の型のポインタを保持できる特殊なポインタ型です。
    • *T (任意の型 T へのポインタ) と unsafe.Pointer は相互に変換可能です。
    • unsafe.Pointeruintptr は相互に変換可能です。
    • GCは unsafe.Pointer を認識し、それが指すメモリを追跡します。 これが最も重要な点です。GCは unsafe.Pointer をポインタとして扱い、その参照先をマーク対象とします。
    • 主に、C言語とのFFI(Foreign Function Interface)や、低レベルのメモリ操作、リフレクションAPIの内部実装などで使用されます。
  • uintptr:

    • ポインタのビットパターンを保持するのに十分な大きさの符号なし整数型です。
    • GCは uintptr を単なる整数として扱います。 uintptr の値がメモリアドレスを保持していたとしても、GCはその値をポインタとして解釈し、参照先を追跡することはありません。
    • unsafe.Pointer から uintptr への変換は、ポインタの値を整数として扱うことを意味します。この変換後、GCはそのポインタが指すオブジェクトへの参照を失います。
    • uintptr は、ポインタの算術演算(例: uintptr(ptr) + offset)を行う際に一時的に使用されることがありますが、GCの追跡が必要な期間は unsafe.Pointer のままであるべきです。

reflect.SliceHeader の構造

Goのスライスは、内部的には以下の構造を持つヘッダによって表現されます。

type SliceHeader struct {
    Data uintptr // underlying arrayへのポインタ
    Len  int     // スライスの現在の長さ
    Cap  int     // underlying arrayの容量
}

Data フィールドは uintptr 型であり、これはスライスの基盤となる配列の先頭アドレスを保持します。reflect パッケージは、Goの実行時型情報にアクセスするための機能を提供し、reflect.SliceHeader を使用することで、スライスの内部構造を直接操作することが可能になります。

Goの型システムと unsafe パッケージの役割

Goは通常、厳格な型システムを持ち、型安全性を重視しています。しかし、unsafe パッケージは、この型安全性の制約を意図的に緩和し、低レベルのメモリ操作を可能にします。unsafe パッケージを使用するコードは、コンパイラやGCの保証の一部を放棄するため、非常に注意深く記述する必要があります。このコミットは、unsafe パッケージの誤用がGCの動作に与える影響を示しています。

技術的詳細

このコミットの技術的な核心は、encoding/gob パッケージの decodeSlice 関数が、スライスのデコード処理中にポインタを uintptr に変換していた点にあります。

元のコードでは、decodeSlice 関数は p uintptr という引数を受け取っていました。これは、デコードされたスライスヘッダが格納されるメモリ位置を指すポインタです。しかし、この uintptr はGCによって追跡されません。

さらに、関数内部では、punsafe.Pointer にキャストし、その unsafe.Pointer を介してスライスヘッダのポインタを操作していました。

// 元のコードの一部
func (dec *Decoder) decodeSlice(atyp reflect.Type, state *decoderState, p uintptr, elemOp decOp, elemWid uintptr, indir, elemIndir int, ovfl error) {
    // ...
    if indir > 0 {
        up := unsafe.Pointer(p) // uintptr p を unsafe.Pointer に変換
        if *(*unsafe.Pointer)(up) == nil {
            // Allocate the slice header.
            *(*unsafe.Pointer)(up) = unsafe.Pointer(new([]unsafe.Pointer))
        }
        p = *(*uintptr)(up) // ここで unsafe.Pointer を再び uintptr に変換し、p に代入
    }
    // ...
    hdrp := (*reflect.SliceHeader)(unsafe.Pointer(p)) // ここで uintptr p を unsafe.Pointer に変換して SliceHeader にキャスト
    // ...
}

このコードの問題点は、p = *(*uintptr)(up) の行です。ここで、up (元の punsafe.Pointer にキャストしたもの) が指すメモリの内容(別のポインタ)を uintptr として読み取り、それを再び p に代入しています。この操作により、GCは p が指すメモリ(スライスヘッダ)への参照を失う可能性がありました。なぜなら、puintptr 型であり、GCは uintptr の値をポインタとして追跡しないからです。

修正は、decodeSlice 関数の引数 puintptr から unsafe.Pointer に変更することから始まります。これにより、関数内で p が常にGCによって追跡可能な unsafe.Pointer として扱われるようになります。

// 修正後のコードの一部
func (dec *Decoder) decodeSlice(atyp reflect.Type, state *decoderState, p unsafe.Pointer, elemOp decOp, elemWid uintptr, indir, elemIndir int, ovfl error) {
    // ...
    if indir > 0 {
        // up := unsafe.Pointer(p) // 不要になった
        if *(*unsafe.Pointer)(p) == nil { // p は既に unsafe.Pointer
            // Allocate the slice header.
            *(*unsafe.Pointer)(p) = unsafe.Pointer(new([]unsafe.Pointer))
        }
        // p = *(*uintptr)(up) // この行が削除された
    }
    // ...
    hdrp := (*reflect.SliceHeader)(p) // p は既に unsafe.Pointer なので直接キャスト
    // ...
}

この変更により、p は関数内で常に unsafe.Pointer として扱われ、GCがその参照を適切に追跡できるようになります。結果として、スライスヘッダやその基盤となる配列がGCによって誤って解放されるリスクがなくなります。

また、decOpFor 関数内の decodeSlice の呼び出し箇所も、引数 puintptr(p) から p に変更されています。これは、decodeSlice のシグネチャ変更に合わせて、呼び出し側も unsafe.Pointer を直接渡すように修正されたことを意味します。

この修正は、GoのランタイムとGCの内部動作に関する深い理解に基づいています。unsafe パッケージを使用する際には、GCの動作を考慮し、ポインタがGCから隠蔽されないように細心の注意を払う必要があることを示唆しています。

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

src/pkg/encoding/gob/decode.go ファイルの変更点です。

--- a/src/pkg/encoding/gob/decode.go
+++ b/src/pkg/encoding/gob/decode.go
@@ -654,21 +654,19 @@ func (dec *Decoder) ignoreMap(state *decoderState, keyOp, elemOp decOp) {
 
 // decodeSlice decodes a slice and stores the slice header through p.
 // 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) {
+func (dec *Decoder) decodeSlice(atyp reflect.Type, state *decoderState, p unsafe.Pointer, elemOp decOp, elemWid uintptr, indir, elemIndir int, ovfl error) {
 	nr := state.decodeUint()
 	n := int(nr)
 	if indir > 0 {
-\t\tup := unsafe.Pointer(p)
-\t\tif *(*unsafe.Pointer)(up) == nil {
+\t\tif *(*unsafe.Pointer)(p) == nil {
 \t\t\t// Allocate the slice header.
-\t\t\t*(*unsafe.Pointer)(up) = unsafe.Pointer(new([]unsafe.Pointer))\n+\t\t\t*(*unsafe.Pointer)(p) = unsafe.Pointer(new([]unsafe.Pointer))\n \t\t}\n-\t\tp = *(*uintptr)(up)\n \t}\n \t// Allocate storage for the slice elements, that is, the underlying array,\n \t// if the existing slice does not have the capacity.\n \t// Always write a header at p.\n-\thdrp := (*reflect.SliceHeader)(unsafe.Pointer(p))\n+\thdrp := (*reflect.SliceHeader)(p)\n \tif hdrp.Cap < n {\n \t\thdrp.Data = reflect.MakeSlice(atyp, n, n).Pointer()\n \t\thdrp.Cap = n
@@ -887,7 +885,7 @@ func (dec *Decoder) decOpFor(wireId typeId, rt reflect.Type, name string, inProg
 		\t\telemOp, elemIndir := dec.decOpFor(elemId, t.Elem(), name, inProgress)\n \t\t\tovfl := overflow(name)\n \t\t\top = func(i *decInstr, state *decoderState, p unsafe.Pointer) {\n-\t\t\t\tstate.dec.decodeSlice(t, state, uintptr(p), *elemOp, t.Elem().Size(), i.indir, elemIndir, ovfl)\n+\t\t\t\tstate.dec.decodeSlice(t, state, p, *elemOp, t.Elem().Size(), i.indir, elemIndir, ovfl)\n \t\t\t}\n \n \t\tcase reflect.Struct:

コアとなるコードの解説

decodeSlice 関数のシグネチャ変更

-func (dec *Decoder) decodeSlice(atyp reflect.Type, state *decoderState, p uintptr, elemOp decOp, elemWid uintptr, indir, elemIndir int, ovfl error) {
+func (dec *Decoder) decodeSlice(atyp reflect.Type, state *decoderState, p unsafe.Pointer, elemOp decOp, elemWid uintptr, indir, elemIndir int, ovfl error) {

最も重要な変更点です。decodeSlice 関数の第4引数 p の型が uintptr から unsafe.Pointer に変更されました。これにより、p が指すメモリ領域はGCによって常に追跡されるようになります。

indir > 0 ブロック内の変更

-\t\tup := unsafe.Pointer(p)
-\t\tif *(*unsafe.Pointer)(up) == nil {
+\t\tif *(*unsafe.Pointer)(p) == nil {
 \t\t\t// Allocate the slice header.
-\t\t\t*(*unsafe.Pointer)(up) = unsafe.Pointer(new([]unsafe.Pointer))\n+\t\t\t*(*unsafe.Pointer)(p) = unsafe.Pointer(new([]unsafe.Pointer))\n \t\t}\n-\t\tp = *(*uintptr)(up)\n```
- `up := unsafe.Pointer(p)` の行が削除されました。これは、`p` が既に `unsafe.Pointer` 型であるため、この中間的な変換が不要になったためです。
- `*(*unsafe.Pointer)(up)` が `*(*unsafe.Pointer)(p)` に変更されました。これにより、`p` が直接 `unsafe.Pointer` として使用され、その参照先のポインタが操作されます。
- `p = *(*uintptr)(up)` の行が削除されました。この行が、GCからポインタを隠蔽する主要な原因でした。この削除により、`p` は `unsafe.Pointer` のままであり続け、GCの追跡対象から外れることがなくなりました。

### `reflect.SliceHeader` へのキャスト

```diff
-\thdrp := (*reflect.SliceHeader)(unsafe.Pointer(p))\n+\thdrp := (*reflect.SliceHeader)(p)\n```
`hdrp := (*reflect.SliceHeader)(unsafe.Pointer(p))` が `hdrp := (*reflect.SliceHeader)(p)` に変更されました。`p` が既に `unsafe.Pointer` であるため、`unsafe.Pointer(p)` という冗長なキャストが不要になりました。これにより、`p` が指すメモリ領域が `reflect.SliceHeader` として解釈され、スライスのデータ、長さ、容量が直接操作できるようになります。

### `decOpFor` 関数内の `decodeSlice` 呼び出しの変更

```diff
-\t\t\t\tstate.dec.decodeSlice(t, state, uintptr(p), *elemOp, t.Elem().Size(), i.indir, elemIndir, ovfl)\n+\t\t\t\tstate.dec.decodeSlice(t, state, p, *elemOp, t.Elem().Size(), i.indir, elemIndir, ovfl)\n```
`decOpFor` 関数は、特定の型に対するデコード操作(`decOp`)を生成する役割を担っています。ここで `decodeSlice` を呼び出す際に、以前は `uintptr(p)` として `uintptr` にキャストして渡していましたが、`decodeSlice` のシグネチャ変更に伴い、`p` を直接 `unsafe.Pointer` として渡すように修正されました。これにより、呼び出し側と被呼び出し側の型が一致し、GCの追跡が途切れることなく維持されます。

これらの変更は、Goのガベージコレクタがポインタを正しく追跡できるように、`unsafe.Pointer` の使用方法を厳密に修正したものです。これにより、`encoding/gob` がスライスをデコードする際のメモリ安全性が向上し、潜在的なクラッシュやデータ破損のリスクが軽減されました。

## 関連リンク

*   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言語の `encoding/gob` パッケージのドキュメント: [https://pkg.go.dev/encoding/gob](https://pkg.go.dev/encoding/gob)
*   Goのガベージコレクションに関する公式ブログ記事(古いものも含むが概念は共通):
    *   Go's new GC: [https://go.dev/blog/go15gc](https://go.dev/blog/go15gc)
    *   The Go garbage collector: [https://go.dev/doc/gc-guide](https://go.dev/doc/gc-guide)

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

*   Go言語の公式ドキュメント
*   Go言語のガベージコレクションに関する一般的な知識と技術記事
*   `unsafe.Pointer` と `uintptr` の違いに関するGoコミュニティの議論と解説