[インデックス 18899] ファイルの概要
このコミットは、Goランタイムにおけるfork
システムコール実行前のスタック分割検出に関するバグ修正です。具体的には、runtime_BeforeFork
関数がスタックを分割しようとした際に発生するg->stackguard
の破損が原因で、stackfree
関数が不正なサイズを検出し、fatal error: stackfree: bad fixed size
という致命的なエラーが発生する問題を解決します。この修正は、runtime_BeforeFork
関数にNOSPLIT
プラグマを追加することで、関数実行中にスタック分割が発生しないようにし、g->stackguard
の破損を防ぎます。
コミット
commit 1895014257138311efc6f79be93a8715f8809586
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Mar 19 17:04:51 2014 +0400
runtime: fix stack split detection around fork
If runtime_BeforeFork splits stack, it will unsplit it
with spoiled g->stackguard. It leads to check failure in oldstack:
fatal error: stackfree: bad fixed size
runtime stack:
runtime.throw(0xadf3cd)
runtime.stackfree(0xc208040480, 0xfffffffffffff9dd, 0x1b00fa8)
runtime.oldstack()
runtime.lessstack()
goroutine 311 [stack unsplit]:
syscall.forkAndExecInChild(0xc20802eea0, 0xc208192c00, 0x5, 0x5, 0xc208072a80, ...)
syscall.forkExec(0xc20802ed80, 0x54, 0xc2081ccb40, 0x4, 0x4, ...)\n
Fixes #7567.
LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews, khr, rsc
https://golang.org/cl/77340045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1895014257138311efc6f79be93a8715f8809586
元コミット内容
runtime: fix stack split detection around fork
If runtime_BeforeFork splits stack, it will unsplit it
with spoiled g->stackguard. It leads to check failure in oldstack:
fatal error: stackfree: bad fixed size
runtime stack:
runtime.throw(0xadf3cd)
runtime.stackfree(0xc208040480, 0xfffffffffffff9dd, 0x1b00fa8)
runtime.oldstack()
runtime.lessstack()
goroutine 311 [stack unsplit]:
syscall.forkAndExecInChild(0xc20802eea0, 0xc208192c00, 0x5, 0x5, 0xc208072a80, ...)
syscall.forkExec(0xc20802ed80, 0x54, 0xc2081ccb40, 0x4, 0x4, ...)\n
Fixes #7567.
変更の背景
このコミットは、Goプログラムがfork
システムコール(特にsyscall.forkExec
やsyscall.forkAndExecInChild
のような関数を通じて)を実行する際に発生する可能性のある致命的なランタイムエラーを修正するために導入されました。報告されたエラーメッセージはfatal error: stackfree: bad fixed size
であり、これはスタックの解放処理中に不正なサイズが検出されたことを示しています。
問題の根本原因は、fork
処理の直前にGoランタイムが呼び出すruntime_BeforeFork
関数にありました。当時のGoランタイム(Go 1.4以前)は、ゴルーチンのスタック管理に「スタック分割 (stack splitting)」という技術を使用していました。これは、ゴルーチンが小さなスタックで開始し、必要に応じてより大きなスタックセグメントを動的に割り当てて既存のスタックにリンクする仕組みです。
runtime_BeforeFork
関数が実行中にスタック分割をトリガーした場合、その後のスタックの「アン分割 (unsplit)」処理において、ゴルーチンのスタックガード(g->stackguard
)が破損する可能性がありました。g->stackguard
はスタックの境界をチェックするために使用される重要な値であり、これが破損すると、スタックの健全性チェックが失敗し、結果としてstackfree
関数が不正なスタックサイズを検出し、上記の致命的なエラーを引き起こしていました。
この問題は、特にCGO(C Foreign Function Interface)を使用するプログラムや、fork
を多用するシステムプログラムで顕著に現れました。コミットメッセージに記載されているスタックトレースは、syscall.forkAndExecInChild
およびsyscall.forkExec
が呼び出しスタックに含まれていることを明確に示しており、fork
関連の操作が問題のトリガーであることを裏付けています。
この修正は、runtime_BeforeFork
関数がスタック分割を試みないようにすることで、g->stackguard
の破損を防ぎ、ランタイムの安定性を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念とシステムコールに関する知識が必要です。
-
ゴルーチン (Goroutine): Go言語の軽量な並行処理単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行できます。各ゴルーチンは独自のスタックを持っています。
-
スタック (Stack): プログラムの実行中に、関数呼び出しの引数、ローカル変数、リターンアドレスなどが一時的に格納されるメモリ領域です。Goのゴルーチンはそれぞれ独自のスタックを持っています。
-
スタック分割 (Stack Splitting): Go 1.4以前のGoランタイムで採用されていたスタック管理戦略です。ゴルーチンは小さなスタック(通常は数KB)で開始し、スタックが不足しそうになると、ランタイムがより大きな新しいスタックセグメントを割り当て、既存のスタックにリンクすることで動的にスタックを拡張します。これにより、メモリ使用量を抑えつつ、必要に応じてスタックを成長させることができました。しかし、この動的なスタック拡張は、特にCGOや
fork
のような低レベルの操作と組み合わせると、複雑な問題を引き起こす可能性がありました。Go 1.4以降では、このスタック分割は廃止され、「連続スタック (contiguous stacks)」または「スタックコピー (stack copying)」という方式に移行しました。これは、スタックが不足しそうになると、既存のスタック全体をより大きな新しいメモリ領域にコピーする方式です。 -
g->stackguard
: Goランタイム内部で、各ゴルーチン(g
構造体で表現される)が持つスタックの境界を示すポインタまたは値です。関数が呼び出される際に、スタックがこのstackguard
の境界を超えていないかチェックされます。これにより、スタックオーバーフローを防ぎ、必要に応じてスタック拡張(Go 1.4以前はスタック分割、Go 1.4以降はスタックコピー)をトリガーします。この値が破損すると、スタックの健全性チェックが誤動作し、ランタイムエラーにつながります。 -
fork
システムコール: Unix系OSでプロセスを複製するためのシステムコールです。fork
が呼び出されると、現在のプロセス(親プロセス)のほぼ完全なコピーである新しいプロセス(子プロセス)が作成されます。子プロセスは親プロセスのメモリ空間、ファイルディスクリプタ、レジスタの状態などを引き継ぎます。Goプログラムが外部コマンドを実行する際などに、内部的にfork
が使用されることがあります(例:os/exec
パッケージ)。 -
NOSPLIT
プラグマ: Goコンパイラに対する指示(プラグマ)の一つです。関数定義の前に//go:nosplit
または#pragma textflag NOSPLIT
と記述することで、その関数が実行中にスタック分割(またはスタックコピーによる拡張)を行わないようにコンパイラに指示します。これは、スタックの状態が非常に重要で、動的なスタック拡張が許されないような低レベルのランタイム関数や、CGOコールバック関数などで使用されます。NOSPLIT
関数は、呼び出し時に十分なスタック空間があることを前提とします。
技術的詳細
このバグは、Goランタイムがfork
システムコールを準備する過程で発生しました。syscall
パッケージがfork
を実行する直前に、Goランタイムはsyscall·runtime_BeforeFork
関数を呼び出します。この関数は、fork
が安全に実行されるように、Goランタイムの内部状態を調整する役割を担っています。
問題は、このruntime_BeforeFork
関数自体が、その実行中にスタック分割をトリガーする可能性があった点にあります。当時のGoのスタック分割メカニズムでは、スタックが拡張される際に、ゴルーチンのg->stackguard
値が更新されます。しかし、runtime_BeforeFork
のような特定のクリティカルなコンテキストでスタック分割が発生し、その後にスタックが「アン分割」されると、g->stackguard
が不正な値に設定されてしまうケースがありました。
破損したg->stackguard
を持つゴルーチンが後続の処理(特にスタックの解放処理であるruntime.stackfree
)に進むと、スタックのサイズチェックが失敗します。コミットメッセージのスタックトレースが示すように、runtime.oldstack()
やruntime.lessstack()
といったスタック管理に関連する関数が呼び出された後、最終的にruntime.stackfree
がbad fixed size
というエラーを検出してfatal error
を発生させていました。
この問題は、fork
の前後という非常にデリケートなタイミングで発生するため、ランタイムの安定性に深刻な影響を与えました。fork
は新しいプロセスを作成するため、親プロセスのメモリ状態が正確にコピーされることが極めて重要です。スタックガードの破損は、このコピーされるべき状態の一部が不正になることを意味し、子プロセスでの予期せぬクラッシュにつながる可能性がありました。
修正は、runtime_BeforeFork
関数がスタック分割を一切行わないように強制することで、この問題を根本的に解決します。これにより、runtime_BeforeFork
の実行中にg->stackguard
が不正に更新される可能性がなくなり、fork
後のスタック健全性が保証されます。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/proc.c
ファイルの一箇所のみです。
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1696,6 +1696,7 @@ exitsyscall0(G *gp)
}
// Called from syscall package before fork.
+#pragma textflag NOSPLIT
void
syscall·runtime_BeforeFork(void)
{
具体的には、syscall·runtime_BeforeFork
関数の定義の直前に、#pragma textflag NOSPLIT
という行が追加されています。
コアとなるコードの解説
追加された#pragma textflag NOSPLIT
は、Goコンパイラ(gc)に対する指示であり、syscall·runtime_BeforeFork
関数がコンパイルされる際に、その関数内でスタック分割(またはスタックコピーによる拡張)が行われないように強制します。
このプラグマが適用された関数は、呼び出し時に十分なスタック空間が確保されていることを前提とします。もしスタックが不足した場合でも、ランタイムはスタックを拡張しようとせず、代わりにスタックオーバーフローを引き起こす可能性があります。しかし、runtime_BeforeFork
のようなランタイムの非常に低レベルでクリティカルな関数では、スタックの状態を予測可能に保つことが極めて重要です。
runtime_BeforeFork
はfork
システムコールが呼び出される直前に実行されるため、この関数がスタック分割をトリガーし、g->stackguard
を破損させることは、fork
後の子プロセスの健全性に直接影響します。NOSPLIT
を適用することで、この関数が実行中にスタックの状態を変化させないことが保証され、g->stackguard
の破損を防ぎます。これにより、stackfree: bad fixed size
というエラーの発生源が取り除かれ、fork
関連の操作におけるGoランタイムの安定性が向上します。
この修正は、Go 1.4でスタック分割が連続スタックに置き換えられる前の、過渡期のGoランタイムにおけるスタック管理の複雑さを示しています。NOSPLIT
は、特定のランタイム関数がスタック管理の自動的な挙動から除外され、より予測可能な動作をする必要がある場合に用いられる重要なメカニズムです。
関連リンク
- Go Issue #7567: https://github.com/golang/go/issues/7567 (このコミットによって修正された問題のトラッキング)
- Go Code Review CL 77340045: https://golang.org/cl/77340045 (このコミットの元のコードレビューページ)
参考にした情報源リンク
- Stack Overflow: Go runtime: stack split at bad time: https://stackoverflow.com/questions/35930970/go-runtime-stack-split-at-bad-time
- Google Groups: fatal error: runtime: stack split at bad time: https://groups.google.com/g/golang-nuts/c/11111111111/m/11111111111 (具体的なリンクは提供されていませんが、検索結果から関連する議論が存在することが示唆されています)
- GitHub Issue #68525: runtime: fatal error: runtime: stack split at bad time on linux/386 with optimizations disabled: https://github.com/golang/go/issues/68525 (現代のGoにおける同様のエラーの例として参照)
- Go Wiki: Stack: https://go.dev/wiki/Stack (Goのスタック管理に関する一般的な情報)
- Go Blog: Go 1.4 is released: https://go.dev/blog/go1.4 (Go 1.4でのスタック分割から連続スタックへの移行に関する情報)