[インデックス 19529] ファイルの概要
このコミットは、Goランタイムにおけるdefer
されたnil
関数がパニック時にクラッシュする問題を修正します。具体的には、src/pkg/runtime/stack.c
内のruntime·gostartcallfn
関数が変更され、test/fixedbugs/issue8047b.go
という新しいテストファイルが追加されています。
コミット
commit 36207a91d3c97a9c64984572af89727495310469
Author: Russ Cox <rsc@golang.org>
Date: Thu Jun 12 16:34:36 2014 -0400
runtime: fix defer of nil func
Fixes #8047.
LGTM=r, iant
R=golang-codereviews, r, iant
CC=dvyukov, golang-codereviews, khr
https://golang.org/cl/105140044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/36207a91d3c97a9c64984572af89727495310469
元コミット内容
runtime: fix defer of nil func
Fixes #8047.
LGTM=r, iant
R=golang-codereviews, r, iant
CC=dvyukov, golang-codereviews, khr
https://golang.org/cl/105140044
変更の背景
このコミットは、Goプログラムがパニック状態に陥った際に、nil
関数がdefer
されていた場合に発生するクラッシュを修正するために導入されました。Go言語のdefer
ステートメントは、関数がリターンする直前(パニック時を含む)に実行されるようにスケジュールされた関数呼び出しを登録します。通常、defer
される関数は有効な関数であると期待されますが、何らかの理由でnil
関数がdefer
されるシナリオが発生し得ます。
以前のGoランタイムの挙動では、nil
関数がdefer
された場合、そのnil
関数が実際に呼び出されるまでパニックが遅延("late panicking")される傾向がありました。しかし、この「遅延パニック」の挙動は、特にWindows環境において、プラットフォーム固有の不整合や予期せぬクラッシュを引き起こす可能性がありました。この問題は、GoのIssue 8047として報告され、このコミットはその問題を解決することを目的としています。
この修正の目的は、nil
関数がdefer
された場合でも、ランタイムが安定して動作し、予測可能なパニック処理を行うようにすることです。これにより、異なるオペレーティングシステム間でのGoプログラムの安定性と移植性が向上します。
前提知識の解説
Goにおけるdefer
defer
ステートメントは、Go言語の強力な機能の一つで、現在の関数が実行を終了する直前(return
ステートメントの実行後、またはパニック発生時)に、指定された関数呼び出しをスケジュールします。defer
された関数はLIFO(後入れ先出し)の順序で実行されます。これは、リソースの解放(ファイルクローズ、ロック解除など)や、パニックからの回復(recover
関数と組み合わせて)によく使用されます。
例:
func readFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close() // 関数終了時にファイルが閉じられることを保証
// ファイルの読み込み処理
data, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
return data, nil
}
panic
とrecover
panic
: Goプログラムが回復不能なエラーに遭遇した際に、通常の実行フローを中断するために使用されます。panic
が呼び出されると、現在の関数の実行が停止し、defer
された関数が実行され、その後呼び出し元の関数へとパニックが伝播していきます。最終的に、main
関数までパニックが到達すると、プログラムはクラッシュします。recover
:panic
から回復するために使用されます。recover
はdefer
された関数内でのみ有効です。recover
が呼び出されると、パニックが停止し、recover
を呼び出したdefer
関数が属する関数の通常の実行が再開されます。recover
はパニックの値(panic
に渡された引数)を返します。パニックが発生していないときにrecover
を呼び出すと、nil
が返されます。
例:
func mayPanic() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
fmt.Println("About to panic")
panic("something went wrong")
fmt.Println("This will not be printed")
}
GoランタイムにおけるFuncVal
Goランタイムの内部では、関数はFuncVal
という構造体で表現されます。これは、関数のコードへのポインタ(fn
フィールド)と、その関数がクロージャである場合にキャプチャされた変数へのポインタを含みます。runtime·gostartcallfn
のようなランタイム関数は、このFuncVal
を受け取り、実際の関数呼び出しをセットアップします。
技術的詳細
このコミットが修正する問題は、defer
された関数がnil
である場合に、Goランタイムがそのnil
関数を呼び出そうとした際に発生するクラッシュです。特に、パニックが発生している最中にdefer
された関数が実行されるコンテキストで問題が顕在化しました。
Goランタイムのruntime·gostartcallfn
関数は、新しいゴルーチンを開始したり、defer
された関数を呼び出したりする際に、指定された関数(FuncVal
構造体で表現される)の呼び出しを準備します。この関数は、FuncVal
から実際の関数ポインタfv->fn
を取得してruntime·gostartcall
に渡します。
問題は、fv
(FuncVal
へのポインタ)がnil
である可能性があるにもかかわらず、以前の実装ではfv->fn
を直接参照していた点にありました。fv
がnil
の場合にfv->fn
にアクセスしようとすると、ヌルポインタ参照が発生し、ランタイムがクラッシュします。
このコミットの修正は、runtime·gostartcallfn
関数内でfv
がnil
であるかどうかを明示的にチェックすることで、このヌルポインタ参照を防ぎます。もしfv
がnil
であれば、fn
(呼び出す関数ポインタ)もnil
に設定されます。これにより、runtime·gostartcall
にnil
関数ポインタが渡されることになりますが、これはランタイムが適切に処理できる(例えば、パニックを発生させる)状態であり、不正なメモリ参照によるクラッシュは回避されます。
この変更は、defer
されたnil
関数がパニック時にクラッシュするバグ(Issue 8047)を解決し、Goランタイムの堅牢性を向上させます。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/stack.c
ファイルにあります。
--- a/src/pkg/runtime/stack.c
+++ b/src/pkg/runtime/stack.c
@@ -856,7 +856,12 @@ runtime·newstack(void)\
void
runtime·gostartcallfn(Gobuf *gobuf, FuncVal *fv)
{
- runtime·gostartcall(gobuf, fv->fn, fv);
+ void *fn;
+
+ fn = nil;
+ if(fv != nil)
+ fn = fv->fn;
+ runtime·gostartcall(gobuf, fn, fv);
}
// Maybe shrink the stack being used by gp.
また、この修正を検証するための新しいテストファイルtest/fixedbugs/issue8047b.go
が追加されました。
// 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. Defer setup during panic shouldn't crash for nil defer.
package main
func main() {
defer func() {
recover()
}()
f()
}
func f() {
var g func()
defer g() // g is nil
panic(1)
}
コアとなるコードの解説
src/pkg/runtime/stack.c
の変更
変更されたruntime·gostartcallfn
関数は以下のようになります。
void
runtime·gostartcallfn(Gobuf *gobuf, FuncVal *fv)
{
void *fn; // 関数ポインタを格納する新しい変数
fn = nil; // まずnilで初期化
if(fv != nil) // FuncValポインタがnilでないかチェック
fn = fv->fn; // nilでなければ、FuncValから実際の関数ポインタを取得
runtime·gostartcall(gobuf, fn, fv); // gostartcallを呼び出す
}
以前のコードでは、runtime·gostartcall(gobuf, fv->fn, fv);
と直接fv->fn
にアクセスしていました。fv
がnil
の場合、これはヌルポインタデリファレンスを引き起こし、ランタイムクラッシュの原因となっていました。
新しいコードでは、fn
というローカル変数を導入し、まずnil
で初期化します。次に、fv
がnil
でない場合にのみ、fv->fn
から実際の関数ポインタをfn
に代入します。これにより、fv
がnil
の場合でも安全にruntime·gostartcall
を呼び出すことができ、fn
にはnil
が渡されます。runtime·gostartcall
はnil
関数ポインタを適切に処理できるため、クラッシュが回避されます。
test/fixedbugs/issue8047b.go
の追加
このテストケースは、defer
されたnil
関数がパニック時にクラッシュしないことを確認するために追加されました。
main
関数では、recover
を含むdefer
関数をセットアップし、f()
を呼び出します。これにより、f()
内でパニックが発生してもプログラムがクラッシュしないようにします。f
関数では、g
という関数型の変数を宣言しますが、初期化しないため、その値はnil
になります。defer g()
という行で、nil
関数g
がdefer
されます。- その直後に
panic(1)
が呼び出され、パニックが発生します。
このシナリオでは、パニック処理中にdefer g()
が実行され、ランタイムがnil
関数g
を呼び出そうとします。修正前はここでクラッシュが発生しましたが、修正後はruntime·gostartcallfn
がnil
関数ポインタを安全に処理できるようになり、クラッシュが回避されます。テストの目的は、このコードがクラッシュせずに正常に実行される(run
)ことです。
関連リンク
- Go Change-ID: https://golang.org/cl/105140044
- Go Issue 8047 (直接のリンクは見つかりませんでしたが、コミットメッセージで参照されています)
参考にした情報源リンク
- Go言語の
defer
ステートメントに関する公式ドキュメントやチュートリアル - Go言語の
panic
とrecover
に関する公式ドキュメントやチュートリアル - Goランタイムの内部構造に関する情報(特に
FuncVal
について) - Web検索: "golang.org/cl/105140044"
- Web検索: "Go issue 8047" (直接的な公式Issueトラッカーのリンクは見つかりませんでしたが、関連する議論や文脈を理解するために使用しました)