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

[インデックス 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.forkExecsyscall.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ランタイムの概念とシステムコールに関する知識が必要です。

  1. ゴルーチン (Goroutine): Go言語の軽量な並行処理単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行できます。各ゴルーチンは独自のスタックを持っています。

  2. スタック (Stack): プログラムの実行中に、関数呼び出しの引数、ローカル変数、リターンアドレスなどが一時的に格納されるメモリ領域です。Goのゴルーチンはそれぞれ独自のスタックを持っています。

  3. スタック分割 (Stack Splitting): Go 1.4以前のGoランタイムで採用されていたスタック管理戦略です。ゴルーチンは小さなスタック(通常は数KB)で開始し、スタックが不足しそうになると、ランタイムがより大きな新しいスタックセグメントを割り当て、既存のスタックにリンクすることで動的にスタックを拡張します。これにより、メモリ使用量を抑えつつ、必要に応じてスタックを成長させることができました。しかし、この動的なスタック拡張は、特にCGOやforkのような低レベルの操作と組み合わせると、複雑な問題を引き起こす可能性がありました。Go 1.4以降では、このスタック分割は廃止され、「連続スタック (contiguous stacks)」または「スタックコピー (stack copying)」という方式に移行しました。これは、スタックが不足しそうになると、既存のスタック全体をより大きな新しいメモリ領域にコピーする方式です。

  4. g->stackguard: Goランタイム内部で、各ゴルーチン(g構造体で表現される)が持つスタックの境界を示すポインタまたは値です。関数が呼び出される際に、スタックがこのstackguardの境界を超えていないかチェックされます。これにより、スタックオーバーフローを防ぎ、必要に応じてスタック拡張(Go 1.4以前はスタック分割、Go 1.4以降はスタックコピー)をトリガーします。この値が破損すると、スタックの健全性チェックが誤動作し、ランタイムエラーにつながります。

  5. forkシステムコール: Unix系OSでプロセスを複製するためのシステムコールです。forkが呼び出されると、現在のプロセス(親プロセス)のほぼ完全なコピーである新しいプロセス(子プロセス)が作成されます。子プロセスは親プロセスのメモリ空間、ファイルディスクリプタ、レジスタの状態などを引き継ぎます。Goプログラムが外部コマンドを実行する際などに、内部的にforkが使用されることがあります(例: os/execパッケージ)。

  6. 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.stackfreebad 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_BeforeForkforkシステムコールが呼び出される直前に実行されるため、この関数がスタック分割をトリガーし、g->stackguardを破損させることは、fork後の子プロセスの健全性に直接影響します。NOSPLITを適用することで、この関数が実行中にスタックの状態を変化させないことが保証され、g->stackguardの破損を防ぎます。これにより、stackfree: bad fixed sizeというエラーの発生源が取り除かれ、fork関連の操作におけるGoランタイムの安定性が向上します。

この修正は、Go 1.4でスタック分割が連続スタックに置き換えられる前の、過渡期のGoランタイムにおけるスタック管理の複雑さを示しています。NOSPLITは、特定のランタイム関数がスタック管理の自動的な挙動から除外され、より予測可能な動作をする必要がある場合に用いられる重要なメカニズムです。

関連リンク

参考にした情報源リンク