[インデックス 16283] ファイルの概要
このコミットは、Go言語のランタイムにおけるselect
文使用時のクラッシュバグを修正するものです。具体的には、runtime.park()
関数が解放済みのselect
ディスクリプタにアクセスしてしまう競合状態(race condition)を解消します。
コミット
commit 26d95d802750371cdfa50e7fe0a305c20dac1826
Author: Ian Lance Taylor <iant@golang.org>
Date: Wed May 8 14:58:34 2013 -0700
runtime: fix crash in select
runtime.park() can access freed select descriptor
due to a racing free in another thread.
See the comment for details.
Slightly modified version of dvyukov's CL 9259045.
No test yet. Before this CL, the test described in issue 5422
would fail about every 40 times for me. With this CL, I ran
the test 5900 times with no failures.
Fixes #5422.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/9311043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/26d95d802750371cdfa50e7fe0a305c20dac1826
元コミット内容
このコミットは、src/pkg/runtime/chan.c
ファイルに対して行われた変更です。主な変更は、selunlock
関数のロジック修正であり、select
ディスクリプタが解放された後にアクセスされることを防ぐためのものです。
変更の背景
この変更は、Go言語のランタイムにおけるselect
文の利用時に発生するクラッシュバグを修正するために導入されました。具体的には、Issue 5422で報告された問題に対応しています。
問題の根源は、runtime.park()
関数が、別のゴルーチン(スレッド)によって既に解放されたselect
ディスクリプタにアクセスしようとする競合状態にありました。select
文は複数のチャネル操作を待機するために使用され、その内部ではSelect
構造体というディスクリプタが使用されます。このSelect
構造体は、select
文の処理が完了すると解放される可能性があります。
しかし、runtime.park()
がゴルーチンをスリープさせ、その後別のゴルーチンがselect
文を完了させてSelect
構造体を解放した場合、runtime.park()
がスリープから復帰した際に、解放済みのメモリ領域にアクセスしようとしてクラッシュを引き起こす可能性がありました。コミットメッセージによると、この問題は特定のテストケースで約40回に1回の頻度で発生していたとのことです。
この修正は、dvyukov氏によるCL 9259045の修正版であり、既存のテストでは再現が困難であったものの、特定の条件下で発生する深刻なバグであったことが示唆されます。
前提知識の解説
このコミットを理解するためには、以下のGo言語のランタイムに関する知識が必要です。
- Goランタイム (Go Runtime): Goプログラムの実行を管理するシステムです。ゴルーチンのスケジューリング、メモリ管理(ガベージコレクション)、チャネル操作、システムコールなどが含まれます。C言語で書かれた部分も多く、本コミットの対象ファイルも
chan.c
とC言語で実装されています。 - ゴルーチン (Goroutine): Go言語における軽量な並行処理の単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行することも可能です。ゴルーチンはGoランタイムによってスケジューリングされます。
- チャネル (Channel): ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは同期プリミティブとしても機能し、ゴルーチン間の安全なデータ交換を可能にします。
select
文: 複数のチャネル操作(送受信)を同時に待機し、準備ができた最初の操作を実行するためのGo言語の制御構造です。どのチャネル操作も準備ができていない場合、select
文はブロックされます。default
ケースがある場合はブロックされません。runtime.park()
: Goランタイム内部で使用される関数で、現在のゴルーチンをスリープ状態にするために呼び出されます。これは、チャネル操作が完了するまで待機する場合や、他の同期プリミティブが満たされるまで待機する場合などに使用されます。- 競合状態 (Race Condition): 複数のゴルーチン(またはスレッド)が共有リソースに同時にアクセスし、そのアクセス順序によってプログラムの最終結果が非決定的に変わってしまう状態を指します。本件では、
Select
ディスクリプタの解放とアクセスが競合していました。 - メモリ解放 (Memory Deallocation): プログラムが使用しなくなったメモリ領域をシステムに返却するプロセスです。解放されたメモリは、他の目的のために再利用される可能性があります。解放済みのメモリにアクセスしようとすると、セグメンテーション違反などのクラッシュを引き起こす可能性があります(Use-After-Freeバグ)。
- ロック (Lock): 共有リソースへのアクセスを同期するためのメカニズムです。ロックを使用することで、一度に一つのゴルーチンだけが共有リソースにアクセスできるようにし、競合状態を防ぎます。Goランタイムのチャネル実装では、チャネルへのアクセスを保護するためにロックが使用されます。
技術的詳細
このコミットの技術的詳細は、selunlock
関数の変更に集約されます。selunlock
関数は、select
文が完了した際に、select
操作中にロックされたチャネルのロックを解除する役割を担っています。
変更前のselunlock
関数は、sel->lockorder
配列を逆順に走査し、各チャネルのロックを解除していました。しかし、この処理中に、sel
(Select
ディスクリプタ)自体が別のゴルーチンによって解放されてしまう可能性がありました。
コミットメッセージのコメントで詳細に説明されているように、問題のシナリオは以下の通りです。
- 最初のM(OSスレッド)が
runtime.selectgo()
内でruntime.park()
を呼び出し、sel
を渡します。 runtime.park()
が最後のロックを解除すると、別のMがselect
を呼び出したG(ゴルーチン)を再び実行可能にし、スケジューリングします。- 別のM上でGが実行されると、すべてのロックを再取得し、その後
sel
を解放します。 - この時点で、最初のMが
runtime.park()
から復帰し、解放済みのsel
にアクセスしようとすると、クラッシュが発生します。
この問題を解決するために、selunlock
関数は、最後のロックを解除した後にsel
に触れないように修正されました。
変更点:
selunlock
関数のループ処理が変更されました。以前はHchan *c, *c0;
と宣言されていた変数が、int32 i, n, r;
とHchan *c;
に変更されています。- ループの開始と終了条件がより厳密になりました。特に、
n = (int32)sel->ncase;
でケース数を取得し、r = 0;
またはr = 1;
でデフォルトケースをスキップするロジックが追加されています。 - 最も重要な変更は、
if(i>0 && sel->lockorder[i-1] == c)
という条件が追加されたことです。これは、現在のチャネルc
が前のチャネルと同じである場合、ロック解除をスキップするというものです。これにより、同じチャネルが複数回ロックされている場合でも、最後のロック解除までsel
へのアクセスを遅らせることができます。 - コメントで「We must be very careful here to not touch sel after we have unlocked the last lock, because sel can be freed right after the last unlock.」と明記されており、最後のロック解除後に
sel
にアクセスしないことの重要性が強調されています。
この修正により、selunlock
関数は、select
ディスクリプタが解放される可能性のあるタイミングを考慮し、安全にロックを解除するようになりました。
コアとなるコードの変更箇所
src/pkg/runtime/chan.c
ファイルのselunlock
関数が変更されています。
--- a/src/pkg/runtime/chan.c
+++ b/src/pkg/runtime/chan.c
@@ -809,16 +809,27 @@ sellock(Select *sel)
static void
selunlock(Select *sel)
{
- uint32 i;
- Hchan *c, *c0;
+ int32 i, n, r;
+ Hchan *c;
- c = nil;
- for(i=sel->ncase; i-->0;) {
- c0 = sel->lockorder[i];
- if(c0 && c0 != c) {
- c = c0;
- runtime·unlock(c);
- }
+ // We must be very careful here to not touch sel after we have unlocked
+ // the last lock, because sel can be freed right after the last unlock.
+ // Consider the following situation.
+ // First M calls runtime·park() in runtime·selectgo() passing the sel.
+ // Once runtime·park() has unlocked the last lock, another M makes
+ // the G that calls select runnable again and schedules it for execution.
+ // When the G runs on another M, it locks all the locks and frees sel.
+ // Now if the first M touches sel, it will access freed memory.
+ n = (int32)sel->ncase;
+ r = 0;
+ // skip the default case
+ if(n>0 && sel->lockorder[0] == nil)
+ r = 1;
+ for(i = n-1; i >= r; i--) {
+ c = sel->lockorder[i];
+ if(i>0 && sel->lockorder[i-1] == c)
+ continue; // will unlock it on the next iteration
+ runtime·unlock(c);
}
}
コアとなるコードの解説
変更されたselunlock
関数の主要なロジックは以下の通りです。
- 変数の初期化:
n = (int32)sel->ncase;
:select
文のケース数(チャネル操作の数)を取得します。r = 0;
: ループの開始インデックスを初期化します。
- デフォルトケースのスキップ:
if(n>0 && sel->lockorder[0] == nil)
: もしselect
文にdefault
ケースが含まれている場合(sel->lockorder[0]
がnil
であることで識別される)、ループの開始インデックスr
を1
に設定し、default
ケースの処理をスキップします。default
ケースはチャネル操作ではないため、ロックの対象ではありません。
- チャネルロックの解除ループ:
for(i = n-1; i >= r; i--)
:select
ケースの配列sel->lockorder
を逆順に走査します。これは、ロックを取得した順序と逆順に解除するためです。c = sel->lockorder[i];
: 現在のインデックスi
に対応するチャネルを取得します。if(i>0 && sel->lockorder[i-1] == c)
: この行が最も重要な変更点です。- もし現在のチャネル
c
が、配列の前の要素(sel->lockorder[i-1]
)と同じチャネルを指している場合、それは同じチャネルが複数回ロックされていることを意味します。 - この場合、
continue
して現在のイテレーションをスキップし、次のイテレーションで同じチャネルのロック解除を処理します。これにより、同じチャネルに対する複数のロック解除呼び出しを避け、最後のロック解除までsel
へのアクセスを遅らせることができます。
- もし現在のチャネル
runtime·unlock(c);
: 現在のチャネルc
のロックを解除します。
この修正により、selunlock
関数は、select
ディスクリプタが解放される可能性のあるタイミングを考慮し、最後のロック解除が完了するまでsel
構造体へのアクセスを最小限に抑えることで、Use-After-Freeバグを防いでいます。
関連リンク
- Go Issue 5422: https://github.com/golang/go/issues/5422
- このコミットが修正した具体的なバグ報告です。詳細な再現手順や議論が記載されています。
- Go CL 9259045 (dvyukov's original CL): https://go-review.googlesource.com/c/go/+/9259045
- このコミットのベースとなった、dvyukov氏による元の変更リストです。
- Go CL 9311043 (This commit's CL): https://golang.org/cl/9311043
- このコミットに対応するGoの変更リスト(Change List)です。
参考にした情報源リンク
- Go Issue 5422: runtime: fix crash in select: https://github.com/golang/go/issues/5422
- Go CL 9311043: runtime: fix crash in select: https://golang.org/cl/9311043
- Go CL 9259045: runtime: fix crash in select: https://go-review.googlesource.com/c/go/+/9259045
- Go言語のチャネルとselect文に関する公式ドキュメントやチュートリアル
- Goランタイムの内部構造に関する資料(例:Goのスケジューラ、メモリ管理に関するブログ記事や論文)
- 競合状態(Race Condition)とUse-After-Freeバグに関する一般的な情報源
runtime.park()
に関するGoランタイムのソースコードコメントや関連する議論- Go言語の
select
文の動作原理に関する解説記事 - Go言語の並行処理と同期プリミティブに関する書籍やオンラインリソース
- C言語におけるポインタとメモリ管理に関する一般的な知識
- Go言語の
go.dev
公式ドキュメント - Go言語のソースコードリポジトリ(GitHub)
- Go言語のメーリングリストやフォーラムでの関連議論
- Go言語のIssueトラッカー
- Go言語の変更リスト(CL)レビューシステム (Gerrit)I have generated the commit explanation as requested, following all the specified instructions and chapter structure. The output is in Markdown format and is in Japanese. I have included details about the background, prerequisite knowledge, and technical aspects of the change, along with core code changes and relevant links.