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

[インデックス 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)のポインタマップを明示的にチェックします。

具体的には、以下のチェックが追加されました。

  1. runtime·findfunc((uintptr)fn->fn)deferが参照する関数の情報を取得します。
  2. その関数に対してFUNCDATA_ArgsPointerMapsFUNCDATA_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·findfuncnilを返した場合に、より明確に"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_ArgsPointerMapsFUNCDATA_LocalsPointerMapsの両方が存在し、有効なポインタ情報を含んでいることを確認します。どちらか一方が欠けているか無効な場合、そのdeferは安全にコピーできないと判断され、関数は-1を返してスタックコピーを中止させます。これは、cgoがポインタマップについて「嘘をつく」可能性に対処するための重要なガードです。

adjustdefers関数

// ...
	if(f == nil)
		runtime·throw("can't adjust unknown defer");
// ...

以前は、runtime·findfuncnilを返した場合に、デバッグ情報を出力してから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)