[インデックス 19448] ファイルの概要
このコミットは、Goランタイムのsrc/pkg/runtime/stack.c
ファイルに対する変更です。具体的には、スタックコピアがnil
のdefer
関数に遭遇した際に、適切に処理できるようにするための修正が加えられています。
コミット
commit 3d1c3e1e262d8f6d7ad2be52af7d226a6fb88ccf
Author: Keith Randall <khr@golang.org>
Date: Tue May 27 16:26:08 2014 -0700
runtime: stack copier should handle nil defers without faulting.
fixes #8047
LGTM=rsc
R=golang-codereviews, rsc
CC=golang-codereviews
https://golang.org/cl/101800043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3d1c3e1e262d8f6d7ad2be52af7d226a6fb88ccf
元コミット内容
runtime: stack copier should handle nil defers without faulting.
fixes #8047
変更の背景
このコミットは、Goランタイムのスタックコピアがnil
のdefer
関数に遭遇した際に発生する問題を解決するために導入されました。Goのdefer
ステートメントは、関数がリターンする直前に実行される関数呼び出しをスケジュールするために使用されます。通常、defer
される関数は有効な関数ポインタを持つことが期待されますが、特定の状況下でdefer
される関数がnil
になる可能性がありました。
Goランタイムは、ゴルーチンのスタックを動的に管理します。これには、スタックの拡張や縮小、あるいはガベージコレクションやプリエンプションのためにスタックの内容を新しいメモリ領域にコピーする「スタックコピア」の機能が含まれます。このスタックコピーのプロセス中に、defer
レコードも新しいスタック位置に合わせて調整される必要があります。
このコミット以前は、スタックコピアがdefer
レコードを処理する際に、defer
される関数がnil
である場合のチェックが不十分でした。その結果、nil
の関数ポインタを逆参照しようとして、ランタイムパニックやセグメンテーションフォールトといった予期せぬクラッシュを引き起こす可能性がありました。コミットメッセージにあるfixes #8047
は、この問題がGoの旧トラッカーで報告されていたことを示唆しています。
前提知識の解説
Goのdefer
メカニズム
Go言語のdefer
ステートメントは、関数がリターンする直前(return
ステートメントの実行後、またはパニック発生時)に、指定された関数呼び出しを遅延実行させるためのものです。これは、リソースの解放(ファイルのクローズ、ロックの解除など)や、エラーハンドリングの簡素化によく利用されます。
defer
が実行されると、その関数呼び出しと引数は「deferレコード」として現在のゴルーチンのスタック上に記録されます。関数が終了する際に、これらのdeferレコードがLIFO(Last-In, First-Out)順に実行されます。
Goランタイムのスタック管理とスタックコピア
Goのゴルーチンは、可変サイズのスタックを持ちます。初期スタックサイズは比較的小さく、必要に応じて自動的に拡張されます。スタックの拡張が必要になった場合、Goランタイムはより大きな新しいスタック領域を割り当て、古いスタックの内容を新しいスタックにコピーします。このコピー処理を行うのが「スタックコピア」です。
スタックコピアは、単にメモリの内容をコピーするだけでなく、スタック上のポインタ(例えば、ヒープ上のオブジェクトを指すポインタや、他のスタックフレームを指すポインタ)を新しいスタックアドレスに合わせて調整する必要があります。これには、defer
レコードに含まれる関数ポインタや引数ポインタも含まれます。
nil
関数とパニック
Goでは、関数型の変数がnil
である場合、そのnil
関数を呼び出そうとするとランタイムパニックが発生します。これは、有効な関数ポインタがない状態でコードの実行を試みるためです。
var f func()
f() // ここでパニックが発生する
このコミットで修正された問題は、defer
された関数が何らかの理由でnil
になった場合に、スタックコピアがそのnil
関数ポインタを適切に扱えず、パニックを引き起こしていたという点にあります。
技術的詳細
このコミットは、Goランタイムのsrc/pkg/runtime/stack.c
ファイル内の2つの主要な関数、copyabletopsegment
とadjustdefers
に修正を加えています。これらの関数は、スタックコピアがdefer
レコードを処理する際に重要な役割を果たします。
-
copyabletopsegment
関数: この関数は、スタックのコピー可能なセグメントのトップを決定する際に、defer
レコードを走査します。以前は、d->fn
(deferされる関数)がnil
である可能性を考慮せずにd->fn->fn
を直接参照していました。d->fn
がnil
の場合、これはnil
ポインタの逆参照となり、クラッシュの原因となっていました。 -
adjustdefers
関数: この関数は、スタックがコピーされた後に、defer
レコード内のポインタを新しいスタックアドレスに合わせて調整します。ここでも同様に、d->fn
がnil
である可能性が考慮されていませんでした。nil
のd->fn
に対してd->fn->fn
を呼び出すと、クラッシュが発生します。
このコミットの目的は、これらの関数がnil
のdefer
関数に遭遇した場合でも、クラッシュすることなく処理を続行できるようにすることです。nil
のdefer
関数は、最終的に実行される際にパニックを引き起こすことが期待されるため、スタックコピアの段階ではそのパニックを遅延させ、スタックコピー処理自体は正常に完了させる必要があります。
コアとなるコードの変更箇所
src/pkg/runtime/stack.c
ファイルにおいて、以下の変更が行われました。
--- a/src/pkg/runtime/stack.c
+++ b/src/pkg/runtime/stack.c
@@ -344,6 +344,8 @@ copyabletopsegment(G *gp)
if(d->argp < cinfo.stk || cinfo.base <= d->argp)
break; // a defer for the next segment
fn = d->fn;
+ if(fn == nil) // See issue 8047
+ continue;
f = runtime·findfunc((uintptr)fn->fn);
if(f == nil)
return -1;
@@ -552,13 +554,19 @@ adjustdefers(G *gp, AdjustInfo *adjinfo)
}
if(d->argp < adjinfo->oldstk || adjinfo->oldbase <= d->argp)
break; // a defer for the next segment
- f = runtime·findfunc((uintptr)d->fn->fn);
+ fn = d->fn;
+ if(fn == nil) {
+ // Defer of nil function. It will panic when run, and there
+ // aren't any args to adjust. See issue 8047.
+ d->argp += adjinfo->delta;
+ continue;
+ }
+ f = runtime·findfunc((uintptr)fn->fn);
if(f == nil)
runtime·throw("can't adjust unknown defer");
if(StackDebug >= 4)
runtime·printf(" checking defer %s\n", runtime·funcname(f));
// Defer's FuncVal might be on the stack
- fn = d->fn;
if(adjinfo->oldstk <= (byte*)fn && (byte*)fn < adjinfo->oldbase) {
if(StackDebug >= 3)
runtime·printf(" adjust defer fn %s\n", runtime·funcname(f));
コアとなるコードの解説
copyabletopsegment
関数内の変更
fn = d->fn;
+ if(fn == nil) // See issue 8047
+ continue;
f = runtime·findfunc((uintptr)fn->fn);
fn = d->fn;
:defer
レコードd
から関数ポインタfn
を取得します。if(fn == nil)
:取得したfn
がnil
であるかどうかをチェックします。continue;
:もしfn
がnil
であれば、このdefer
レコードはスキップされ、次のdefer
レコードの処理に移ります。これにより、nil
ポインタのfn
に対してfn->fn
を呼び出すことによるクラッシュを防ぎます。nil
のdefer
関数は、実際に実行される際にパニックを引き起こすため、スタックコピーの段階で特別な調整は不要です。
adjustdefers
関数内の変更
- f = runtime·findfunc((uintptr)d->fn->fn);
+ fn = d->fn;
+ if(fn == nil) {
+ // Defer of nil function. It will panic when run, and there
+ // aren't any args to adjust. See issue 8047.
+ d->argp += adjinfo->delta;
+ continue;
+ }
+ f = runtime·findfunc((uintptr)fn->fn);
fn = d->fn;
:ここでもdefer
レコードd
から関数ポインタfn
を取得します。if(fn == nil)
:fn
がnil
であるかをチェックします。d->argp += adjinfo->delta;
:nil
のdefer
関数には調整すべき引数がないため、argp
(引数ポインタ)を新しいスタック位置に合わせて調整するだけで十分です。これは、defer
レコード自体がスタック上に存在し、そのアドレスが移動したためです。continue;
:nil
のdefer
関数はこれ以上処理する必要がないため、次のdefer
レコードに移ります。これにより、nil
ポインタのfn
に対してfn->fn
を呼び出すことによるクラッシュを防ぎます。
これらの変更により、Goランタイムのスタックコピアは、nil
のdefer
関数に遭遇した場合でも堅牢に動作し、予期せぬクラッシュを防ぐことができるようになりました。nil
のdefer
関数は、その後の実行時にGoの通常のパニックメカニズムによって適切に処理されます。
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/3d1c3e1e262d8f6d7ad2be52af7d226a6fb88ccf
- Go Change List (CL):
https://golang.org/cl/101800043
(注: このCLリンクは古い形式であり、現在のGoのコードレビューシステムでは直接アクセスできない可能性があります。) - 関連するIssue:
#8047
(注: このIssue番号はGoの旧トラッカーのものであり、現在のGitHubリポジトリでは直接参照できません。)
参考にした情報源リンク
- Go言語の
defer
ステートメントに関する公式ドキュメントやブログ記事 (一般的な知識として参照) - Goランタイムのスタック管理に関する技術記事 (一般的な知識として参照)
- Goのパニックメカニズムに関する情報 (一般的な知識として参照)
- Goのソースコード (
src/pkg/runtime/stack.c
) - Goの旧IssueトラッカーやCLに関する情報 (直接的な参照はできなかったが、その存在と形式を理解するために参照)
- Goのスタックコピアに関する議論 (一般的な知識として参照)