[インデックス 19517] ファイルの概要
このコミットは、Goランタイムにおけるnilのdefer関数がスタックコピアによって適切に処理されることを保証するためのテストを追加します。具体的には、deferされた関数がnilである場合に、ランタイムがクラッシュするのではなく、予期されたパニックを発生させることを検証します。これは、以前に修正されたバグ(Issue 8047)に対する回帰テストとして機能します。
コミット
commit aa04caa7594506d805f82b7d7abed35a3a8fbec4
Author: Keith Randall <khr@golang.org>
Date: Wed Jun 11 20:34:46 2014 -0400
runtime: add test for issue 8047.
Make sure stack copier doesn't barf on a nil defer.
Bug was fixed in https://golang.org/cl/101800043
This change just adds a test.
Fixes #8047
LGTM=dvyukov, rsc
R=dvyukov, rsc
CC=golang-codereviews
https://golang.org/cl/108840043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/aa04caa7594506d805f82b7d7abed35a3a8fbec4
元コミット内容
runtime: add test for issue 8047.
Make sure stack copier doesn't barf on a nil defer.
Bug was fixed in https://golang.org/cl/101800043
This change just adds a test.
Fixes #8047
LGTM=dvyukov, rsc
R=dvyukov, rsc
CC=golang-codereviews
https://golang.org/cl/108840043
変更の背景
このコミットは、Goランタイムの以前のバグ(Issue 8047)に対するテストを追加するものです。Issue 8047は、「runtime: how should a nil go or nil defer behave?」(nilのgoまたはdeferはどのように振る舞うべきか?)という問題提起から始まりました。これに関連して、Issue 8045「runtime: go of nil func value crashes」(nilの関数値をgoするとクラッシュする)という具体的な問題も存在しました。
これらの問題の核心は、nilの関数ポインタをgoルーチンとして起動したり、deferしたりした場合に、ランタイムがクラッシュ(致命的なエラー)するのか、それともパニック(回復可能なエラー)を発生させるべきかという点でした。Goの設計思想では、予期せぬエラーはパニックとして処理され、recoverによって捕捉されることが期待されます。しかし、特定の条件下では、nilの関数をgoまたはdeferすると、ランタイムがクラッシュし、プログラムが強制終了してしまうという問題がありました。
このバグは、https://golang.org/cl/101800043 で修正されたとコミットメッセージに記載されています。この修正により、nilのdefer関数がスタックコピアによって処理される際に、クラッシュではなくパニックが発生するようになりました。このコミットは、その修正が将来の変更によって回帰しないことを保証するために、専用のテストケースを追加するものです。
前提知識の解説
Goのdefer文
defer文は、関数がリターンする直前(またはパニックが発生して関数が終了する直前)に実行される関数呼び出しをスケジュールするために使用されます。deferされた関数はLIFO(後入れ先出し)の順序で実行されます。これは、リソースの解放(ファイルのクローズ、ロックの解除など)や、パニックの捕捉(recoverと組み合わせて)によく利用されます。
例:
func readFile(filename string) {
f, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
defer f.Close() // 関数が終了する際にf.Close()が実行される
// ファイルの読み込み処理
}
Goのスタックコピアとスタック管理
Goのgoroutineは、可変サイズのスタックを持っています。これは、必要に応じてスタックが自動的に拡大・縮小されることを意味します。スタックが拡大する必要がある場合、Goランタイムは現在のスタックの内容をより大きな新しいスタック領域にコピーします。このプロセスは「スタックコピア」によって行われます。
スタックコピアは、スタック上のすべてのデータ(ローカル変数、関数引数、リターンアドレスなど)を正確にコピーする必要があります。これには、deferされた関数に関する情報も含まれます。もしスタックコピアがdeferされたnil関数を適切に処理できない場合、スタックコピー中に不正なメモリアクセスが発生し、ランタイムがクラッシュする可能性があります。
nil関数とパニック
Goでは、関数型の変数はnilになることがあります。nilの関数を直接呼び出そうとすると、ランタイムパニックが発生します。
var f func()
f() // panic: call of nil func
この挙動は、プログラマが予期せぬnil関数呼び出しを検出し、recoverメカニズムを通じて処理できるようにするために重要です。Issue 8047/8045の背景にあった問題は、goやdeferのコンテキストでnil関数が使用された場合に、この通常のパニック挙動ではなく、より深刻なクラッシュが発生していた点にありました。
技術的詳細
このコミットが追加するテストは、Goランタイムがnilのdefer関数を処理する際の堅牢性を検証します。問題の核心は、defer ((func())(nil))() のようなコードが実行されたときに、スタックコピアがこのnilのdeferエントリをどのように扱うかでした。
Goのスタックコピアは、スタックフレームを走査し、各フレームに関連付けられたdeferレコードを処理します。deferレコードには、deferされた関数のポインタが含まれています。もしこのポインタがnilである場合、スタックコピアはそれを安全にスキップするか、あるいはパニックをトリガーするような方法で処理する必要があります。以前のバグでは、nilの関数ポインタがスタックコピアによって不正にアクセスされ、クラッシュを引き起こしていた可能性があります。
修正されたランタイムでは、defer ((func())(nil))() が実行されると、deferリストにnil関数が追加されます。その後、関数がリターンする際にdeferされた関数が実行されますが、このnil関数が呼び出される直前にランタイムがパニックを発生させるようになります。このパニックは、テストコード内でrecover()によって捕捉され、期待通りの挙動であることを確認します。
テストのstackit(1000)呼び出しは、スタックを深くすることで、スタックコピアが動作する可能性を高めます。これにより、スタックが拡大される際にnilのdeferエントリがコピーされるシナリオをシミュレートし、その過程で問題が発生しないことを確認します。
コアとなるコードの変更箇所
このコミットは、既存のコードを変更するものではなく、新しいテストファイル test/fixedbugs/issue8047.go を追加するものです。
--- /dev/null
+++ test/fixedbugs/issue8047.go
@@ -0,0 +1,29 @@
+// run
+
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Issue 8047. Stack copier shouldn't crash if there
+// is a nil defer.
+
+package main
+
+func stackit(n int) {
+ if n == 0 {
+ return
+ }
+ stackit(n - 1)
+}
+
+func main() {
+ defer func() {
+ // catch & ignore panic from nil defer below
+ err := recover()
+ if err == nil {
+ panic("defer of nil func didn't panic")
+ }
+ }()
+ defer ((func())(nil))()
+ stackit(1000)
+}
コアとなるコードの解説
追加されたテストファイル test/fixedbugs/issue8047.go は、以下の主要な部分で構成されています。
// runディレクティブ: このファイルがGoのテストスイートによって実行されるべきテストであることを示します。- 著作権表示とライセンス: Goプロジェクトの標準的なヘッダです。
- コメント:
// Issue 8047. Stack copier shouldn't crash if there // is a nil defer.は、このテストの目的を明確に示しています。 package main: 実行可能なプログラムであることを示します。func stackit(n int):- この関数は再帰的に自身を呼び出し、スタックを深くするために使用されます。
nが0になるまで再帰を続けます。main関数内でstackit(1000)として呼び出され、深いスタックを生成し、スタックコピアが動作する可能性を高めます。
func main():- 外側の
deferとrecover:
このdefer func() { err := recover() if err == nil { panic("defer of nil func didn't panic") } }()deferブロックは、その後に続くnilのdeferがパニックを発生させることを期待して、そのパニックを捕捉するために設置されています。もしnilのdeferがパニックを発生させなかった場合(つまり、以前のバグのようにクラッシュした場合や、何も起こらなかった場合)、このrecover()はnilを返し、その結果、このテスト自体が「deferof nil func didn't panic」というメッセージでパニックし、テスト失敗となります。これにより、期待されるパニック挙動が保証されます。 - 問題の
nildefer:
これがテストの核心となる行です。defer ((func())(nil))()func() (nil)は、nil値を持つ関数型の変数を表します。このnil関数をdeferすることで、ランタイムがnilのdeferエントリをどのように処理するかをテストします。修正が適用されていれば、このdeferは関数終了時にパニックを発生させるはずです。 stackit(1000): 前述の通り、スタックを深くし、スタックコピアが動作するシナリオを作り出します。これにより、スタックコピアがnilのdeferエントリをコピーする際に問題が発生しないことを確認します。
- 外側の
このテストは、nilのdeferがランタイムをクラッシュさせることなく、適切にパニックを発生させるというGoランタイムの修正された挙動を効果的に検証しています。
関連リンク
- Go Issue 8047: https://github.com/golang/go/issues/8047
- Go Issue 8045: https://github.com/golang/go/issues/8045
- 修正が行われたとされるCL (Change List): https://golang.org/cl/101800043 (コミットメッセージに記載されているリンク)
- このコミットのCL: https://golang.org/cl/108840043
参考にした情報源リンク
- Go Issue Tracker (GitHub)
- Go Change List (Gerrit)
- Go言語の
defer文に関する公式ドキュメントやチュートリアル - Goランタイムのスタック管理に関する技術記事やGoのソースコード分析