[インデックス 16905] ファイルの概要
このコミットは、GoランタイムのFreeBSDビルドにおける問題を修正するものです。具体的には、src/pkg/runtime/os_freebsd.c
ファイル内のruntime·futexsleep
関数におけるスタックオーバーフローの問題に対処しています。このファイルは、GoランタイムがFreeBSDオペレーティングシステムと対話するための低レベルなシステムコールやプリミティブを実装しています。
コミット
runtime: fix freebsd build
notetsleep: nosplit stack overflow
120 assumed on entry to notetsleep
80 after notetsleep uses 40
72 on entry to runtime.futexsleep
16 after runtime.futexsleep uses 56
8 on entry to runtime.printf
-16 after runtime.printf uses 24
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/12047043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d83688165aabfe31f3501a302f6a8b94393439f3
元コミット内容
runtime: fix freebsd build
notetsleep: nosplit stack overflow
120 assumed on entry to notetsleep
80 after notetsleep uses 40
72 on entry to runtime.futexsleep
16 after runtime.futexsleep uses 56
8 on entry to runtime.printf
-16 after runtime.printf uses 24
変更の背景
このコミットの背景には、GoランタイムがFreeBSD上で動作する際のスタック管理に関する深い問題があります。Goランタイムは、ゴルーチン(goroutine)と呼ばれる軽量なスレッドを効率的に管理するために、独自のスタック管理メカニズムを持っています。通常、ゴルーチンのスタックは必要に応じて動的に拡張されます。しかし、ランタイムの非常に低レベルな部分、特にシステムコールをラップするような関数では、スタックの拡張("stack split")が許可されない場合があります。このような関数はnosplit
関数と呼ばれ、コンパイル時に固定された小さなスタック領域しか使用できません。
コミットメッセージに記載されているスタック使用量の数値は、この問題の核心を示しています。
notetsleep
へのエントリで120バイトのスタックが利用可能と仮定。notetsleep
が40バイト使用した後、80バイトが残る。runtime.futexsleep
へのエントリで72バイトが利用可能。runtime.futexsleep
が56バイト使用した後、16バイトが残る。runtime.printf
へのエントリで8バイトが利用可能。runtime.printf
が24バイト使用した後、スタックが-16バイトになる(つまり、24バイト必要だが8バイトしかなく、16バイトのオーバーフローが発生)。
このシーケンスは、runtime.futexsleep
関数内でruntime.printf
を呼び出した際に、runtime.printf
が必要とするスタック領域が、runtime.futexsleep
に割り当てられた残りのスタック領域を超過し、スタックオーバーフローを引き起こしていたことを示しています。runtime.futexsleep
のような低レベル関数は、nosplit
としてマークされている可能性が高く、スタックの動的な拡張ができないため、この問題が顕在化しました。
スタックオーバーフローは、プログラムのクラッシュや予期せぬ動作につながるため、ランタイムの安定性にとって致命的な問題です。特に、デバッグ目的で低レベルなエラーパスにprintf
のような関数を使用すると、そのprintf
自体がスタックを使いすぎて問題を悪化させるという、皮肉な状況が発生することがあります。
前提知識の解説
Goランタイム
Goランタイムは、Goプログラムの実行を管理する非常に重要なコンポーネントです。これには、ゴルーチンのスケジューリング、メモリ管理(ガベージコレクションを含む)、チャネル操作、システムコールインターフェースなどが含まれます。Goプログラムは、OSのネイティブスレッド上で動作しますが、Goランタイムがこれらのスレッドを抽象化し、より軽量なゴルーチンを提供します。
notetsleep
notetsleep
は、Goランタイム内部で使用される低レベルな同期プリミティブです。これは、特定のイベントが発生するまでゴルーチンをスリープさせるために使用されます。例えば、ネットワークI/Oの完了や、他のゴルーチンからの通知を待つ場合などに利用されます。これはruntime.note
という構造体と関連しており、runtime.notewakeup
によって通知されます。
futexsleep
futexsleep
は、LinuxやFreeBSDなどのUnix系OSで提供されるfutex
(Fast Userspace Mutex)システムコールをGoランタイムがラップしたものです。futex
は、ユーザー空間での高速なロックや同期メカニズムを実装するために使用される低レベルなカーネルプリミティブです。futexsleep
は、特定の条件が満たされるまで現在のスレッド(GoランタイムのコンテキストではM、つまりOSスレッド)をスリープさせるために使用されます。
umtx_wait
umtx_wait
は、FreeBSDオペレーティングシステムに特有のシステムコールです。umtx
(User-space Mutex)は、ユーザー空間で効率的な同期プリミティブを実装するためのメカニズムを提供します。umtx_wait
は、特定のumtx
アドレスで待機し、他のスレッドがumtx_wake
を呼び出すまでブロックします。これは、futex
と同様に、低レベルな同期操作に使用されます。
nosplit
関数
Goコンパイラは、関数のスタックフレームサイズを分析し、必要に応じてスタックを動的に拡張するコード("stack split check")を生成します。しかし、ランタイムの非常に低レベルな関数、特にスタックの拡張自体が問題を引き起こす可能性がある場所(例えば、スタックの拡張処理自体がスタックを必要とする場合や、割り込みハンドラなど)では、このスタック拡張チェックを無効にする必要があります。このような関数はnosplit
としてマークされ、コンパイル時に固定された小さなスタック領域しか使用できません。nosplit
関数内で、その関数に割り当てられたスタックサイズを超えるスタックを使用しようとすると、スタックオーバーフローが発生します。
技術的詳細
このコミットの技術的詳細は、Goランタイムのデバッグ出力メカニズムとスタック管理の間の相互作用にあります。
問題は、runtime·futexsleep
関数内のエラーパスでruntime·printf
が呼び出されていたことに起因します。runtime·printf
は、フォーマット文字列の解析、引数の処理、文字列の構築など、比較的多くのスタックを消費する可能性があります。コミットメッセージのスタック使用量の分析が示すように、runtime·futexsleep
が呼び出された時点で利用可能なスタックは72バイトでしたが、runtime·futexsleep
自身が56バイトを使用し、残りは16バイトでした。この16バイトの残りのスタックでruntime·printf
を呼び出すと、runtime·printf
が24バイトを必要とするため、8バイトのスタック不足が発生し、結果としてスタックオーバーフローを引き起こしていました。
Goランタイムの低レベルな部分では、スタックの制約が非常に厳しくなります。特に、futexsleep
のようなシステムコールを直接扱う関数は、OSのカーネルスタックとユーザー空間スタックの間の遷移を伴うため、非常に注意深くスタックを使用する必要があります。このような関数は通常nosplit
としてマークされており、スタックの動的な拡張ができません。
この問題を解決するために、開発者はruntime·printf
の使用を避け、よりスタック消費の少ない低レベルなプリント関数に置き換えました。具体的には、runtime·prints
(文字列出力)、runtime·printpointer
(ポインタ値出力)、runtime·printint
(整数値出力)を使用することで、必要なデバッグ情報を出力しつつ、スタック使用量を最小限に抑えることができました。これらの関数は、runtime·printf
よりも単純な実装であり、スタックフットプリントが小さいため、nosplit
関数内での使用に適しています。
この変更は、Goランタイムの堅牢性を高め、FreeBSD上での安定した動作を保証するために不可欠でした。低レベルなデバッグ出力であっても、ランタイムの安定性を損なわないように細心の注意を払う必要があることを示しています。
コアとなるコードの変更箇所
diff --git a/src/pkg/runtime/os_freebsd.c b/src/pkg/runtime/os_freebsd.c
index 7987a58340..98de6dc346 100644
--- a/src/pkg/runtime/os_freebsd.c
+++ b/src/pkg/runtime/os_freebsd.c
@@ -61,7 +61,13 @@ runtime·futexsleep(uint32 *addr, uint32 val, int64 ns)
return;
fail:
- runtime·printf("umtx_wait addr=%p val=%d ret=%d\n", addr, val, ret);
+ runtime·prints("umtx_wait addr=");
+ runtime·printpointer(addr);
+ runtime·prints(" val=");
+ runtime·printint(val);
+ runtime·prints(" ret=");
+ runtime·printint(ret);
+ runtime·prints("\n");
*(int32*)0x1005 = 0x1005;
}
コアとなるコードの解説
変更はsrc/pkg/runtime/os_freebsd.c
ファイルのruntime·futexsleep
関数内のfail:
ラベルの直下にあります。
変更前:
fail:
runtime·printf("umtx_wait addr=%p val=%d ret=%d\n", addr, val, ret);
*(int32*)0x1005 = 0x1005;
ここでは、umtx_wait
システムコールの呼び出しが失敗した場合に、デバッグ情報をruntime·printf
を使って標準エラー出力(またはそれに相当するランタイムのデバッグ出力)に表示しようとしていました。前述の通り、このruntime·printf
の呼び出しがスタックオーバーフローの原因となっていました。
変更後:
fail:
runtime·prints("umtx_wait addr=");
runtime·printpointer(addr);
runtime·prints(" val=");
runtime·printint(val);
runtime·prints(" ret=");
runtime·printint(ret);
runtime·prints("\n");
*(int32*)0x1005 = 0x1005;
変更後では、単一のruntime·printf
呼び出しが、複数のよりプリミティブなプリント関数に置き換えられています。
runtime·prints("umtx_wait addr=");
: 固定文字列 "umtx_wait addr=" を出力します。runtime·printpointer(addr);
: ポインタaddr
の値を16進数形式で出力します。runtime·prints(" val=");
: 固定文字列 " val=" を出力します。runtime·printint(val);
: 整数val
の値を10進数形式で出力します。runtime·prints(" ret=");
: 固定文字列 " ret=" を出力します。runtime·printint(ret);
: 整数ret
の値を10進数形式で出力します。runtime·prints("\n");
: 改行を出力します。
これらのプリミティブな関数は、それぞれが非常に限定されたタスク(文字列、ポインタ、整数の出力)のみを実行するため、runtime·printf
のような複雑なフォーマット処理を行う関数よりもはるかに少ないスタックしか消費しません。これにより、runtime·futexsleep
関数内で安全にデバッグ情報を出力できるようになり、スタックオーバーフローの問題が解消されました。
*(int32*)0x1005 = 0x1005;
の行は、おそらくデバッグ目的で特定のメモリアドレスに値を書き込むことで、クラッシュを意図的に引き起こすか、特定の状態を示すためのものです。これは変更の対象外であり、スタックオーバーフローの問題とは直接関係ありません。
関連リンク
- Go CL 12047043: https://golang.org/cl/12047043
参考にした情報源リンク
- Go言語の公式ドキュメント (Go runtime, stack management,
nosplit
functionsに関する一般的な情報) - FreeBSDのmanページ (
umtx_wait
に関する情報) - Goのソースコード (
src/pkg/runtime/
ディレクトリ内の関連ファイル) - GoのIssueトラッカーやメーリングリスト (関連する議論やバグ報告)