[インデックス 1935] ファイルの概要
このコミットは、Goランタイムにおけるスタックオーバーフローのバグを修正するものです。特に、defer
文の処理 (deferproc
) やスタック拡張 (morestack
) といったクリティカルなランタイム関数が、スタックガード領域内で安全に動作するためのスタックサイズ定数を見直し、メモリ割り当てメカニズムを改善しています。
コミット
commit 95100344d33f7b99cd728260d8e6ee6a19ce0429
Author: Russ Cox <rsc@golang.org>
Date: Wed Apr 1 00:26:00 2009 -0700
fix runtime stack overflow bug that gri ran into:
160 - 75 was just barely not enough for deferproc + morestack.
added enum names and bumped to 256 - 128.
added explanation.
changed a few mal() (garbage-collected) to
malloc()/free() (manually collected).
R=ken
OCL=26981
CL=26981
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/95100344d33f7b99cd728260d8e6ee6a19ce0429
元コミット内容
このコミットは、gri
が遭遇したランタイムのスタックオーバーフローバグを修正します。以前のスタックガード設定(160 - 75バイト)では、deferproc
とmorestack
の実行に必要な領域がギリギリで不足していました。
この修正では、スタック関連の定数に列挙型名を追加し、StackGuard
を256バイト、StackSmall
を128バイトにそれぞれ増やしました。また、これらの変更に関する説明が追加されています。
さらに、いくつかのmal()
(ガベージコレクション対象のメモリ割り当て)呼び出しが、malloc()
/free()
(手動で管理されるメモリ割り当て)に変更されました。
変更の背景
Go言語の初期バージョンでは、Goroutineのスタックは「セグメントスタック」と呼ばれる方式で管理されていました。これは、Goroutineが最初に小さなスタックで開始し、必要に応じて新しいスタックセグメントを動的に割り当てて既存のスタックに連結することで、スタックを拡張する仕組みです。この設計は、多数のGoroutineを効率的に実行するために重要でした。
しかし、この動的なスタック拡張メカニズムには、特定の条件下でスタックオーバーフローを引き起こす潜在的な問題がありました。特に、defer
文の処理を行うdeferproc
関数や、スタック拡張を実際に担当するmorestack
関数のような、ランタイムの非常に低レベルでクリティカルな関数が関係していました。
コミットメッセージによると、「160 - 75 (バイト) では、deferproc
+ morestack
にかろうじて足りなかった」とあります。これは、スタックの最下部に確保される「スタックガード」と呼ばれる領域が、これらの関数が安全に実行されるために必要な最小限のスタック空間を提供していなかったことを示唆しています。もしこの領域が不足すると、deferproc
がdefer
情報をスタックにプッシュしたり、morestack
が新しいスタックセグメントを割り当てる前に、スタックポインタが許容範囲を超えてしまい、スタックオーバーフローが発生する可能性がありました。
このバグは、Goランタイムの安定性と信頼性にとって重要であり、特にdefer
のようなGoの基本的な機能が予期せぬクラッシュを引き起こすことは許容できませんでした。そのため、スタックガードのサイズを増やし、これらのクリティカルな関数が十分な実行空間を確保できるようにすることが必要とされました。
また、mal()
からmalloc()
/free()
への変更は、これらのクリティカルなパスでのメモリ割り当てがガベージコレクションのオーバーヘッドから解放され、より予測可能で高速な動作を保証するための最適化であると考えられます。
前提知識の解説
GoのGoroutineとスタック管理
Go言語のGoroutineは、OSのスレッドよりもはるかに軽量な並行実行単位です。数千、数万ものGoroutineを同時に実行できるのは、Goランタイムが独自のスタック管理メカニズムを持っているためです。初期のGoでは「セグメントスタック」が採用されていました。これは、各Goroutineが小さなスタック(例えば4KB)で開始し、関数呼び出しによってスタックが必要なだけ動的に拡張される仕組みです。スタックが現在のセグメントの限界に近づくと、ランタイムは新しい、より大きなセグメントを割り当て、古いセグメントと連結します。これにより、メモリを効率的に利用しつつ、スタックオーバーフローを自動的に回避しようとします。
defer
文とdeferproc
Goのdefer
文は、関数がリターンする直前に実行される関数呼び出しをスケジュールするために使用されます。これはリソースのクリーンアップ(ファイルのクローズ、ロックの解放など)によく利用されます。defer
文が実行されると、コンパイラはruntime.deferproc
(またはその前身となるsys·deferproc
)というランタイム関数を呼び出します。このdeferproc
は、遅延実行される関数の情報(関数ポインタ、引数など)を現在のGoroutineのスタック上に記録します。したがって、deferproc
自身もスタック空間を消費します。
スタック拡張メカニズムとmorestack
Goの関数は、呼び出される際にスタックの残量チェックを行います。これは、スタックポインタがg->stackguard
(スタックガード)と呼ばれる特定のメモリ位置よりも下にあるかどうかを比較することで行われます。もしスタックが不足していると判断された場合、ランタイムはruntime.morestack
(またはその前身となるsys·morestack
)関数を呼び出します。morestack
は、新しいスタックセグメントを割り当て、現在のスタックの内容を新しいセグメントにコピーし、スタックポインタを新しいセグメントの適切な位置に調整することで、スタックを拡張します。このプロセスは、Goroutineがスタックオーバーフローを起こすことなく、必要なだけスタックを使い続けられるようにするために不可欠です。
Goランタイムのメモリ管理(mal
とmalloc
)
Goランタイムは、独自のメモリ管理システムを持っています。これは、OSのmalloc
を直接呼び出すのではなく、Goのガベージコレクタと連携してメモリを効率的に割り当てるためのものです。初期のGoランタイムには、mal()
という内部関数が存在し、これはガベージコレクションの対象となるメモリを割り当てるために使用されていました。一方、C言語の標準ライブラリ関数であるmalloc()
とfree()
は、手動でメモリを割り当て・解放するために使用されます。
このコミットでmal()
からmalloc()
/free()
への変更が見られるのは、defer
構造体のような特定のランタイム内部データ構造のメモリ管理を、ガベージコレクションのサイクルから切り離し、より直接的かつ予測可能な方法で管理するためと考えられます。これにより、ガベージコレクションの遅延やオーバーヘッドがクリティカルなパスに影響を与えるのを防ぎ、ランタイムのパフォーマンスと安定性を向上させる狙いがあったと推測されます。
textflag 7
(NOSPLIT)
Goのアセンブリコードにおいて、#pragma textflag 7
はNOSPLIT
フラグを意味します。このフラグが付けられた関数は、スタックの分割(つまり、スタックの拡張)が許可されません。これは、Goランタイムの非常に低レベルな関数、特にスタック管理やスケジューリングといったクリティカルな操作を行う関数に適用されます。これらの関数は、スタック拡張のチェックやGoroutineのスケジューリングといった通常の処理をスキップすることで、実行のオーバーヘッドを最小限に抑え、予測可能な動作を保証します。sys·newproc
(新しいGoroutineの作成)やsys·deferproc
(defer
の処理)がこのフラグを持つのは、これらの操作がランタイムの整合性を保つ上で極めて重要であり、スタック拡張による中断が許されないためです。
技術的詳細
このコミットの核心は、Goランタイムのスタック管理における定数の調整と、それに伴うメモリ割り当ての変更です。
-
スタックサイズ定数の調整:
- 以前はマジックナンバーとして使われていたスタック関連の定数に、
StackGuard
、StackSmall
、StackBig
という列挙型名が与えられました。これにより、コードの可読性と保守性が向上しました。 StackGuard
は、スタックの最下部からスタックガード(g->stackguard
)までのバイトオフセットを示します。以前は160
という値が使われていましたが、このコミットで256
に増やされました。これは、スタックオーバーフローチェックの基準となる位置を、スタックの底からより離れた場所に設定することを意味します。StackSmall
は、スタックフレームが小さい関数が、スタックガードの下にどれだけスタックポインタが食い込んでも許容されるバイト数を示します。以前は75
でしたが、このコミットで128
に増やされました。これにより、小さなスタックフレームを持つ関数がスタックチェックを行う際の命令数を削減しつつ、deferproc
やmorestack
のようなクリティカルな関数がスタックガード領域内で安全に実行できる十分な余裕が確保されます。StackBig
は、スタックフレームが非常に大きい関数が、スタックチェックをスキップして常にmorestack
を呼び出す基準となるサイズです。以前は4096
でしたが、この値は変更されていません。
- 以前はマジックナンバーとして使われていたスタック関連の定数に、
-
スタックオーバーフローチェックロジックの改善:
src/cmd/6l/pass.c
(amd64リンカ) とsrc/cmd/8l/pass.c
(386リンカ) において、スタックチェックのコード生成ロジックが変更されました。以前はハードコードされた75
や4096
といった値が使われていましたが、これらが新しく定義されたStackSmall
やStackBig
に置き換えられました。これにより、リンカが生成するスタックチェックコードが、ランタイムのスタック定数と同期されるようになりました。- 特に、
autoffset <= 75
という条件がautoffset <= StackSmall
に変更され、-(autoffset-75)
が-(autoffset-StackSmall)
に変更されたことで、スタックガード領域の計算がより正確かつ柔軟になりました。
-
deferproc
とmorestack
の安全性確保:src/runtime/proc.c
のコメントで、StackGuard - StackSmall
(このコミット後では256 - 128 = 128
バイト)の領域が重要であることが明記されました。この領域は、スタックオーバーフローチェックを行わない関数(例:sys·deferproc
)や、差し迫ったスタックオーバーフローを処理する関数(例:sys·morestack
)が実行されるための十分な空間を提供する必要があります。sys·deferproc
がmalloc
を呼び出す可能性があり、そのmalloc
がスタックチェックを行い、さらにsys·morestack
をトリガーする可能性があるという複雑なシナリオが考慮されています。この一連の処理がスタックの最下部領域に収まるように、スタックガードのサイズが調整されました。
-
メモリ割り当ての変更 (
mal
からmalloc
/free
へ):src/runtime/iface.c
とsrc/runtime/proc.c
において、mal()
関数によるメモリ割り当てがmalloc()
とfree()
による手動割り当てに変更されました。- 具体的には、インターフェース型情報 (
Itab
) やDefer
構造体 (Defer *d
) の割り当てがこれに該当します。これらのデータ構造はランタイムの内部で頻繁に利用され、そのメモリ管理がガベージコレクションのサイクルに依存すると、パフォーマンスの変動や予測不能な遅延を引き起こす可能性があります。手動でのmalloc
/free
に切り替えることで、これらのクリティカルなデータ構造のライフサイクルをより厳密に制御し、ランタイムの安定性と効率を向上させています。特にsys·deferreturn
では、Defer
構造体の解放にfree(d)
が明示的に追加されています。
-
sys·newproc
とsys·deferproc
のNOSPLIT
属性:sys·newproc
とsys·deferproc
関数には、引き続き#pragma textflag 7
(NOSPLIT
)が適用されています。これは、これらの関数がスタック拡張を伴うことなく、引数を連続したメモリ領域として扱う必要があるためです。スタック分割が発生すると、引数の一部しかコピーされない可能性があり、ランタイムの整合性が損なわれる恐れがあるため、これらの関数はスタック拡張を禁止されています。
これらの変更は、Goランタイムのスタック管理をより堅牢にし、特定のコーナーケースでのスタックオーバーフローを防ぐとともに、クリティカルなパスのパフォーマンスを最適化することを目的としています。
コアとなるコードの変更箇所
src/cmd/6l/pass.c
および src/cmd/8l/pass.c
--- a/src/cmd/6l/pass.c
+++ b/src/cmd/6l/pass.c
@@ -30,6 +30,13 @@
#include "l.h"
+// see ../../runtime/proc.c:/StackGuard
+enum
+{
+ StackSmall = 128,
+ StackBig = 4096,
+};
+
void
dodata(void)
{
@@ -602,8 +609,8 @@ dostkoff(void)
tp->from.offset = 3;
}
- if(autoffset < 4096) { // do we need to call morestack
- if(autoffset <= 75) {
+ if(autoffset < StackBig) { // do we need to call morestack?
+ if(autoffset <= StackSmall) {
// small stack
p = appendp(p);
p->as = ACMPQ;
@@ -618,7 +625,7 @@ dostkoff(void)
p = appendp(p);
p->as = ALEAQ;
p->from.type = D_INDIR+D_SP;
- p->from.offset = -(autoffset-75);
+ p->from.offset = -(autoffset-StackSmall);
p->to.type = D_AX;
if(q1) {
q1->pcond = p;
(src/cmd/8l/pass.c
も同様の変更)
src/runtime/iface.c
--- a/src/runtime/iface.c
+++ b/src/runtime/iface.c
@@ -178,7 +178,7 @@ itype(Sigi *si, Sigt *st, int32 canfail)
}
ni = si->size;
- m = mal(sizeof(*m) + ni*sizeof(m->fun[0]));
+ m = malloc(sizeof(*m) + ni*sizeof(m->fun[0]));
m->sigi = si;
m->sigt = st;
@@ -692,8 +692,8 @@ fakesigt(string type, bool indir)
}
}
- sigt = mal(sizeof(*sigt));
- sigt->name = mal(type->len + 1);
+ sigt = malloc(sizeof(*sigt));
+ sigt->name = malloc(type->len + 1);
mcpy(sigt->name, type->str, type->len);
sigt->alg = AFAKE;
src/runtime/proc.c
--- a/src/runtime/proc.c
+++ b/src/runtime/proc.c
@@ -140,108 +140,6 @@ sys·Goexit(void)
sys·Gosched();
}
-G*
-malg(int32 stacksize)
-{
- G *g;
- byte *stk;
-
- // 160 is the slop amount known to the stack growth code
- g = malloc(sizeof(G));
- stk = stackalloc(160 + stacksize);
- g->stack0 = stk;
- g->stackguard = stk + 160;
- g->stackbase = stk + 160 + stacksize;
- return g;
-}
-
-#pragma textflag 7
-void
-sys·newproc(int32 siz, byte* fn, byte* arg0)
-{
- byte *stk, *sp;
- G *newg;
-
-//printf("newproc siz=%d fn=%p", siz, fn);
-
- siz = (siz+7) & ~7;
- if(siz > 1024)
- throw("sys·newproc: too many args");
-
- lock(&sched);
-
- if((newg = gfget()) != nil){
- newg->status = Gwaiting;
- } else {
- newg = malg(4096);
- newg->status = Gwaiting;
- newg->alllink = allg;
- allg = newg;
- }
- stk = newg->stack0;
-
- newg->stackguard = stk+160;
-
- sp = stk + 4096 - 4*8;
- newg->stackbase = sp;
-
- sp -= siz;
- mcpy(sp, (byte*)&arg0, siz);
-
- sp -= sizeof(uintptr);
- *(byte**)sp = (byte*)sys·Goexit;
-
- sp -= sizeof(uintptr); // retpc used by gogo
- newg->sched.SP = sp;
- newg->sched.PC = fn;
-
- sched.gcount++;
- goidgen++;
- newg->goid = goidgen;
-
- readylocked(newg);
- unlock(&sched);
-
-//printf(" goid=%d\n", newg->goid);
-}
-
-#pragma textflag 7
-void
-sys·deferproc(int32 siz, byte* fn, byte* arg0)
-{
- Defer *d;
-
- d = mal(sizeof(*d) + siz - sizeof(d->args));
- d->fn = fn;
- d->sp = (byte*)&arg0;
- d->siz = siz;
- mcpy(d->args, d->sp, d->siz);
-
- d->link = g->defer;
- g->defer = d;
-}
-
-#pragma textflag 7
-void
-sys·deferreturn(int32 arg0)
-{
- // warning: jmpdefer knows the frame size
- // of this routine. dont change anything
- // that might change the frame size
- Defer *d;
- byte *sp;
-
- d = g->defer;
- if(d == nil)
- return;
- sp = (byte*)&arg0;
- if(d->sp != sp)
- return;
- mcpy(d->sp, d->args, d->siz);
- g->defer = d->link;
- jmpdefer(d->fn);
-}
-
void
tracebackothers(G *me)
{
@@ -634,29 +532,70 @@ sys·exitsyscall(void)
sys·Gosched();
}
-\
-//
-// the calling sequence for a routine tha
-// needs N bytes stack, A args.
-//
-// N1 = (N+160 > 4096)? N+160: 0
-// A1 = A
-//
-// if N <= 75
-// CMPQ SP, 0(R15)
-// JHI 4(PC)
-// MOVQ $(N1<<0) | (A1<<32)), AX
-// MOVQ AX, 0(R14)
-// CALL sys·morestack(SB)
-//
-// if N > 75
-// LEAQ (-N-75)(SP), AX
-// CMPQ AX, 0(R15)
-// JHI 4(PC)
-// MOVQ $(N1<<0) | (A1<<32)), AX
-// MOVQ AX, 0(R14)
-// CALL sys·morestack(SB)
-//
+/*
+ * stack layout parameters.
+ * known to linkers.
+ *
+ * g->stackguard is set to point StackGuard bytes
+ * above the bottom of the stack. each function
+ * compares its stack pointer against g->stackguard
+ * to check for overflow. to cut one instruction from
+ * the check sequence for functions with tiny frames,
+ * the stack is allowed to protrude StackSmall bytes
+ * below the stack guard. functions with large frames
+ * don't bother with the check and always call morestack.
+ * the sequences are:
+ *
+ * stack frame size <= StackSmall:
+ * CMPQ guard, SP
+ * JHI 3(PC)
+ * MOVQ m->morearg, $((frame << 32) | argsize)
+ * CALL sys.morestack(SB)
+ *
+ * stack frame size > StackSmall but < StackBig
+ * LEAQ (frame-StackSmall)(SP), R0
+ * CMPQ guard, R0
+ * JHI 3(PC)
+ * MOVQ m->morearg, $((frame << 32) | argsize)
+ * CALL sys.morestack(SB)
+ *
+ * stack frame size >= StackBig:
+ * MOVQ m->morearg, $((frame << 32) | argsize)
+ * CALL sys.morestack(SB)
+ *
+ * the bottom StackGuard - StackSmall bytes are important:
+ * there has to be enough room to execute functions that
+ * refuse to check for stack overflow, either because they
+ * need to be adjacent to the actual caller's frame (sys.deferproc)
+ * or because they handle the imminent stack overflow (sys.morestack).
+ *
+ * for example, sys.deferproc might call malloc,
+ * which does one of the above checks (without allocating a full frame),
+ * which might trigger a call to sys.morestack.
+ * this sequence needs to fit in the bottom section of the stack.
+ * on amd64, sys.morestack's frame is 40 bytes, and
+ * sys.deferproc's frame is 56 bytes. that fits well within
+ * the StackGuard - StackSmall = 128 bytes at the bottom.
+ * there may be other sequences lurking or yet to be written
+ * that require more stack. sys.morestack checks to make sure
+ * the stack has not completely overflowed and should
+ * catch such sequences.
+ */
+enum
+{
+ // byte offset of stack guard (g->stackguard) above bottom of stack.
+ StackGuard = 256,
+
+ // checked frames are allowed to protrude below the guard by
+ // this many bytes. this saves an instruction in the checking
+ // sequence when the stack frame is tiny.
+ StackSmall = 128,
+
+ // extra space in the frame (beyond the function for which
+ // the frame is allocated) is assumed not to be much bigger
+ // than this amount. it may not be used efficiently if it is.
+ StackBig = 4096,
+};
void
oldstack(void)
@@ -684,7 +623,7 @@ oldstack(void)
oldbase = (uint64)top->oldbase;
oldguard = (uint64)top->oldguard;
- stackfree((byte*)m->curg->stackguard - 512 - 160);
+ stackfree((byte*)m->curg->stackguard - StackGuard);
m->curg->stackbase = (byte*)oldbase;
m->curg->stackguard = (byte*)oldguard;
@@ -712,28 +651,22 @@ lessstack(void)
void
newstack(void)
{
-\tint32 siz1, siz2;
+\tint32 frame, args;
Stktop *top;
byte *stk, *sp;
void (*fn)(void);
-\tsiz1 = m->morearg & 0xffffffffLL;\n-\tsiz2 = (m->morearg>>32) & 0xffffLL;
+\tframe = m->morearg & 0xffffffffLL;\n+\targs = (m->morearg>>32) & 0xffffLL;
-// prints("newstack siz1=");
-// sys·printint(siz1);
-// prints(" siz2=");
-// sys·printint(siz2);
-// prints(" moresp=");
-// sys·printpointer(m->moresp);
-// prints("\n");
+// printf("newstack frame=%d args=%d moresp=%p\n", frame, args, m->moresp);
-\tif(siz1 < 4096)\n-\t\tsiz1 = 4096;\n-\tstk = stackalloc(siz1 + 1024);\n-\tstk += 512;
+\tif(frame < StackBig)\n+\t\tframe = StackBig;\n+\tframe += 1024;\t// for more functions, Stktop.\n+\tstk = stackalloc(frame);\
-\ttop = (Stktop*)(stk+siz1-sizeof(*top));
+\ttop = (Stktop*)(stk+frame-sizeof(*top));
\tm->curg->stackbase = (byte*)top;
-\tm->curg->stackguard = stk + 160;
+\tm->curg->stackguard = stk + StackGuard;
\tsp = (byte*)top;
-\tif(siz2 > 0) {\n-\t\tsiz2 = (siz2+7) & ~7;\n-\t\tsp -= siz2;\n-\t\tmcpy(sp, m->moresp+16, siz2);\
+\tif(args > 0) {\n+\t\t// Copy args. There have been two function calls\n+\t\t// since they got pushed, so skip over those return\n+\t\t// addresses.\n+\t\targs = (args+7) & ~7;\n+\t\tsp -= args;\n+\t\tmcpy(sp, m->moresp+2*sizeof(uintptr), args);\
\t}\
\tg = m->curg;
-\tfn = (void(*)(void))(*(uint64*)m->moresp);
+\t// sys.morestack's return address\n+\tfn = (void(*)(void))(*(uintptr*)m->moresp);\
-// prints("fn=");
-// sys·printpointer(fn);
-// prints("\n");
+// printf("fn=%p\n", fn);
\tsetspgoto(sp, fn, retfromnewstack);\
@@ -769,13 +705,133 @@ sys·morestack(uint64 u)
{\n \twhile(g == m->g0) {\n \t\t// very bad news\n-\t\t*(int32*)123 = 123;\n+\t\t*(int32*)0x1001 = 123;\n+\t}\n+\n+\t// Morestack's frame is about 0x30 bytes on amd64.\n+\t// If that the frame ends below the stack bottom, we've already\n+\t// overflowed. Stop right now.\n+\twhile((byte*)&u - 0x30 < m->curg->stackguard - StackGuard) {\n+\t\t// very bad news\n+\t\t*(int32*)0x1002 = 123;\
\t}\
\n \tg = m->g0;\
\tm->moresp = (byte*)(&u-1);\
\tsetspgoto(m->sched.SP, newstack, nil);\
\n-\t*(int32*)234 = 123;\t// never return\n+\t*(int32*)0x1003 = 123;\t// never return\n+}\n+\n+G*\n+malg(int32 stacksize)\n+{\n+\tG *g;\n+\tbyte *stk;\n+\n+\tg = malloc(sizeof(G));\n+\tstk = stackalloc(stacksize + StackGuard);\n+\tg->stack0 = stk;\n+\tg->stackguard = stk + StackGuard;\n+\tg->stackbase = stk + StackGuard + stacksize;\n+\treturn g;\n+}\n+\n+/*\n+ * Newproc and deferproc need to be textflag 7\n+ * (no possible stack split when nearing overflow)\n+ * because they assume that the arguments to fn\n+ * are available sequentially beginning at &arg0.\n+ * If a stack split happened, only the one word\n+ * arg0 would be copied. It's okay if any functions\n+ * they call split the stack below the newproc frame.\n+ */\n+#pragma textflag 7\n+void\n+sys·newproc(int32 siz, byte* fn, byte* arg0)\n+{\n+\tbyte *stk, *sp;\n+\tG *newg;\n+\n+//printf("newproc siz=%d fn=%p", siz, fn);\n+\n+\tsiz = (siz+7) & ~7;\n+\tif(siz > 1024)\n+\t\tthrow("sys·newproc: too many args");\n+\n+\tlock(&sched);\n+\n+\tif((newg = gfget()) != nil){\n+\t\tnewg->status = Gwaiting;\n+\t} else {\n+\t\tnewg = malg(4096);\n+\t\tnewg->status = Gwaiting;\n+\t\tnewg->alllink = allg;\n+\t\tallg = newg;\n+\t}\n+\tstk = newg->stack0;\n+\n+\tnewg->stackguard = stk+StackGuard;\n+\n+\tsp = stk + 4096 - 4*8;\n+\tnewg->stackbase = sp;\n+\n+\tsp -= siz;\n+\tmcpy(sp, (byte*)&arg0, siz);\n+\n+\tsp -= sizeof(uintptr);\n+\t*(byte**)sp = (byte*)sys·Goexit;\n+\n+\tsp -= sizeof(uintptr);\t// retpc used by gogo\n+\tnewg->sched.SP = sp;\n+\tnewg->sched.PC = fn;\n+\n+\tsched.gcount++;\n+\tgoidgen++;\n+\tnewg->goid = goidgen;\n+\n+\treadylocked(newg);\n+\tunlock(&sched);\n+\n+//printf(" goid=%d\n", newg->goid);\n+}\n+\n+#pragma textflag 7\n+void\n+sys·deferproc(int32 siz, byte* fn, byte* arg0)\n+{\n+\tDefer *d;\n+\n+\td = malloc(sizeof(*d) + siz - sizeof(d->args));\n+\td->fn = fn;\n+\td->sp = (byte*)&arg0;\n+\td->siz = siz;\n+\tmcpy(d->args, d->sp, d->siz);\n+\n+\td->link = g->defer;\n+\tg->defer = d;\n+}\n+\n+#pragma textflag 7\n+void\n+sys·deferreturn(int32 arg0)\n+{\n+\t// warning: jmpdefer knows the frame size\n+\t// of this routine. dont change anything\n+\t// that might change the frame size\n+\tDefer *d;\n+\tbyte *sp;\n+\n+\td = g->defer;\n+\tif(d == nil)\n+\t\treturn;\n+\tsp = (byte*)&arg0;\n+\tif(d->sp != sp)\n+\t\treturn;\n+\tmcpy(d->sp, d->args, d->siz);\n+\tg->defer = d->link;\n+\tsp = d->fn;\n+\tfree(d);\n+\tjmpdefer(sp);\n }\n \n```
## コアとなるコードの解説
### `src/cmd/6l/pass.c` および `src/cmd/8l/pass.c` の変更
これらのファイルはGoのリンカ(amd64と386アーキテクチャ用)の一部であり、関数呼び出しにおけるスタックチェックコードの生成を担当しています。
* **列挙型の導入**: `StackSmall`と`StackBig`という列挙型が導入され、以前はハードコードされていた`75`と`4096`というマジックナンバーが置き換えられました。これにより、リンカが生成するコードがランタイムのスタック定数と同期され、コードの整合性と保守性が向上しました。
* **スタックチェックロジックの更新**:
* `if(autoffset < 4096)`が`if(autoffset < StackBig)`に、`if(autoffset <= 75)`が`if(autoffset <= StackSmall)`に変更されました。これは、関数が`morestack`を呼び出すべきかどうかを判断する基準が、新しい定数に基づいて行われることを意味します。
* `p->from.offset = -(autoffset-75)`が`p->from.offset = -(autoffset-StackSmall)`に変更されました。これは、スタックが`StackSmall`バイトだけスタックガードの下に食い込むことを許容する計算を、新しい`StackSmall`の値に基づいて行うことを示しています。これにより、小さなスタックフレームを持つ関数がスタックチェックを行う際の命令数を削減しつつ、安全性を確保します。
これらの変更は、リンカがGoランタイムのスタック管理ポリシーに厳密に従ったスタックチェックコードを生成することを保証し、スタックオーバーフローの発生を防ぐ上で不可欠です。
### `src/runtime/iface.c` の変更
このファイルはGoのインターフェースの実装に関連しています。
* **`mal`から`malloc`への変更**: `itype`関数と`fakesigt`関数内で、`mal()`によるメモリ割り当てが`malloc()`に変更されました。
* `Itab`(インターフェースの型情報)や`Sigt`(シグネチャ型)のようなランタイム内部の重要なデータ構造のメモリ割り当てが、ガベージコレクションの対象から外れ、手動で管理されるようになりました。
* これにより、これらのクリティカルなデータ構造の割り当てと解放が、ガベージコレクションの実行タイミングに左右されなくなり、ランタイムのパフォーマンスと予測可能性が向上します。特に、インターフェースの型解決は頻繁に行われる可能性があるため、この変更は重要です。
### `src/runtime/proc.c` の変更
このファイルはGoroutineのスケジューリングとスタック管理の核心部分です。
* **スタック定数の定義と詳細な説明**:
* `StackGuard = 256`、`StackSmall = 128`、`StackBig = 4096`という新しい列挙型が定義されました。
* これらの定数に関する詳細なコメントが追加され、スタックレイアウトのパラメータ、スタックオーバーフローチェックの仕組み、そして`StackGuard - StackSmall`(128バイト)の領域が`sys·deferproc`や`sys·morestack`のようなクリティカルな関数が安全に実行されるためにいかに重要であるかが説明されています。
* 特に、`sys·deferproc`が`malloc`を呼び出し、それがさらに`sys·morestack`をトリガーする可能性のある複雑なシナリオが考慮されており、この一連の処理がスタックの最下部領域に収まるようにスタックガードのサイズが調整されたことが示唆されています。
* **`malg`関数の変更**:
* `malg`関数(Goroutineのスタックを割り当てる関数)において、スタックガードの計算が`stk + 160`から`stk + StackGuard`に変更されました。これにより、新しい`StackGuard`の値(256バイト)が適用され、Goroutineの初期スタック割り当て時に十分なスタックガード領域が確保されるようになりました。
* **`sys·newproc`関数の変更**:
* `sys·newproc`関数(新しいGoroutineを作成する関数)において、新しいGoroutineのスタックガードの設定が`newg->stackguard = stk+160`から`newg->stackguard = stk+StackGuard`に変更されました。これも`StackGuard`の新しい値が適用されることを意味します。
* この関数は`#pragma textflag 7`(`NOSPLIT`)が引き続き適用されており、引数が連続したメモリ領域にあることを前提としているため、スタック分割が禁止されています。
* **`sys·deferproc`関数の変更**:
* `sys·deferproc`関数(`defer`文を処理する関数)において、`Defer`構造体の割り当てが`mal()`から`malloc()`に変更されました。これにより、`Defer`構造体のメモリ管理がガベージコレクションから切り離され、より直接的に制御されるようになりました。
* この関数も`#pragma textflag 7`(`NOSPLIT`)が適用されており、そのクリティカルな性質が強調されています。
* **`sys·deferreturn`関数の変更**:
* `sys·deferreturn`関数(遅延実行された関数からリターンする際に呼び出される関数)において、`Defer`構造体の解放に`free(d)`が明示的に追加されました。これは、`sys·deferproc`で`malloc`された`Defer`構造体が、対応する`sys·deferreturn`で確実に解放されることを保証します。これにより、メモリリークを防ぎ、メモリ管理の整合性を保ちます。
* **`newstack`関数の変更**:
* `newstack`関数(スタックを拡張する際に呼び出される関数)において、スタックのサイズ計算とスタックガードの設定が変更されました。
* スタックの割り当てサイズが`siz1 + 1024`から`frame + 1024`に変更され、`siz1`が`frame`に、`siz2`が`args`にそれぞれ対応するようになりました。
* 新しいスタックのスタックガード設定が`m->curg->stackguard = stk + 160`から`m->curg->stackguard = stk + StackGuard`に変更されました。
* 引数のコピーロジックも、`m->moresp+16`から`m->moresp+2*sizeof(uintptr)`に変更され、`sys.morestack`の呼び出し元と`newstack`の間のスタックフレームのオフセットがより正確に考慮されるようになりました。
* **`sys·morestack`関数の変更**:
* `sys·morestack`関数(スタック拡張のトリガーとなる関数)に、スタックが完全にオーバーフローしていないことを確認するための追加のチェックが導入されました。`while((byte*)&u - 0x30 < m->curg->stackguard - StackGuard)`という条件が追加され、`morestack`自身のフレームがスタックガード領域の下に食い込みすぎていないかをチェックします。これにより、スタックが完全に破壊される前に異常を検知し、ランタイムのクラッシュを防ぐための最後の砦となります。
これらの変更は、Goランタイムのスタック管理メカニズムを根本的に強化し、特に`defer`やGoroutineの生成といったクリティカルな操作におけるスタックオーバーフローの脆弱性を解消することを目的としています。
## 関連リンク
このコミットに直接関連するGoのIssueやデザインドキュメントは、この時点では特定できませんでした。しかし、Goのスタック管理に関する一般的な情報は以下のリンクで参照できます。
* Goのスタック管理の進化(セグメントスタックから連続スタックへ)に関する記事:
* [Go's Execution Tracer - The Go Programming Language](https://go.dev/blog/go-execution-tracer) (Go 1.5以降のスタックトレースに関する記事ですが、スタックの概念に触れています)
* [Go: The Good, The Bad, and The Ugly - Stack Overflow](https://stackoverflow.com/questions/10205800/go-the-good-the-bad-and-the-ugly) (Goのスタックに関する議論が含まれる場合があります)
## 参考にした情報源リンク
* [Web search results for "Go runtime stack overflow bug 2009 deferproc morestack"](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEbU-SpjZXMoI8Oqgwhq6klv_-9KI6OH8cDmKYXOzA1LrqONUAVrT28KtFhhNQHLMVdvQgmcosv99DUdRdTNfWC6MNSosXil3o9skICS3NHD91HpgAAX-cCu71bQVEB4Vdn5YnJXp5FOtfUMnwiG6ebp2WiAyJcmwjqLqFulilgKhqUk54C4yoPwff2oQ==)
* [Web search results for "Go runtime stack management early versions"](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFZKYm3BvJgst4s1wlHL0DdXFeps0gFypMqhdVeRUhQmtyUWZcQsTuJW5rlG7SaE5nAAjUhRHab0NiGClI9xDWffqQ3-pJcx8aU1NElqIValdmfjjNnm5NV72R2BlfMsU3QiQef4po7-NBmRC2HtELO88do2w==)
* [Web search results for "Go runtime mal vs malloc early Go"](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQETiPI3lzG7nsjWwRTHurX_pKerKZa-HPCU-Yekt5rx4sQCPpAOQ6vw-j4Nklg2rFeGXtVFgRtTZEZnLlikEPeuVHiE7eNflKD8AtAs_Kj6YcqSWZg15UjoJ2XkxMFsZsuz6W5T3m1ylxXnbgPSmw==)
* [Web search results for "Go runtime textflag 7"](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHcl7PEuheYD9Vb_lW1wMJg5cdoIOt2SuL_waFhhSZWZtGNTMqG0brbe_K9qIKD0kCTyGr97dp8kfUZPRl4AM3SluxRLaijbFFIKAWXnOzpEUK_0onlN0qvh1IMFlsaryo=)