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

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

このコミットは、Go言語の標準ライブラリ encoding/gob パッケージにおける、unsafe.Pointer の取り扱いに関する重要な修正です。具体的には、unsafe.Pointeruintptr に変換して隠蔽するのではなく、直接 unsafe.Pointer として扱うように変更することで、型安全性の問題を解消し、潜在的なバグを防ぐことを目的としています。

コミット

commit 77913e9aab3bcf62b2e0be42709149315a9074d9
Author: Carl Shapiro <cshapiro@google.com>
Date:   Tue Dec 3 15:24:27 2013 -0800

    encoding/gob: do not hide an unsafe.Pointer in a uintptr
    
    R=golang-dev, r, rsc
    CC=golang-dev
    https://golang.org/cl/23320044

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

https://github.com/golang/go/commit/77913e9aab3bcf62b2e0be42709149315a9074d9

元コミット内容

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

## 変更の背景

このコミットの背景には、Go言語の `unsafe` パッケージの利用におけるベストプラクティスと、ガベージコレクタ(GC)の正確性に関する考慮事項があります。

Go言語では、ポインタと整数型の間で変換を行うために `unsafe.Pointer` と `uintptr` が提供されています。`unsafe.Pointer` は任意の型のポインタを保持できる特殊なポインタ型であり、GCによって追跡されます。一方、`uintptr` はポインタの値を保持できる整数型であり、GCによって追跡されません。

以前のコードでは、`decodeSlice` 関数がスライスヘッダへのポインタを `uintptr` として受け取っていました。これは、`unsafe.Pointer` を `uintptr` に変換することで、そのポインタが指すメモリ領域がGCによって追跡されなくなるという問題を引き起こす可能性がありました。もしGCがそのメモリ領域を解放してしまった場合、後でその `uintptr` を `unsafe.Pointer` に戻してアクセスしようとすると、不正なメモリ参照(Use-After-Free)が発生し、プログラムがクラッシュしたり、予期せぬ動作を引き起こしたりする可能性がありました。

`encoding/gob` パッケージは、Goのデータ構造をシリアライズ・デシリアライズするためのものであり、内部でリフレクションと `unsafe` パッケージを多用して効率的な処理を実現しています。このような低レベルの操作を行う際には、GCの挙動を正確に理解し、ポインタの取り扱いを慎重に行う必要があります。

このコミットは、`unsafe.Pointer` を `uintptr` に変換して隠蔽するという危険なパターンを排除し、`unsafe.Pointer` を直接使用することで、GCがポインタを正しく追跡できるようにし、メモリ安全性を向上させることを目的としています。

## 前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とパッケージに関する知識が必要です。

1.  **`unsafe` パッケージ**:
    *   Go言語は通常、メモリ安全性を重視し、ポインタ演算を制限しています。しかし、`unsafe` パッケージを使用することで、低レベルのメモリ操作が可能になります。
    *   **`unsafe.Pointer`**: 任意の型のポインタを保持できる特殊なポインタ型です。Goのポインタ型 `*T` と `unsafe.Pointer` の間、および `uintptr` と `unsafe.Pointer` の間で相互変換が可能です。`unsafe.Pointer` はGCによって追跡されるため、それが指すメモリがGCによって誤って解放されることはありません。
    *   **`uintptr`**: ポインタの値を保持できる整数型です。`uintptr` はGCによって追跡されません。つまり、`uintptr` に変換されたポインタが指すメモリが、他の有効なポインタから参照されていない場合、GCはそのメモリを解放してしまう可能性があります。
    *   **`unsafe.Pointer` と `uintptr` の違い**: `unsafe.Pointer` はGCによって追跡される「ポインタ」であり、`uintptr` は単なる「数値」です。`unsafe.Pointer` を `uintptr` に変換すると、GCはそのポインタが指すメモリを追跡しなくなり、メモリ安全性の問題を引き起こす可能性があります。

