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

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

このコミットは、GoのCgo機能における潜在的なスタック破損バグを実証するためのテストケースを追加するものです。具体的には、Cgoが生成するC関数がGoのdefer文と組み合わされた際に、Goランタイムのスタックコピーメカニズムが誤動作する可能性を指摘しています。この問題は、Goの整数型がCgoによってポインタとして誤って解釈されることに起因し、Goランタイムがスタックを安全にコピーできなくなることで、メモリ破損やクラッシュを引き起こす可能性があります。

コミット

commit dc370995a87a37b43546a9ac3413d533d24e0665
Author: Russ Cox <rsc@golang.org>
Date:   Wed Apr 16 23:06:37 2014 -0400

    test: demo for issue 7695
    
    Cgo writes C function declarations pretending every arg is a pointer.
    If the C function is deferred, it does not inhibit stack copying on split.
    The stack copying code believes the C declaration, possibly misinterpreting
    integers as pointers.
    
    Probably the right fix for Go 1.3 is to make deferred C functions inhibit
    stack copying.
    
    For Go 1.4 and beyond we probably need to make cgo generate Go code
    for 6g here, not C code for 6c.
    
    Update #7695
    
    LGTM=khr
    R=khr
    CC=golang-codereviews
    https://golang.org/cl/83820043

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

https://github.com/golang/go/commit/dc370995a87a37b43546a9ac3413d533d24e0665

元コミット内容

test: demo for issue 7695

Cgo writes C function declarations pretending every arg is a pointer.
If the C function is deferred, it does not inhibit stack copying on split.
The stack copying code believes the C declaration, possibly misinterpreting
integers as pointers.

Probably the right fix for Go 1.3 is to make deferred C functions inhibit
stack copying.

For Go 1.4 and beyond we probably need to make cgo generate Go code
for 6g here, not C code for 6c.

Update #7695

変更の背景

このコミットは、GoのCgo機能とランタイムのスタック管理における重要なバグ、Issue 7695を実証するために作成されました。問題の核心は、CgoがGoとCの間のインターフェースを生成する際に、C側の関数宣言においてすべての引数をポインタとして扱うという挙動にあります。

Goのランタイムは、関数の実行中にスタックが不足した場合、より大きなスタックを割り当てて既存のスタックの内容を新しいスタックにコピーする「スタックコピー」というメカニズムを持っています。この際、ガベージコレクタはスタック上のどの値がポインタであるかを正確に識別し、それらのポインタの指す先を更新する必要があります。

しかし、Cgoを介して呼び出されるC関数がGoのdefer文によって遅延実行される場合、このC関数はスタックコピーを阻害する(つまり、スタックコピー中にその関数が実行されないようにする)ようにマークされていませんでした。その結果、スタックコピーのコードは、Cgoが生成したC側の関数宣言(すべての引数がポインタであると「偽装」している)を信頼してしまい、実際には整数であるGoの引数をポインタとして誤って解釈してしまう可能性がありました。

このような誤解釈は、ガベージコレクタがスタック上の非ポインタ値をポインタとして扱い、その内容をデリファレンスしようとすることで、メモリ破損(セグメンテーション違反など)やプログラムのクラッシュを引き起こす深刻なバグとなります。このコミットは、この脆弱性を再現し、将来のGoバージョンでの修正の必要性を示すためのデモンストレーションとして機能します。

前提知識の解説

Goのスタック管理とスタックコピー

Goランタイムは、各Goroutineに動的にサイズ変更可能なスタックを割り当てます。関数呼び出しによってスタックが不足しそうになると、Goランタイムは自動的に現在のスタックよりも大きな新しいスタックを割り当て、古いスタックの内容(ローカル変数、引数、リターンアドレスなど)を新しいスタックにコピーします。このプロセスは「スタックコピー」と呼ばれ、GoのGoroutineが軽量であることの重要な要素です。スタックコピー中、ガベージコレクタはスタック上のポインタを正確に識別し、それらが指すヒープ上のオブジェクトへの参照を更新する必要があります。

Cgo

Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoのツールです。Cgoを使用すると、GoとCの間のインターフェースが自動的に生成されます。このインターフェースは、Goの型とCの型をマッピングし、関数呼び出しの規約を調整します。Cgoは、GoのコードからC関数を呼び出すためのスタブ関数を生成しますが、この生成プロセスが今回の問題の根源となります。

defer

Goのdefer文は、そのdefer文を含む関数がリターンする直前に、指定された関数呼び出しを遅延実行させるためのものです。deferされた関数は、関数の正常終了時だけでなく、パニックが発生して関数が異常終了する場合でも実行されるため、リソースの解放(ファイルのクローズ、ロックの解除など)によく使用されます。deferされた関数は、そのdefer文が評価された時点の引数の値で実行されます。

