[インデックス 18857] ファイルの概要
このコミットは、Goランタイムにおけるクラッシュ時のパニック処理を強化することを目的としています。特にSetPanicOnCrash
が有効な場合や、mallocgc
におけるnilポインタ参照のような状況で、ランタイムがパニックを起こす条件をより厳密に定義しています。これにより、デッドロック、ハング、または不明瞭なクラッシュにつながる可能性のある、有用でないパニックの発生を防ぎます。
コミット
commit 156962872575382697a0487030cd5777312d6d0c
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Mar 13 13:25:59 2014 +0400
runtime: harden conditions when runtime panics on crash
This is especially important for SetPanicOnCrash,
but also useful for e.g. nil deref in mallocgc.
Panics on such crashes can't lead to anything useful,
only to deadlocks, hangs and obscure crashes.
This is a copy of broken but already LGTMed
https://golang.org/cl/68540043/
TBR=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/75320043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/156962872575382697a0487030cd5777312d6d0c
元コミット内容
このコミットの元々の内容は、Goランタイムがクラッシュ時にパニックを発生させる条件をより厳しくするというものです。特にSetPanicOnCrash
が設定されている場合や、メモリ割り当て(mallocgc
)中のnilポインタ参照のような状況で、ランタイムがパニックを起こすことが、デッドロックやハング、あるいは原因不明のクラッシュを引き起こす可能性があるため、これを避けるための変更です。この変更は、以前に承認されたものの問題があったhttps://golang.org/cl/68540043/
のコピーであり、その修正版としてhttps://golang.org/cl/75320043
が提案されました。
変更の背景
Goランタイムは、プログラムの異常終了(クラッシュ)を検知した際に、通常はプロセスを終了させます。しかし、SetPanicOnCrash
のような設定が有効になっている場合、クラッシュを検知した際にGoのパニック機構を介してエラーを報告しようと試みることがあります。
このコミットの背景には、以下のような問題意識があります。
- デッドロックやハングの回避: クラッシュが発生している状況は、ランタイムの内部状態が既に破損している可能性が高いです。このような状況で無理にパニックを発生させようとすると、パニック処理自体がさらに別のエラーを引き起こしたり、ランタイムのロックが適切に解放されずにデッドロックやハング状態に陥ったりするリスクがあります。
- 不明瞭なクラッシュの防止: パニック処理が失敗することで、本来のクラッシュ原因とは異なる、より理解しにくいクラッシュが発生する可能性があります。これはデバッグを非常に困難にします。
mallocgc
におけるnil参照: ガベージコレクタ(GC)やメモリ割り当て処理(mallocgc
)中にnilポインタ参照のような致命的なエラーが発生した場合、その時点でランタイムは非常に不安定な状態にあります。このような状況でパニックを試みることは、さらなる問題を引き起こすだけで、有用な情報を提供しないと判断されました。
したがって、このコミットは、ランタイムが「安全に」パニックを発生させることができる条件を厳密にすることで、システムの安定性とデバッグのしやすさを向上させることを目指しています。つまり、パニックを発生させることがかえって有害であると判断される状況では、パニックではなく直接プロセスを終了させるべきであるという思想に基づいています。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念とC言語の知識が必要です。
-
Goroutine (G), Machine (M), Processor (P):
- G (Goroutine): Goの軽量スレッドです。Goプログラムの実行単位であり、Go関数が実行されるコンテキストです。
- M (Machine): OSのスレッド(カーネルスレッド)を表します。GoランタイムはMをOSに要求し、その上でGを実行します。
- P (Processor): MがGを実行するために必要な論理プロセッサです。PはMに割り当てられ、Gの実行に必要なリソース(スケジューラキューなど)を提供します。
m->curg
: 現在Mが実行しているGoroutine(G)を指します。m->g0
: 各Mに紐付けられた特別なGoroutineで、ランタイムコード(C言語で書かれた部分)を実行するために使用されます。Goコードは実行しません。
-
シグナルハンドリング:
- OSは、プログラムに異常が発生した場合(例: セグメンテーション違反、不正なメモリアクセス)にシグナルを送信します。
- Goランタイムは、これらのシグナルを捕捉し、適切に処理するためのシグナルハンドラを設定しています。
SigPanic
フラグ: シグナルテーブル(runtime·sigtab
)内のフラグで、特定のシグナルがGoのパニックを引き起こすべきかどうかを示します。SI_USER
: シグナルがユーザープロセスによって送信されたことを示すコードです。この場合、通常はパニックを引き起こしません。
-
Goランタイムの状態変数:
m->locks
: ランタイムがロックを保持しているかどうかを示すカウンタです。0でない場合、ランタイムはロックを保持しており、デッドロックのリスクがあります。m->mallocing
: ランタイムがメモリ割り当て中であるかどうかを示すフラグです。メモリ割り当て中にクラッシュした場合、ヒープの状態が不安定である可能性があります。m->throwing
: ランタイムがパニック処理中であるかどうかを示すフラグです。既にパニック処理中であれば、再帰的なパニックは避けるべきです。m->gcing
: ランタイムがガベージコレクション中であるかどうかを示すフラグです。GC中にクラッシュした場合、GCの状態が不安定である可能性があります。m->dying
: ランタイムが終了処理中であるかどうかを示すフラグです。gp->status
: Goroutineの現在の状態(例:Grunning
はGoroutineが実行中であることを示す)。gp->syscallsp
: Goroutineがシステムコール中であるかどうかを示すスタックポインタです。システムコール中にクラッシュした場合、OSのコンテキストにいるため、Goのパニック処理を安全に行えない可能性があります。m->libcallsp
(Windows固有): 外部ライブラリ呼び出し中であるかどうかを示すスタックポインタです。
-
runtime·throw
: Goランタイム内部で、回復不可能なエラーが発生した際に呼び出される関数です。通常、プログラムを終了させます。 -
C言語のポインタと構造体: Goランタイムの低レベル部分はC言語で書かれており、ポインタや構造体(
G
,M
,Siginfo
など)を多用します。
技術的詳細
このコミットの主要な変更点は、runtime·canpanic
という新しい関数の導入と、既存のシグナルハンドラにおけるその利用です。
runtime·canpanic
関数の導入
src/pkg/runtime/panic.c
にruntime·canpanic(G *gp)
関数が追加されました。この関数は、与えられたGoroutine gp
がクラッシュ時にパニックを発生させることが安全であるかどうかを判断します。
この関数は以下の条件をチェックします。
gp == nil || gp != m->curg
:gp
がnilである場合、またはgp
が現在のMが実行しているGoroutine(m->curg
)ではない場合、パニックは安全ではありません。これは、パニック処理が特定のGoroutineのコンテキストに依存するためです。特に、m->g0
(ランタイムGoroutine)上で発生したクラッシュは、Goコードのコンテキストではないため、パニックを発生させるべきではありません。
m->locks != 0 || m->mallocing != 0 || m->throwing != 0 || m->gcing != 0 || m->dying != 0
:- ランタイムがロックを保持している、メモリ割り当て中である、既にパニック処理中である、ガベージコレクション中である、または終了処理中である場合、パニックは安全ではありません。これらの状態はランタイムの内部状態が不安定であるか、デッドロックのリスクがあることを示します。
gp->status != Grunning || gp->syscallsp != 0
:- Goroutine
gp
がGrunning
状態ではない場合、またはシステムコール中である場合、パニックは安全ではありません。Grunning
でないGoroutineは実行コンテキストが不完全である可能性があり、システムコール中のGoroutineはOSのコンテキストにいるため、Goのパニック処理を安全に実行できません。
- Goroutine
m->libcallsp != 0
(Windows固有):- Windowsの場合、外部ライブラリ呼び出し中である場合もパニックは安全ではありません。これは、外部ライブラリのコードがGoランタイムの制御外で実行されており、そのコンテキストでパニック処理を行うことが困難であるためです。
これらの条件のいずれかが真であれば、runtime·canpanic
はfalse
を返し、パニックは安全ではないと判断されます。
シグナルハンドラでの利用
src/pkg/runtime/signal_386.c
, src/pkg/runtime/signal_amd64x.c
, src/pkg/runtime/signal_arm.c
の各シグナルハンドラ(runtime·sighandler
)において、パニックを発生させるかどうかの条件が変更されました。
変更前は、シグナルがユーザープロセスからのものでなく、かつSigPanic
フラグが立っている場合に、gp == nil || gp == m->g0
という条件でパニックを避けていました。これは、Goroutineが存在しないか、ランタイムGoroutine(g0
)上でクラッシュが発生した場合に、直接runtime·throw
(プログラム終了)にジャンプするという意味です。
変更後は、この条件が!runtime·canpanic(gp)
に置き換えられました。これにより、パニックを発生させるべきでない状況がより広範に、かつ厳密にチェックされるようになりました。つまり、runtime·canpanic
がfalse
を返す(パニックが安全でない)場合にのみ、goto Throw
(プログラム終了)が実行されます。
runtime.h
の変更
src/pkg/runtime/runtime.h
にruntime·canpanic
関数のプロトタイプ宣言が追加されました。
bool runtime·canpanic(G*);
これは、runtime·canpanic
関数が他のファイルから呼び出されるために必要です。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルとコード箇所は以下の通りです。
-
src/pkg/runtime/panic.c
:runtime·canpanic
関数の新規追加。この関数がパニックの安全性を判断するロジックをカプセル化しています。
-
src/pkg/runtime/runtime.h
:runtime·canpanic
関数のプロトタイプ宣言の追加。
-
src/pkg/runtime/signal_386.c
:runtime·sighandler
関数内で、パニックを発生させる条件が!runtime·canpanic(gp)
に変更されました。
-
src/pkg/runtime/signal_amd64x.c
:runtime·sighandler
関数内で、パニックを発生させる条件が!runtime·canpanic(gp)
に変更されました。
-
src/pkg/runtime/signal_arm.c
:runtime·sighandler
関数内で、パニックを発生させる条件が!runtime·canpanic(gp)
に変更されました。
コアとなるコードの解説
src/pkg/runtime/panic.c
におけるruntime·canpanic
関数
bool
runtime·canpanic(G *gp)
{
byte g;
USED(&g); // don't use global g, it points to gsignal
// Is it okay for gp to panic instead of crashing the program?
// Yes, as long as it is running Go code, not runtime code,
// and not stuck in a system call.
if(gp == nil || gp != m->curg)
return false;
if(m->locks != 0 || m->mallocing != 0 || m->throwing != 0 || m->gcing != 0 || m->dying != 0)
return false;
if(gp->status != Grunning || gp->syscallsp != 0)
return false;
#ifdef GOOS_windows
if(m->libcallsp != 0)
return false;
#endif
return true;
}
この関数は、Goroutine gp
がパニックを発生させるべきかどうかを決定する中心的なロジックです。
USED(&g);
: これはコンパイラに対するヒントで、ローカル変数g
が使用されていることを示し、最適化による削除を防ぎます。g
自体はここでは直接的な意味を持ちません。コメントにあるように、グローバルなg
(gsignal
を指す)と混同しないようにするためのものです。- 最初の
if
文:gp
がnil
であるか、または現在のMが実行しているGoroutine(m->curg
)と異なる場合、false
を返します。これは、シグナルがg0
(ランタイムGoroutine)上で発生した場合や、有効なGoルーチンコンテキストがない場合にパニックを避けるためです。 - 2番目の
if
文:m
(現在のOSスレッドのコンテキスト)が、ロックを保持している、メモリ割り当て中、パニック処理中、GC中、または終了処理中である場合、false
を返します。これらの状態はランタイムが不安定であるか、デッドロックのリスクがあることを示唆します。 - 3番目の
if
文:gp
のステータスがGrunning
でない(つまり、実行中ではない)か、またはシステムコール中である場合(gp->syscallsp != 0
)、false
を返します。実行中でないGoroutineやシステムコール中のGoroutineは、Goのパニック処理を安全に実行できる状態にありません。 #ifdef GOOS_windows
: Windows固有の条件で、外部ライブラリ呼び出し中である場合(m->libcallsp != 0
)、false
を返します。
これらの条件をすべて満たした場合にのみ、true
が返され、パニックが安全であると判断されます。
シグナルハンドラにおける変更例 (src/pkg/runtime/signal_amd64x.c
より)
// 変更前
// if(gp == nil || gp == m->g0)
// goto Throw;
// 変更後
if(!runtime·canpanic(gp))
goto Throw;
この変更により、シグナルハンドラは、シグナルがユーザープロセスからのものでなく、かつSigPanic
フラグが立っている場合に、まずruntime·canpanic(gp)
を呼び出します。
- もし
runtime·canpanic(gp)
がfalse
を返した場合(つまり、パニックが安全でないと判断された場合)、goto Throw
が実行され、プログラムは直接終了します。 runtime·canpanic(gp)
がtrue
を返した場合(パニックが安全であると判断された場合)、シグナルハンドラはGoのパニック機構を介してエラーを報告しようと試みます。
この変更は、Goランタイムの堅牢性を高め、特に予期せぬクラッシュが発生した際に、デッドロックやハングといった二次的な問題を防ぐ上で非常に重要です。
関連リンク
- Goのパニックとリカバリに関する公式ドキュメント(一般的な概念): https://go.dev/blog/defer-panic-and-recover
- Goランタイムのシグナルハンドリングに関する議論(より深い理解のため): Goのソースコードやメーリングリストのアーカイブが参考になります。
参考にした情報源リンク
- Goのコミット履歴: https://github.com/golang/go/commits/master
- Goのコードレビューシステム (Gerrit): https://go.dev/cl/ (特にコミットメッセージに記載されているCL番号)
https://golang.org/cl/68540043
https://golang.org/cl/75320043
- Goランタイムのソースコード(特に
src/runtime
ディレクトリ) - Goのメーリングリストアーカイブ (golang-dev, golang-nuts)
- Goの内部実装に関するブログ記事やドキュメント(例: Go scheduler, GC internals)
- Go scheduler: https://go.dev/doc/articles/go_scheduler.html
- Go GC: https://go.dev/doc/gc-guide
- C言語のシグナルハンドリングに関する一般的な知識。
SetPanicOnCrash
に関する情報(Goの古いバージョンや特定のデバッグツールで使われる可能性のある機能)。