[インデックス 18856] ファイルの概要
このコミットは、Goランタイムにおけるスタックサイズチェックの不具合を修正するものです。具体的には、ゴルーチンのスタックが拡張される際に、スタックの総サイズではなく、新しく確保されたスタックセグメントのサイズのみをチェックしていた問題を解決します。これにより、スタックオーバーフローの検出がより正確に行われるようになります。
コミット
commit 5daffee17fdd8c10ead83a87861d99c39f05561d
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Mar 13 13:16:02 2014 +0400
runtime: fix stack size check
When we copy stack, we check only new size of the top segment.
This is incorrect, because we can have other segments below it.
LGTM=khr
R=golang-codereviews, khr
CC=golang-codereviews, rsc
https://golang.org/cl/73980045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5daffee17fdd8c10ead83a87861d99c39f05561d
元コミット内容
このコミットは、Goランタイムのsrc/pkg/runtime/stack.c
ファイルに対して行われた変更です。
変更前は、runtime·newstack
関数内でスタックをコピーする際、新しく確保されるスタックセグメントのサイズ(newsize
)のみをruntime·maxstacksize
と比較していました。
- if(newsize > runtime·maxstacksize) {
- runtime·printf("runtime: goroutine stack exceeds %D-byte limit\\n", (uint64)runtime·maxstacksize);
- runtime·throw("stack overflow");
- }
copystack(gp, nframes, newsize);
if(StackDebug >= 1)
runtime·printf("stack grow done\\n");
変更の背景
Goのランタイムは、ゴルーチンのスタックを必要に応じて動的に拡張する「セグメントスタック」というメカニズムを採用しています。これは、スタックの利用効率を高め、多数のゴルーチンを効率的に実行するために設計されています。スタックが不足すると、ランタイムは新しい、より大きなスタックセグメントを割り当て、古いスタックの内容を新しいスタックにコピーします。このプロセスは「スタックコピー」または「スタックグロース(stack growth)」と呼ばれます。
このコミット以前のGoランタイムでは、スタックが拡張される際に、スタックの最大サイズ(runtime·maxstacksize
)を超えていないかどうかのチェックが不適切に行われていました。具体的には、新しく確保されるスタックセグメントのサイズ(newsize
)のみをチェックしており、ゴルーチンが既に持っている既存のスタックセグメントの合計サイズを考慮していませんでした。
この問題は、ゴルーチンが複数のスタックセグメントを持っている場合に顕在化します。例えば、既存のスタックセグメントの合計サイズが既にruntime·maxstacksize
に近い、あるいは超えているにもかかわらず、新しく追加されるセグメント単体のサイズがruntime·maxstacksize
以下であれば、スタックオーバーフローが検出されずに処理が続行されてしまう可能性がありました。これは、最終的に予期せぬクラッシュや未定義の動作を引き起こす原因となります。
このコミットは、この不正確なスタックサイズチェックを修正し、ゴルーチンのスタックの総サイズ(gp->stacksize
)がruntime·maxstacksize
を超えていないかを適切に確認することで、スタックオーバーフローを正確に検出できるようにすることを目的としています。
前提知識の解説
Goランタイムとゴルーチン
Goは、軽量な並行処理の単位として「ゴルーチン(goroutine)」を提供します。ゴルーチンはOSのスレッドよりもはるかに軽量であり、数百万のゴルーチンを同時に実行することも可能です。Goランタイムは、これらのゴルーチンを効率的にスケジューリングし、実行するための様々なメカニズムを提供します。
セグメントスタック (Segmented Stacks)
Go 1.3以前のバージョンでは、ゴルーチンのスタック管理に「セグメントスタック」という方式が採用されていました。これは、スタックを固定サイズのセグメントに分割し、必要に応じて新しいセグメントを割り当てて既存のスタックに連結していく方式です。スタックが不足すると、新しいセグメントが追加され、古いセグメントの内容が新しいセグメントにコピーされます。これにより、スタックのメモリ使用量を効率的に管理し、多数のゴルーチンをサポートすることが可能になります。
しかし、セグメントスタックにはいくつかの欠点がありました。特に、スタックの拡張(stack growth)や縮小(stack shrink)の際に、スタックの内容をコピーする必要があり、これがパフォーマンスオーバーヘッドとなることがありました。また、スタックの分割により、スタックポインタの操作が複雑になるという問題もありました。
補足: Go 1.3以降では、セグメントスタックは「連続スタック(contiguous stacks)」に置き換えられました。連続スタックでは、スタックは単一の連続したメモリ領域として扱われ、必要に応じてより大きな連続した領域にコピーされます。この変更により、スタック操作のオーバーヘッドが削減され、パフォーマンスが向上しました。このコミットはGo 1.3以前のセグメントスタック時代のコードベースに対する修正ですが、スタックの拡張とサイズチェックの概念は連続スタックにも通じるものがあります。
runtime·newstack
関数
runtime·newstack
はGoランタイム内部の関数で、ゴルーチンのスタックが不足した際に呼び出され、スタックを拡張する処理を担当します。この関数は、新しいスタック領域を確保し、既存のスタックの内容を新しい領域にコピーするなどの一連の操作を行います。
copystack
関数
copystack
は、runtime·newstack
内で呼び出される関数で、古いスタックの内容を新しいスタック領域に実際にコピーする処理を行います。
runtime·maxstacksize
runtime·maxstacksize
は、Goランタイムがゴルーチンに割り当てを許可するスタックの最大サイズを定義する定数です。この制限は、無限のスタック使用を防ぎ、システムリソースの枯渇を防ぐために設けられています。このサイズを超えると、スタックオーバーフローとして扱われ、プログラムはパニック(panic)します。
gp->stacksize
gp
は現在のゴルーチンを表すG
構造体へのポインタです。gp->stacksize
は、そのゴルーチンが現在使用しているスタックの総サイズ(バイト単位)を表します。セグメントスタックの場合、これはすべてのスタックセグメントの合計サイズを指します。
技術的詳細
このコミットの技術的な核心は、スタックサイズチェックのタイミングと対象の変更にあります。
変更前: スタック拡張のロジックは以下のようでした。
- 現在のスタックのトップセグメントのサイズ(
oldsize
)を計算し、新しいトップセグメントの推奨サイズ(newsize = oldsize * 2
)を決定します。 copystack
を呼び出す前に、newsize
がruntime·maxstacksize
を超えていないかをチェックします。- if(newsize > runtime·maxstacksize) { - runtime·printf("runtime: goroutine stack exceeds %D-byte limit\\n", (uint64)runtime·maxstacksize); - runtime·throw("stack overflow"); - } copystack(gp, nframes, newsize);
このアプローチの問題点は、newsize
が単一の新しいスタックセグメントのサイズのみを考慮しており、ゴルーチンが既に持っている他のスタックセグメントのサイズを全く考慮していない点です。そのため、たとえnewsize
がruntime·maxstacksize
以下であっても、copystack
が実行された結果、ゴルーチン全体のスタックサイズがruntime·maxstacksize
を超えてしまう可能性がありました。
変更後: 修正後のロジックは以下のようになります。
copystack
関数を呼び出し、スタックのコピーと拡張をまず行います。copystack
が完了すると、ゴルーチンのgp->stacksize
は、新しいスタックの総サイズを正確に反映するようになります。copystack
を呼び出した後に、ゴルーチン全体のスタックサイズであるgp->stacksize
がruntime·maxstacksize
を超えていないかをチェックします。copystack(gp, nframes, newsize); if(StackDebug >= 1) runtime·printf("stack grow done\\n"); + if(gp->stacksize > runtime·maxstacksize) { + runtime·printf("runtime: goroutine stack exceeds %D-byte limit\\n", (uint64)runtime·maxstacksize); + runtime·throw("stack overflow"); + }
この変更により、スタックサイズチェックは、ゴルーチンが使用しているスタックの総量に対して行われるようになり、より正確なスタックオーバーフローの検出が可能になりました。これは、セグメントスタックの特性(複数のセグメントからなるスタック)を正しく考慮した修正と言えます。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/stack.c
ファイルのruntime·newstack
関数内で行われています。
--- a/src/pkg/runtime/stack.c
+++ b/src/pkg/runtime/stack.c
@@ -662,13 +662,13 @@ runtime·newstack(void)
\toldbase = (byte*)gp->stackbase + sizeof(Stktop);\
\toldsize = oldbase - oldstk;\
\tnewsize = oldsize * 2;\
-\t\t\tif(newsize > runtime·maxstacksize) {\
-\t\t\t\truntime·printf(\"runtime: goroutine stack exceeds %D-byte limit\\n\", (uint64)runtime·maxstacksize);\
-\t\t\t\truntime·throw(\"stack overflow\");\
-\t\t\t}\
\t\t\tcopystack(gp, nframes, newsize);\
\t\t\tif(StackDebug >= 1)\
\t\t\t\truntime·printf(\"stack grow done\\n\");\
+\t\t\tif(gp->stacksize > runtime·maxstacksize) {\
+\t\t\t\truntime·printf(\"runtime: goroutine stack exceeds %D-byte limit\\n\", (uint64)runtime·maxstacksize);\
+\t\t\t\truntime·throw(\"stack overflow\");\
+\t\t\t}\
\t\t\tgp->status = oldstatus;\
\t\t\truntime·gogo(&gp->sched);\
\t\t}
コアとなるコードの解説
変更の核心は、スタックサイズチェックの条件がnewsize
からgp->stacksize
に、そしてチェックのタイミングがcopystack
の前から後に移動した点です。
-
削除されたコード:
- if(newsize > runtime·maxstacksize) { - runtime·printf("runtime: goroutine stack exceeds %D-byte limit\\n", (uint64)runtime·maxstacksize); - runtime·throw("stack overflow"); - }
この部分は、新しいスタックセグメントのサイズ(
newsize
)のみをチェックしていました。これは、スタックが複数のセグメントで構成されている場合に、全体のスタックサイズを正確に反映しないため、不適切でした。 -
追加されたコード:
+ if(gp->stacksize > runtime·maxstacksize) { + runtime·printf("runtime: goroutine stack exceeds %D-byte limit\\n", (uint64)runtime·maxstacksize); + runtime·throw("stack overflow"); + }
この新しいチェックは、
copystack
が実行された後に配置されています。copystack
が完了すると、ゴルーチンのスタックポインタ(gp->stacksize
)は、新しく拡張されたスタックの総サイズを正確に指すようになります。したがって、この時点でgp->stacksize
をruntime·maxstacksize
と比較することで、ゴルーチン全体のスタック使用量が最大制限を超えているかどうかを正確に判断できます。
この修正により、Goランタイムはスタックオーバーフローをより堅牢に検出し、プログラムの安定性を向上させることができました。
関連リンク
- Goのセグメントスタックに関する議論(Go 1.3以前の文脈):
- Go's segmented stacks (Go 1.3での連続スタックへの移行に関する公式ブログ記事ですが、セグメントスタックの背景も説明されています)
参考にした情報源リンク
- Goのソースコード(
src/pkg/runtime/stack.c
) - Goのコミット履歴
- Goの公式ブログ (Go 1.3 Stack Changes)
- GoのIssue Tracker (関連するIssueやCL)