ポインタと非ポインタ型

  • ポインタ: メモリ上の特定のアドレスを指し示す変数です。Goでは*Tのように宣言され、CではT*のように宣言されます。ガベージコレクタはポインタを追跡し、参照されているメモリを解放しないようにします。
  • 非ポインタ型: 整数(int, uintptrなど)、浮動小数点数、ブール値、構造体(ポインタを含まないもの)など、直接値を保持する型です。これらの値はガベージコレクタの追跡対象ではありません(ただし、それらがポインタを含む構造体の一部である場合は、構造体全体が追跡対象となります)。

今回の問題では、Goのuintptr型(ポインタを保持できる十分な大きさの符号なし整数型)がCgoによってC側のポインタ型として扱われることが問題となります。uintptrはGo側では整数ですが、Cgoが生成するCのスタブではポインタとして扱われるため、スタックコピー時にGoランタイムが混乱します。

Goのガベージコレクションとポインタスキャン

Goのガベージコレクタ(GC)は、到達可能なオブジェクトを特定し、到達不能なオブジェクトが占めるメモリを解放します。GCは、スタック、レジスタ、グローバル変数など、プログラムのルートセットからポインタをスキャンし、それらが指すオブジェクトをマークします。スタックコピーが発生する際、GCは新しいスタック上のポインタを正確に識別し、それらが指すヒープ上のオブジェクトへの参照を更新する必要があります。もし非ポインタ値がポインタとして誤って識別されると、GCは無効なメモリアドレスをデリファレンスしようとし、クラッシュにつながります。

技術的詳細

このバグは、CgoがGoの関数宣言をCの関数宣言に変換する際の特定の挙動と、Goランタイムのスタックコピーメカニズムの相互作用によって引き起こされます。

  1. Cgoの引数処理の「偽装」: Cgoは、Goの関数がCの関数を呼び出すためのスタブを生成する際、Goの関数に渡された引数をC側で受け取るための構造体を生成します。この構造体は、Goの引数がどのような型であっても、C側ではそれらをポインタの配列として扱うように設計されています。これは、CgoがGoの異なる型の引数をCの単一の汎用的なインターフェースで処理するための一般的なアプローチです。コミットメッセージにある「Cgo writes C function declarations pretending every arg is a pointer」とは、この挙動を指しています。例えば、Go側でfunc F(x int, y float64)と宣言された関数がCgoを介してCから呼び出される場合、C側ではvoid F(struct { void *x_ptr; void *y_ptr; } args)のような形式で引数を受け取るスタブが生成されるイメージです。

  2. deferされたC関数のスタックコピー阻害の欠如: Goランタイムは、スタックコピー中に特定の関数(例えば、Cgoを介してCコードを実行中の関数)が実行されないようにするメカニズムを持っています。これは、CコードがGoランタイムのスタックレイアウトについて知らないため、スタックが移動するとCコードが誤動作する可能性があるためです。しかし、defer文によって遅延実行されるCgo関数は、このスタックコピー阻害の対象外となっていました。つまり、deferされたCgo関数がまだ実行されていない状態でスタックコピーが発生する可能性がありました。

  3. スタックコピーコードの誤解釈: スタックコピーが実行される際、Goランタイムのガベージコレクタはスタック上の値をスキャンし、どれがポインタであるかを判断します。この判断は、Goの型情報に基づいて行われます。しかし、deferされたCgo関数がスタック上に存在する場合、スタックコピーコードはCgoが生成したC側の関数宣言(すべての引数がポインタであると「偽装」している)を「信じて」しまいます。その結果、Go側ではuintptrのような整数として渡された引数が、Cgoのスタブによってポインタとして扱われるため、スタックコピーコードもそれをポインタとして誤って解釈してしまいます。

  4. 結果としてのメモリ破損: 実際には整数であるスタック上の値がポインタとして誤って解釈されると、ガベージコレクタはその「ポインタ」が指す無効なメモリアドレスをデリファレンスしようとします。これにより、セグメンテーション違反(不正なメモリアクセス)が発生し、プログラムがクラッシュします。

この問題に対する提案された修正は以下の通りです。

  • Go 1.3向け: deferされたCgo関数がスタックコピーを阻害するように変更する。これにより、deferされたCgo関数が実行される前にスタックコピーが発生することを防ぎ、誤解釈の機会をなくします。
  • Go 1.4以降向け: CgoがGoコードを生成する際に、Cコード(6c)ではなくGoコード(6g)を生成するように変更する。これにより、CgoがGoとCの間のインターフェースを生成する方法が根本的に変わり、C側の「すべての引数をポインタとして扱う」という偽装が不要になる可能性があります。

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

