[インデックス 16275] ファイルの概要
コミット
このコミットは、Goランタイムにおけるbadsignal()
関数でのクラッシュを修正するものです。リンカがtextflag 7
を持つ関数が間接関数呼び出しを行う際に、スプリットスタックのプロローグを生成することがあり、その結果badsignal()
がg
(現在のゴルーチン)を逆参照しようとしてクラッシュする問題に対処しています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f322c786923ebef0c012ff65df8bab767f0d1ace
元コミット内容
runtime: fix crash in badsignal()
The linker can generate split stack prolog when a textflag 7 function
makes an indirect function call. If it happens, badsignal() crashes
trying to dereference g.
Fixes #5337.
R=bradfitz, dave, adg, iant, r, minux.ma
CC=adonovan, golang-dev
https://golang.org/cl/9226043
変更の背景
Goランタイムのbadsignal()
関数は、Goによって作成されていないスレッドでシグナルが受信された場合に呼び出されるエラーハンドリング関数です。この関数内で、シグナル名を出力するためにruntime·findnull
という関数を呼び出していました。
問題は、特定の条件下で発生しました。具体的には、リンカがtextflag 7
(Goのコンパイラとリンカが内部的に使用するフラグで、通常はスタックの切り替えを伴わない関数を示す)を持つ関数が間接関数呼び出し(関数ポインタを介した呼び出しなど)を行う際に、誤ってスプリットスタックのプロローグを生成してしまうことがありました。
スプリットスタックは、Goがスタックサイズを動的に拡張するために使用するメカニズムです。関数呼び出し時にスタックの残りが少ない場合、新しいより大きなスタックに切り替えるためのプロローグコードが実行されます。しかし、badsignal()
のような低レベルのランタイム関数は、このようなスタック切り替えが予期しない動作やクラッシュを引き起こす可能性があるため、スプリットスタックのプロローグが生成されるべきではありませんでした。
この誤ったプロローグが生成された場合、badsignal()
内でruntime·findnull
を呼び出すと、スタック切り替えの試行が発生し、その過程で現在のゴルーチン(g
)のポインタを正しく取得できなくなり、結果としてg
を逆参照しようとした際にクラッシュが発生していました。この問題はGoのIssue #5337として報告されていました。
前提知識の解説
- Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのシステムです。スケジューラ、ガベージコレクタ、メモリ管理、ゴルーチン管理、シグナルハンドリングなど、Goプログラムが動作するために必要な多くの機能を提供します。C言語で書かれた部分が多く、OSとのインタフェースを担当します。
- ゴルーチン (Goroutine): Goにおける軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行できます。各ゴルーチンは独自のスタックを持ち、GoランタイムのスケジューラによってOSスレッドにマッピングされます。
- スプリットスタック (Split Stack): Goが採用しているスタック管理戦略の一つです。Goのゴルーチンは非常に小さな初期スタック(数KB)で開始し、必要に応じてスタックを動的に拡張します。関数呼び出し時にスタックの残りが少ない場合、ランタイムはより大きな新しいスタックフレームを割り当て、そこに切り替えます。これにより、スタックオーバーフローを防ぎつつ、メモリ効率を向上させます。
textflag 7
: Goのコンパイラとリンカが使用する内部的なフラグの一つです。このフラグは、特定の関数がスタックの切り替え(スプリットスタックのプロローグ)を必要としないことを示します。通常、非常に短い関数や、ランタイムの低レベル部分でスタック切り替えが望ましくない関数に適用されます。- 間接関数呼び出し (Indirect Function Call): 関数を直接その名前で呼び出すのではなく、関数ポインタやインターフェースを介して呼び出すことです。この場合、呼び出しのターゲットは実行時に決定されます。
badsignal()
: Goランタイム内の関数で、Goによって管理されていない(Cgoなどによって作成された)スレッドでシグナルが受信された場合に呼び出されます。これは通常、予期せぬシグナルや、Goランタイムが直接処理すべきでないシグナルを捕捉した際に、診断メッセージを出力してプログラムを終了させるために使用されます。g
(Goroutine Pointer): GoランタイムのCコード内で、現在の実行中のゴルーチンを表す構造体へのポインタを指す慣習的な変数名です。多くのランタイム関数は、現在のゴルーチンの状態にアクセスするためにこのg
ポインタを使用します。
技術的詳細
この問題の核心は、badsignal()
関数が非常にデリケートなコンテキストで実行されることです。シグナルハンドラは非同期に呼び出される可能性があり、その実行中にスタックの切り替えのような複雑な操作を行うことは、競合状態やデッドロック、あるいはスタックの破損を引き起こす可能性があります。
以前のコードでは、シグナル名を取得するためにruntime·findnull
という関数を呼び出していました。この呼び出しは、静的なスタックサイズチェックを回避するために、関数ポインタを介して動的に行われていました。
// 変更前のコード
static int32 (*findnull)(byte*) = runtime·findnull;
runtime·write(2, runtime·sigtab[sig].name, findnull((byte*)runtime·sigtab[sig].name));
しかし、リンカがtextflag 7
を持つ関数(badsignal()
自体や、そこから間接的に呼び出される関数)に対して誤ってスプリットスタックのプロローグを生成してしまうバグがありました。runtime·findnull
は文字列の長さを計算するシンプルな関数ですが、これが間接的に呼び出されることで、誤って生成されたスプリットスタックプロローグがトリガーされ、スタック切り替えを試みていました。
このスタック切り替えの試行中に、ランタイムは現在のゴルーチン(g
)の情報を参照しようとしますが、シグナルハンドラの特殊なコンテキストや、スタック切り替えが不完全に終わった状態では、g
ポインタが不正な状態になり、その逆参照がクラッシュを引き起こしていました。
修正は、このruntime·findnull
の呼び出しを完全に回避することです。runtime·findnull
の目的は、ヌル終端文字列の長さを取得することだけなので、これをC言語の標準的なfor
ループによる文字列走査に置き換えることで、スプリットスタックのプロローグが挿入される可能性のある関数呼び出しを排除しました。
// 変更後のコード
int32 len;
for(len = 0; runtime·sigtab[sig].name[len]; len++)
;
runtime·write(2, runtime·sigtab[sig].name, len);
この変更により、badsignal()
関数は、スタック切り替えの危険性なしにシグナル名を出力できるようになり、クラッシュが修正されました。これは、低レベルのランタイムコードでは、予期せぬ副作用を避けるために、可能な限りシンプルで副作用のない操作に限定することの重要性を示しています。
コアとなるコードの変更箇所
変更は、Goランタイムのシグナルハンドリングに関連する以下のOS固有のCファイルで行われました。
src/pkg/runtime/os_darwin.c
src/pkg/runtime/os_freebsd.c
src/pkg/runtime/os_linux.c
src/pkg/runtime/os_netbsd.c
src/pkg/runtime/os_openbsd.c
これらのファイル内のruntime·badsignal
関数において、runtime·findnull
の動的な呼び出しが、手動のfor
ループによる文字列長計算に置き換えられました。
コアとなるコードの解説
各ファイルにおける変更は同一です。以下にsrc/pkg/runtime/os_darwin.c
の例を示します。
変更前:
static int8 badsignal[] = "runtime: signal received on thread not created by Go:\n";
void
runtime·badsignal(int32 sig)
{
if (sig == SIGPROF) {
return; // Ignore SIGPROFs intended for a non-Go thread.
}
runtime·write(2, badsignal, sizeof badsignal - 1);
if (0 <= sig && sig < NSIG) {
// Call runtime·findnull dynamically to circumvent static stack size check.
static int32 (*findnull)(byte*) = runtime·findnull;
runtime·write(2, runtime·sigtab[sig].name, findnull((byte*)runtime·sigtab[sig].name));
}
runtime·write(2, "\n", 1);
runtime·exit(1);
}
変更後:
static int8 badsignal[] = "runtime: signal received on thread not created by Go:\n";
void
runtime·badsignal(int32 sig)
{
+ int32 len; // 文字列長を格納するための変数を追加
+
if (sig == SIGPROF) {
return; // Ignore SIGPROFs intended for a non-Go thread.
}
runtime·write(2, badsignal, sizeof badsignal - 1);
if (0 <= sig && sig < NSIG) {
- // Call runtime·findnull dynamically to circumvent static stack size check.
- static int32 (*findnull)(byte*) = runtime·findnull;
- runtime·write(2, runtime·sigtab[sig].name, findnull((byte*)runtime·sigtab[sig].name));
+ // Can't call findnull() because it will split stack.
+ // findnull()を呼び出すとスプリットスタックが発生する可能性があるため、呼び出さない。
+ for(len = 0; runtime·sigtab[sig].name[len]; len++)
+ ; // ヌル終端までループして文字列長を計算
+ runtime·write(2, runtime·sigtab[sig].name, len); // 計算した長さを使用
}
runtime·write(2, "\n", 1);
runtime·exit(1);
}
変更点の詳細:
int32 len;
の追加: シグナル名の長さを格納するための一時変数が導入されました。runtime·findnull
の呼び出しの削除: 以前は関数ポインタを介してruntime·findnull
を呼び出し、シグナル名の長さを取得していました。この呼び出しが、リンカのバグによりスプリットスタックのプロローグを生成する可能性があったため、削除されました。- 手動での文字列長計算ループの導入:
for(len = 0; runtime·sigtab[sig].name[len]; len++);
というC言語の標準的なループが追加されました。これは、runtime·sigtab[sig].name
が指す文字列のヌル終端文字(\0
)に到達するまでlen
をインクリメントすることで、文字列の長さを安全に計算します。この方法は、追加の関数呼び出しを伴わないため、スプリットスタックの問題を回避できます。 runtime·write
への引数の変更:findnull(...)
の戻り値の代わりに、新しく計算されたlen
変数がruntime·write
関数に渡され、シグナル名が正しく出力されるようにしました。
この変更により、badsignal()
関数は、スタック切り替えの危険性なしにシグナル名を出力できるようになり、Goランタイムの安定性が向上しました。
関連リンク
- Go Change-Id:
https://golang.org/cl/9226043
- Go Issue #5337:
https://github.com/golang/go/issues/5337
(コミットメッセージに記載されている修正対象のIssue)
参考にした情報源リンク
- Go言語の公式ドキュメントおよびソースコード
- GoのIssueトラッカー (GitHub)
- Goのランタイムに関する技術記事や議論 (例: Goのスタック管理、シグナルハンドリングに関する情報)
- C言語における文字列操作の基本