[インデックス 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
を返し、その結果、このテスト自体が「defer
of nil func didn't panic」というメッセージでパニックし、テスト失敗となります。これにより、期待されるパニック挙動が保証されます。 - 問題の
nil
defer
:
これがテストの核心となる行です。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のソースコード分析