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

[インデックス 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は、そのゴルーチンが現在使用しているスタックの総サイズ(バイト単位)を表します。セグメントスタックの場合、これはすべてのスタックセグメントの合計サイズを指します。

技術的詳細

このコミットの技術的な核心は、スタックサイズチェックのタイミングと対象の変更にあります。

変更前: スタック拡張のロジックは以下のようでした。

  1. 現在のスタックのトップセグメントのサイズ(oldsize)を計算し、新しいトップセグメントの推奨サイズ(newsize = oldsize * 2)を決定します。
  2. copystackを呼び出す前にnewsizeruntime·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が単一の新しいスタックセグメントのサイズのみを考慮しており、ゴルーチンが既に持っている他のスタックセグメントのサイズを全く考慮していない点です。そのため、たとえnewsizeruntime·maxstacksize以下であっても、copystackが実行された結果、ゴルーチン全体のスタックサイズがruntime·maxstacksizeを超えてしまう可能性がありました。

変更後: 修正後のロジックは以下のようになります。

  1. copystack関数を呼び出し、スタックのコピーと拡張をまず行います。copystackが完了すると、ゴルーチンのgp->stacksizeは、新しいスタックの総サイズを正確に反映するようになります。
  2. copystackを呼び出した後に、ゴルーチン全体のスタックサイズであるgp->stacksizeruntime·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->stacksizeruntime·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)