[インデックス 19055] ファイルの概要
このコミットは、Goランタイムにおけるスタックコピーの堅牢性を向上させるためのものです。特に、cgo
によって生成されたdefer
関数の引数レイアウトに関する問題に対処し、スタックコピー時にそれらが安全に処理されることを保証します。
コミット
commit fc6753c7cd788cbd50cb80e18764541934141e63
Author: Keith Randall <khr@golang.org>
Date: Mon Apr 7 17:40:00 2014 -0700
runtime: make sure associated defers are copyable before trying to copy a stack.
Defers generated from cgo lie to us about their argument layout.
Mark those defers as not copyable.
CL 83820043 contains an additional test for this code and should be
checked in (and enabled) after this change is in.
Fixes bug 7695.
LGTM=rsc
R=golang-codereviews, rsc
CC=golang-codereviews
https://golang.org/cl/84740043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fc6753c7cd788cbd50cb80e18764541934141e63
元コミット内容
runtime: make sure associated defers are copyable before trying to copy a stack.
Defers generated from cgo lie to us about their argument layout.
Mark those defers as not copyable.
CL 83820043 contains an additional test for this code and should be
checked in (and enabled) after this change is in.
Fixes bug 7695.
LGTM=rsc
R=golang-codereviews, rsc
CC=golang-codereviews
https://golang.org/cl/84740043
変更の背景
Goランタイムは、ゴルーチンのスタックを動的に管理します。これには、スタックが不足した場合に新しい、より大きなスタックに内容をコピーする「スタックコピー」のメカニズムが含まれます。このプロセス中、スタック上のポインタは新しいメモリ位置に合わせて調整される必要があります。
問題は、cgo
(GoとC言語のコードを相互運用するためのメカニズム)によって生成されたdefer
関数が、その引数のメモリレイアウトについてランタイムに「嘘をつく」という点にありました。具体的には、cgo
経由で呼び出される関数に関連するdefer
の引数にポインタが含まれている場合、ランタイムがそのポインタの正確な位置を特定するための情報(ポインタマップ)を欠いているか、誤解している可能性がありました。
このような状況でスタックコピーが発生すると、ランタイムはポインタを正しく調整できず、結果として無効なメモリ参照やクラッシュ(セグメンテーション違反など)を引き起こす可能性がありました。この問題はバグ7695として報告されていました。
このコミットは、この問題を解決するために、cgo
によって生成されたdefer
がスタックコピーの対象となる前に、その引数が安全にコピー可能であることを確認するメカニズムを導入します。安全でないと判断された場合、そのdefer
はコピー不可としてマークされ、問題のあるスタックコピーを回避します。
前提知識の解説
Goのdefer
文
defer
文は、Goの関数がリターンする直前(またはパニックが発生した場合)に実行される関数呼び出しをスケジュールするために使用されます。これは、リソースの解放(ファイルのクローズ、ロックの解除など)やクリーンアップ処理に非常に便利です。defer
に渡される引数は、defer
文が評価された時点で評価され、その値が保存されます。
Goのスタック管理(スタックの成長とコピー)
Goのゴルーチンは、比較的小さなスタックで開始し、必要に応じて自動的にスタックを成長させます。スタックが成長する必要がある場合、ランタイムは現在のスタックの内容を、より大きな新しいメモリ領域にコピーします。この「スタックコピー」は透過的に行われますが、コピー中にスタック上のポインタが新しいアドレスに正しく調整されることが不可欠です。
Goのガベージコレクションとポインタマップ
Goはトレース型ガベージコレクタを使用しており、メモリ上の到達可能なオブジェクトを特定するためにポインタを追跡します。ランタイムは、スタックフレームやヒープ上のオブジェクト内のどこにポインタがあるかを知る必要があります。この情報は「ポインタマップ」としてコンパイラによって生成され、ランタイムが利用できるようにします。
FUNCDATA_ArgsPointerMaps
: 関数の引数内のポインタの位置を示すマップ。FUNCDATA_LocalsPointerMaps
: 関数のローカル変数内のポインタの位置を示すマップ。
これらのマップは、ガベージコレクタがスタックをスキャンしたり、スタックコピー中にポインタを調整したりするために不可欠です。
cgo
の概要とGoランタイムとの相互作用
cgo
は、GoプログラムがC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。cgo
を使用すると、Goの型とCの型の間でデータの変換が行われます。cgo
を介してGoの関数がCの関数に渡され、そのCの関数がGoのdefer
を呼び出すような複雑なシナリオでは、Goランタイムが期待するポインタマップ情報が正しく生成されない、または利用できない場合があります。これが「引数レイアウトについて嘘をつく」という問題の根源です。
技術的詳細
このコミットの主要な変更は、runtime/stack.c
内のcopyabletopsegment
関数にあります。この関数は、スタックの最上位セグメントがコピー可能であるかどうかを判断し、コピー可能なフレームの数を返します。コピーできない場合は-1
を返します。
変更前は、defer
がスタック上にある場合、そのコピー可能性はスタックウォーク中に確立されていると見なされていました。しかし、cgo
によって生成されたdefer
の場合、この前提が崩れることがありました。
新しいロジックでは、defer
がスタック上にない(ヒープに割り当てられている)場合、そのdefer
に関連付けられた関数(d->fn
)のポインタマップを明示的にチェックします。
具体的には、以下のチェックが追加されました。
runtime·findfunc((uintptr)fn->fn)
でdefer
が参照する関数の情報を取得します。- その関数に対して
FUNCDATA_ArgsPointerMaps
とFUNCDATA_LocalsPointerMaps
が存在し、かつ有効なポインタマップ情報を含んでいるかを確認します。stackmap = runtime·funcdata(f, FUNCDATA_ArgsPointerMaps);
if(stackmap == nil || stackmap->n <= 0)
stackmap = runtime·funcdata(f, FUNCDATA_LocalsPointerMaps);
if(stackmap == nil || stackmap->n <= 0)
もしこれらのポインタマップがnil
であるか、またはn <= 0
(ポインタ情報がない)である場合、それはランタイムがdefer
の引数内のポインタを安全に識別できないことを意味します。コミットメッセージにあるように、「ポインタマップが提供されない場合、それはポインタマップがCから来たことを意味し、C(特にcgo)は私たちに嘘をつく」ため、このdefer
はコピー不可と判断され、copyabletopsegment
は-1
を返します。
これにより、ランタイムはポインタマップ情報が不完全なcgo
由来のdefer
を含むスタックセグメントのコピーを試みることを避け、潜在的なクラッシュを防ぎます。
また、adjustdefers
関数内のエラーハンドリングも改善され、runtime·findfunc
がnil
を返した場合に、より明確に"can't adjust unknown defer"
というエラーをスローするようになりました。
さらに、stack.c
内のいくつかのデバッグメッセージやコメントが、"stack split"
から"stack overflow"
や"stack growth"
といったより正確な表現に修正されています。これは機能的な変更ではありませんが、コードの可読性と正確性を向上させます。
コアとなるコードの変更箇所
src/pkg/runtime/stack.c
:copyabletopsegment
関数に、defer
の引数とローカル変数のポインタマップをチェックするロジックが追加されました。adjustdefers
関数内のエラーハンドリングが修正されました。- いくつかのデバッグメッセージとコメントが更新されました。
src/pkg/runtime/stack_test.go
:TestDeferPtrs
という新しいテストケースが追加されました。
コアとなるコードの解説
src/pkg/runtime/stack.c
copyabletopsegment
関数
// ...
// Check to make sure all Defers are copyable
for(d = gp->defer; d != nil; d = d->link) {
// ... (deferがスタック上にある場合の既存のチェック)
fn = d->fn;
f = runtime·findfunc((uintptr)fn->fn);
if(f == nil)
return -1; // 関数が見つからない場合はコピー不可
// Check to make sure we have an args pointer map for the defer's args.
// We only need the args map, but we check
// for the locals map also, because when the locals map
// isn't provided it means the ptr map came from C and
// C (particularly, cgo) lies to us. See issue 7695.
stackmap = runtime·funcdata(f, FUNCDATA_ArgsPointerMaps);
if(stackmap == nil || stackmap->n <= 0)
return -1; // 引数ポインタマップがない、または無効な場合はコピー不可
stackmap = runtime·funcdata(f, FUNCDATA_LocalsPointerMaps);
if(stackmap == nil || stackmap->n <= 0)
return -1; // ローカルポインタマップがない、または無効な場合はコピー不可
// ... (FuncValがスタック上にある場合の既存のチェック)
}
// ...
このコードブロックは、defer
がスタック上にない(ヒープに割り当てられている)場合に、そのdefer
に関連する関数のポインタマップを検査します。FUNCDATA_ArgsPointerMaps
とFUNCDATA_LocalsPointerMaps
の両方が存在し、有効なポインタ情報を含んでいることを確認します。どちらか一方が欠けているか無効な場合、そのdefer
は安全にコピーできないと判断され、関数は-1
を返してスタックコピーを中止させます。これは、cgo
がポインタマップについて「嘘をつく」可能性に対処するための重要なガードです。
adjustdefers
関数
// ...
if(f == nil)
runtime·throw("can't adjust unknown defer");
// ...
以前は、runtime·findfunc
がnil
を返した場合に、デバッグ情報を出力してからthrow
していましたが、この変更により、直接throw
するようになりました。これにより、未知のdefer
を調整しようとした際のエラー処理が簡潔になりました。
src/pkg/runtime/stack_test.go
TestDeferPtrs
関数
// TestDeferPtrs tests the adjustment of Defer's argument pointers (p aka &y)
// during a stack copy.
func set(p *int, x int) {
*p = x
}
func TestDeferPtrs(t *testing.T) {
var y int
defer func() {
if y != 42 {
t.Errorf("defer's stack references were not adjusted appropriately")
}
}()
defer set(&y, 42)
growStack()
}
この新しいテストケースは、スタックコピー中にdefer
の引数内のポインタが正しく調整されることを検証します。set(&y, 42)
というdefer
呼び出しでは、y
のアドレス(ポインタ)がdefer
の引数として渡されます。growStack()
が呼び出されると、スタックコピーが発生する可能性があります。スタックコピー後、最初のdefer
が実行されたときにy
の値が42
であることを確認することで、defer
の引数内のポインタが新しいスタック位置に正しく調整されたことを間接的に検証しています。これは、このコミットが解決しようとしている問題(ポインタの不正確な調整)を直接テストするものです。
関連リンク
- Go Change List:
https://golang.org/cl/84740043
- 関連バグ:
bug 7695
(Go issue tracker上の具体的なリンクは不明ですが、コミットメッセージで参照されています)
参考にした情報源リンク
- GitHubコミットページ: https://github.com/golang/go/commit/fc6753c7cd788cbd50cb80e18764541934141e63
- Goの
defer
に関するドキュメント (一般的な情報源) - Goのスタック管理に関するドキュメント (一般的な情報源)
- Goのガベージコレクションとポインタマップに関するドキュメント (一般的な情報源)
cgo
に関するドキュメント (一般的な情報源)- Goのランタイムソースコード (
src/pkg/runtime/stack.c
,src/pkg/runtime/stack_test.go
)