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

[インデックス 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言語の公式ドキュメント (Go runtime, stack management, nosplit functionsに関する一般的な情報)
  • FreeBSDのmanページ (umtx_waitに関する情報)
  • Goのソースコード (src/pkg/runtime/ ディレクトリ内の関連ファイル)
  • GoのIssueトラッカーやメーリングリスト (関連する議論やバグ報告)