2.  **`reflect` パッケージ**:
    *   Goの実行時に型情報を検査・操作するためのパッケージです。
    *   **`reflect.Type`**: Goの型の実行時表現です。
    *   **`reflect.Value`**: Goの値の実行時表現です。
    *   **`reflect.SliceHeader`**: スライスの内部表現(データポインタ、長さ、容量)を定義する構造体です。`unsafe` パッケージと組み合わせて、スライスの基盤となる配列やヘッダを直接操作するために使用されます。
        ```go
        type SliceHeader struct {
            Data uintptr // underlying array pointer
            Len  int     // current length
            Cap  int     // capacity
        }
        ```
        `Data` フィールドが `uintptr` であることに注意してください。これは、`reflect` パッケージが低レベルのメモリ操作を可能にするために、ポインタを整数として扱う必要があるためです。しかし、この `uintptr` は `reflect` パッケージの内部で適切に管理されており、GCの追跡対象となります。問題は、開発者が `unsafe.Pointer` を `uintptr` に変換して、GCの追跡から外してしまうことです。

3.  **`encoding/gob` パッケージ**:
    *   Goのデータ構造をバイナリ形式でエンコード(シリアライズ)およびデコード(デシリアライズ)するためのパッケージです。
    *   異なるGoプログラム間でGoの値をやり取りする際や、永続化する際に使用されます。
    *   内部ではリフレクションを多用し、型の情報に基づいて効率的にデータの読み書きを行います。この過程で、スライスやマップなどの可変長データ構造を扱う際に、メモリの確保やポインタの操作が必要になります。

## 技術的詳細

このコミットの核心は、`encoding/gob` パッケージの `decodeSlice` 関数におけるポインタの型変換の変更です。

以前の `decodeSlice` 関数は、デコード対象のスライスヘッダへのポインタを `uintptr` 型の `p` として受け取っていました。関数内でこの `uintptr` を `unsafe.Pointer` に変換し、さらにその `unsafe.Pointer` を `uintptr` に戻すという、不必要な、かつ危険な変換が行われていました。

具体的には、以下のコードが問題でした。