このコミットは、問題の再現を目的としたテストコードの追加が主です。

  1. misc/cgo/test/backdoor/backdoor.go:

    • func Issue7695(x1, x2, x3, x4, x5, x6, x7, x8 uintptr) という新しい関数宣言が追加されました。この関数はCgoを介してCで実装されることを意図しており、8つのuintptr型の引数を取ります。uintptrはGo側では整数ですが、CgoによってC側ではポインタとして扱われることになります。
  2. misc/cgo/test/backdoor/runtime.c:

    • ·Issue7695 というC関数が追加されました。これは、backdoor.goで宣言されたIssue7695関数のCgoによって生成されるであろうC側のスタブを模倣したものです。
    • このC関数は、struct{void *y[8*sizeof(void*)];}p という単一の構造体引数を取ります。この構造体は、Goから渡される8つのuintptr引数を、それぞれvoid*(汎用ポインタ)の配列として受け取ることを示しています。USED(p);は、引数が使用されていることをコンパイラに伝えるためのマクロです。
  3. misc/cgo/test/issue7695_test.go (新規ファイル):

    • TestIssue7695 という新しいテスト関数が追加されました。
    • このテストの核心は、defer backdoor.Issue7695(1, 0, 2, 0, 0, 3, 0, 4) という行です。ここで、Issue7695関数がdeferされ、整数値(uintptr型)が引数として渡されています。
    • recurse(100) 関数が呼び出されます。この関数は再帰的に自身を呼び出し、各呼び出しでvar x [128]intという大きな配列をスタック上に割り当てます。これにより、スタックが急速に消費され、Goランタイムがスタックコピーを実行せざるを得ない状況を作り出します。
    • このテストの目的は、deferされたIssue7695が実行される前にスタックコピーが発生し、その際にuintptr型の引数がポインタとして誤解釈され、クラッシュが発生するかどうかを検証することです。

コアとなるコードの解説

このテストケースは、GoランタイムのスタックコピーとCgoの相互作用におけるバグを巧みに再現しています。

  1. backdoor.goruntime.cの連携:

    • backdoor.goIssue7695uintptr引数を取るGo関数として宣言されています。これは、GoのコードがCgoを介してC関数を呼び出す際の典型的なパターンです。
    • runtime.c·Issue7695は、CgoがGoのIssue7695のために生成するC側のスタブを模倣しています。ここで重要なのは、Go側でuintptr(整数)として渡された引数が、C側ではvoid*の配列を含む構造体として受け取られる点です。これは、CgoがGoの異なる型の引数をCの汎用的なポインタとして扱うという「偽装」を明確に示しています。
  2. issue7695_test.goでの問題の再現:

    • defer backdoor.Issue7695(...)Issue7695関数は、Goのdeferメカニズムによって遅延実行されます。この関数はCgoを介してCコードを実行するため、本来であればスタックコピー中に実行されるべきではありません。しかし、バグのある状態では、このdeferされたCgo関数はスタックコピーを阻害しません。
    • recurse(100):この再帰関数は、各呼び出しで[128]intという比較的大きな配列をスタック上に割り当てます。100回の再帰呼び出しは、Goランタイムが現在のスタックを使い果たし、より大きな新しいスタックに内容をコピーする「スタックコピー」を強制するのに十分な深さです。
    • バグの発生: recurse関数がスタックコピーをトリガーする際、deferされたIssue7695関数はまだスタック上に存在します。スタックコピーコードは、Cgoが生成したC側の·Issue7695の宣言(すべての引数がポインタであると偽装している)を読み取ります。その結果、Go側でuintptrとして渡された整数値(例:1, 2, 3, 4)が、スタックコピーコードによってポインタとして誤って解釈されます。ガベージコレクタは、これらの誤解釈された「ポインタ」が指す無効なメモリアドレスをスキャンしようとし、結果としてメモリ破損やクラッシュが発生します。

このテストは、CgoとGoランタイムのスタック管理の間の微妙な相互作用が、どのようにして深刻なメモリ安全性の問題を引き起こすかを示すためのものです。

関連リンク

  • Go Issue 7695 (このコミットが参照しているGoのバグトラッカー上のIssue): このコミットメッセージに記載されているUpdate #7695は、Goの公式Issueトラッカー上の問題を参照しています。当時のGoのIssueトラッカーは現在GitHubに移行していますが、この番号はGoプロジェクト内で一意の問題を指します。

参考にした情報源リンク