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

[インデックス 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
}

panicrecover

  • panic: Goプログラムが回復不能なエラーに遭遇した際に、通常の実行フローを中断するために使用されます。panicが呼び出されると、現在の関数の実行が停止し、deferされた関数が実行され、その後呼び出し元の関数へとパニックが伝播していきます。最終的に、main関数までパニックが到達すると、プログラムはクラッシュします。
  • recover: panicから回復するために使用されます。recoverdeferされた関数内でのみ有効です。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に渡します。

問題は、fvFuncValへのポインタ)がnilである可能性があるにもかかわらず、以前の実装ではfv->fnを直接参照していた点にありました。fvnilの場合にfv->fnにアクセスしようとすると、ヌルポインタ参照が発生し、ランタイムがクラッシュします。

このコミットの修正は、runtime·gostartcallfn関数内でfvnilであるかどうかを明示的にチェックすることで、このヌルポインタ参照を防ぎます。もしfvnilであれば、fn(呼び出す関数ポインタ)もnilに設定されます。これにより、runtime·gostartcallnil関数ポインタが渡されることになりますが、これはランタイムが適切に処理できる(例えば、パニックを発生させる)状態であり、不正なメモリ参照によるクラッシュは回避されます。

この変更は、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にアクセスしていました。fvnilの場合、これはヌルポインタデリファレンスを引き起こし、ランタイムクラッシュの原因となっていました。

新しいコードでは、fnというローカル変数を導入し、まずnilで初期化します。次に、fvnilでない場合にのみ、fv->fnから実際の関数ポインタをfnに代入します。これにより、fvnilの場合でも安全にruntime·gostartcallを呼び出すことができ、fnにはnilが渡されます。runtime·gostartcallnil関数ポインタを適切に処理できるため、クラッシュが回避されます。

test/fixedbugs/issue8047b.goの追加

このテストケースは、deferされたnil関数がパニック時にクラッシュしないことを確認するために追加されました。

  • main関数では、recoverを含むdefer関数をセットアップし、f()を呼び出します。これにより、f()内でパニックが発生してもプログラムがクラッシュしないようにします。
  • f関数では、gという関数型の変数を宣言しますが、初期化しないため、その値はnilになります。
  • defer g()という行で、nil関数gdeferされます。
  • その直後にpanic(1)が呼び出され、パニックが発生します。

このシナリオでは、パニック処理中にdefer g()が実行され、ランタイムがnil関数gを呼び出そうとします。修正前はここでクラッシュが発生しましたが、修正後はruntime·gostartcallfnnil関数ポインタを安全に処理できるようになり、クラッシュが回避されます。テストの目的は、このコードがクラッシュせずに正常に実行される(run)ことです。

関連リンク

  • Go Change-ID: https://golang.org/cl/105140044
  • Go Issue 8047 (直接のリンクは見つかりませんでしたが、コミットメッセージで参照されています)

参考にした情報源リンク

  • Go言語のdeferステートメントに関する公式ドキュメントやチュートリアル
  • Go言語のpanicrecoverに関する公式ドキュメントやチュートリアル
  • Goランタイムの内部構造に関する情報(特にFuncValについて)
  • Web検索: "golang.org/cl/105140044"
  • Web検索: "Go issue 8047" (直接的な公式Issueトラッカーのリンクは見つかりませんでしたが、関連する議論や文脈を理解するために使用しました)