```go
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 -> 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 (ここが問題)
    }
    // ...
    hdrp := (*reflect.SliceHeader)(unsafe.Pointer(p)) // uintptr -> unsafe.Pointer
    // ...
}

このコードの問題点は、p = *(*uintptr)(up) の行です。ここで unsafe.Pointer である up が指す値(別の unsafe.Pointer)を uintptr に変換して p に再代入しています。これにより、p が指すメモリ領域がGCの追跡から外れる可能性が生じます。もし up が指す元の unsafe.Pointer がどこからも参照されなくなった場合、GCはそのメモリを解放してしまうかもしれません。その結果、p が指すアドレスが無効になり、その後の hdrp := (*reflect.SliceHeader)(unsafe.Pointer(p)) で不正なメモリにアクセスするリスクがありました。

このコミットでは、decodeSlice 関数の引数 p の型を uintptr から unsafe.Pointer に変更し、関数内で uintptr への不必要な変換を排除しました。これにより、p が常にGCによって追跡される unsafe.Pointer として扱われるようになり、メモリ安全性が確保されます。

変更後のコードでは、p が最初から unsafe.Pointer であるため、uintptr への変換が不要になり、GCがポインタを正しく追跡できるようになります。

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

変更は src/pkg/encoding/gob/decode.go ファイルの以下の箇所に集中しています。

  1. 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) {
    

    p の型が uintptr から unsafe.Pointer に変更されました。

  2. decodeSlice 関数内のポインタ操作の変更:

    -		up := unsafe.Pointer(p)
    -		if *(*unsafe.Pointer)(up) == nil {
    -			// Allocate the slice header.
    -			*(*unsafe.Pointer)(up) = unsafe.Pointer(new([]unsafe.Pointer))
    -		}
    -		p = *(*uintptr)(up)
    +		if *(*unsafe.Pointer)(p) == nil {
    +			// Allocate the slice header.
    +			*(*unsafe.Pointer)(p) = unsafe.Pointer(new([]unsafe.Pointer))
    +		}
    +		p = *(*unsafe.Pointer)(p)
    

    p が既に unsafe.Pointer であるため、up := unsafe.Pointer(p) の行が削除されました。また、p = *(*uintptr)(up) の行も p = *(*unsafe.Pointer)(p) に変更され、uintptr への不必要な変換がなくなりました。

  3. reflect.SliceHeader へのキャストの変更:

    -	hdrp := (*reflect.SliceHeader)(unsafe.Pointer(p))
    +	hdrp := (*reflect.SliceHeader)(p)
    

    p が既に unsafe.Pointer であるため、unsafe.Pointer(p) という冗長なキャストが削除されました。

  4. decOpFor 関数からの decodeSlice 呼び出しの変更:

    -			op = func(i *decInstr, state *decoderState, p unsafe.Pointer) {
    -				state.dec.decodeSlice(t, state, uintptr(p), *elemOp, t.Elem().Size(), i.indir, elemIndir, ovfl)
    -			}
    +			op = func(i *decInstr, state *decoderState, p unsafe.Pointer) {
    +				state.dec.decodeSlice(t, state, p, *elemOp, t.Elem().Size(), i.indir, elemIndir, ovfl)
    +			}
    

    decodeSlice を呼び出す際に、uintptr(p) としていた部分が p に変更されました。これは、decodeSlice のシグネチャ変更に合わせて、引数の型を正しく渡すためです。

コアとなるコードの解説

このコミットの変更は、Go言語における unsafe.Pointeruintptr の厳密な使い分けの重要性を示しています。

  • decodeSlice 関数のシグネチャ変更:

    • p uintptr から p unsafe.Pointer への変更は、この関数がポインタを扱う際に、GCによって追跡されるべき「ポインタ」として扱うことを明確にしています。これにより、関数内で p が指すメモリがGCによって誤って解放されるリスクがなくなります。
  • if indir > 0 ブロック内の変更:

    • 元のコードでは、p (uintptr) を up (unsafe.Pointer) に変換し、その up が指す値(別の unsafe.Pointer)を uintptr に変換して p に再代入していました。この unsafe.Pointer から uintptr への変換が問題でした。
    • 変更後、p は最初から unsafe.Pointer であるため、up := unsafe.Pointer(p) のような冗長な変換は不要になります。
    • p = *(*unsafe.Pointer)(p) の行は、p が指すメモリ位置に格納されている unsafe.Pointer の値を、再度 p 自身に代入しています。これは、スライスヘッダがまだ割り当てられていない場合に、新しく割り当てられたスライスヘッダのポインタを p に設定するロジックの一部です。重要なのは、この操作が unsafe.Pointer 型のまま行われるため、GCの追跡が維持される点です。
  • hdrp := (*reflect.SliceHeader)(p):

    • reflect.SliceHeader は、スライスの内部構造(データポインタ、長さ、容量)を表現する構造体です。この構造体へのポインタを得るために、unsafe.Pointer*reflect.SliceHeader にキャストしています。
    • 変更前は unsafe.Pointer(p) と明示的にキャストしていましたが、p が既に unsafe.Pointer であるため、このキャストは冗長になり削除されました。これにより、コードがより簡潔になりました。
  • decOpFor 関数からの呼び出し:

    • decodeSlice 関数のシグネチャ変更に伴い、呼び出し元も修正されました。uintptr(p) としていた部分が p に変更され、型の一貫性が保たれています。

この修正は、Go言語の unsafe パッケージを扱う際の重要な教訓を示しています。unsafe.Pointer はGCによって追跡されるポインタであり、uintptr はGCによって追跡されない単なる整数です。unsafe.Pointeruintptr に変換すると、GCがそのメモリを追跡しなくなり、メモリリークやUse-After-Freeなどの深刻なバグにつながる可能性があります。したがって、unsafe.Pointeruintptr に変換する際には、そのポインタがGCによって追跡され続けることを保証する、非常に厳密なルールに従う必要があります。このコミットは、そのルールを破っていたコードを修正し、より安全なポインタの取り扱いを実現しました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード
  • Go言語の unsafe パッケージに関する解説記事 (例: "Go's unsafe package" などで検索)
  • Go言語のガベージコレクションに関する技術ブログや論文
  • Goのコードレビューシステム (Gerrit) の該当コミットページ: https://golang.org/cl/23320044