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

[インデックス 19448] ファイルの概要

このコミットは、Goランタイムのsrc/pkg/runtime/stack.cファイルに対する変更です。具体的には、スタックコピアがnildefer関数に遭遇した際に、適切に処理できるようにするための修正が加えられています。

コミット

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ランタイムのスタックコピアがnildefer関数に遭遇した際に発生する問題を解決するために導入されました。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つの主要な関数、copyabletopsegmentadjustdefersに修正を加えています。これらの関数は、スタックコピアがdeferレコードを処理する際に重要な役割を果たします。

  1. copyabletopsegment関数: この関数は、スタックのコピー可能なセグメントのトップを決定する際に、deferレコードを走査します。以前は、d->fn(deferされる関数)がnilである可能性を考慮せずにd->fn->fnを直接参照していました。d->fnnilの場合、これはnilポインタの逆参照となり、クラッシュの原因となっていました。

  2. adjustdefers関数: この関数は、スタックがコピーされた後に、deferレコード内のポインタを新しいスタックアドレスに合わせて調整します。ここでも同様に、d->fnnilである可能性が考慮されていませんでした。nild->fnに対してd->fn->fnを呼び出すと、クラッシュが発生します。

このコミットの目的は、これらの関数がnildefer関数に遭遇した場合でも、クラッシュすることなく処理を続行できるようにすることです。nildefer関数は、最終的に実行される際にパニックを引き起こすことが期待されるため、スタックコピアの段階ではそのパニックを遅延させ、スタックコピー処理自体は正常に完了させる必要があります。

コアとなるコードの変更箇所

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):取得したfnnilであるかどうかをチェックします。
  • continue;:もしfnnilであれば、このdeferレコードはスキップされ、次のdeferレコードの処理に移ります。これにより、nilポインタのfnに対してfn->fnを呼び出すことによるクラッシュを防ぎます。nildefer関数は、実際に実行される際にパニックを引き起こすため、スタックコピーの段階で特別な調整は不要です。

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)fnnilであるかをチェックします。
  • d->argp += adjinfo->delta;nildefer関数には調整すべき引数がないため、argp(引数ポインタ)を新しいスタック位置に合わせて調整するだけで十分です。これは、deferレコード自体がスタック上に存在し、そのアドレスが移動したためです。
  • continue;nildefer関数はこれ以上処理する必要がないため、次のdeferレコードに移ります。これにより、nilポインタのfnに対してfn->fnを呼び出すことによるクラッシュを防ぎます。

これらの変更により、Goランタイムのスタックコピアは、nildefer関数に遭遇した場合でも堅牢に動作し、予期せぬクラッシュを防ぐことができるようになりました。nildefer関数は、その後の実行時に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のスタックコピアに関する議論 (一般的な知識として参照)