[インデックス 18621] ファイルの概要
このコミットは、Goランタイムにおける潜在的なメモリ破損の修正を目的としています。具体的には、ガベージコレクション(GC)のスイープ処理に関連するMSpan_EnsureSwept
関数の保証を強化し、メモリ管理の堅牢性を向上させています。
コミット
commit 6e612ae0f5b527660f0e1ae497d0ad8fbb6953c2
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Feb 24 20:53:20 2014 +0400
runtime: fix potential memory corruption
Reinforce the guarantee that MSpan_EnsureSwept actually ensures that the span is swept.
I have not observed crashes related to this, but I do not see why it can't crash as well.
LGTM=rsc
R=golang-codereviews
CC=golang-codereviews, khr, rsc
https://golang.org/cl/67990043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6e612ae0f5b527660f0e1ae497d0ad8fbb6953c2
元コミット内容
diff --git a/src/pkg/runtime/mgc0.c b/src/pkg/runtime/mgc0.c
index d34ba4c026..238a1e790e 100644
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1694,16 +1694,19 @@ runtime·MSpan_EnsureSwept(MSpan *s)
{
uint32 sg;
+ // Caller must disable preemption.
+ // Otherwise when this function returns the span can become unswept again
+ // (if GC is triggered on another goroutine).
+ if(m->locks == 0 && m->mallocing == 0)
+ runtime·throw("MSpan_EnsureSwept: m is not locked");
+
sg = runtime·mheap.sweepgen;
if(runtime·atomicload(&s->sweepgen) == sg)
return;
- m->locks++;
if(runtime·cas(&s->sweepgen, sg-2, sg-1)) {
runtime·MSpan_Sweep(s);
- m->locks--;
return;
}
- m->locks--;
// unfortunate condition, and we don't have efficient means to wait
while(runtime·atomicload(&s->sweepgen) != sg)
runtime·osyield();
diff --git a/src/pkg/runtime/mheap.c b/src/pkg/runtime/mheap.c
index 5c5a6fe164..ba46b6404e 100644
--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -653,6 +653,7 @@ addspecial(void *p, Special *s)
// Ensure that the span is swept.
// GC accesses specials list w/o locks. And it's just much safer.
+ m->locks++;
runtime·MSpan_EnsureSwept(span);
offset = (uintptr)p - (span->start << PageShift);
@@ -665,6 +666,7 @@ addspecial(void *p, Special *s)
while((x = *t) != nil) {
if(offset == x->offset && kind == x->kind) {
runtime·unlock(&span->specialLock);
+ m->locks--;
return false; // already exists
}
if(offset < x->offset || (offset == x->offset && kind < x->kind))
@@ -676,6 +678,7 @@ addspecial(void *p, Special *s)
s->next = x;
*t = s;
runtime·unlock(&span->specialLock);
+ m->locks--;
return true;
}
@@ -695,6 +698,7 @@ removespecial(void *p, byte kind)
// Ensure that the span is swept.
// GC accesses specials list w/o locks. And it's just much safer.
+ m->locks++;
runtime·MSpan_EnsureSwept(span);
offset = (uintptr)p - (span->start << PageShift);
@@ -707,11 +711,13 @@ removespecial(void *p, byte kind)
if(offset == s->offset && kind == s->kind) {
*t = s->next;
runtime·unlock(&span->specialLock);
+ m->locks--;
return s;
}
t = &s->next;
}
runtime·unlock(&span->specialLock);
+ m->locks--;
return nil;
}
@@ -805,6 +811,8 @@ runtime·freeallspecials(MSpan *span, void *p, uintptr size)
Special *s, **t, *list;
uintptr offset;
+ if(span->sweepgen != runtime·mheap.sweepgen)
+ runtime·throw("runtime: freeallspecials: unswept span");
// first, collect all specials into the list; then, free them
// this is required to not cause deadlock between span->specialLock and proflock
list = nil;
変更の背景
このコミットは、Goランタイムにおける潜在的なメモリ破損の問題に対処しています。Goのガベージコレクタ(GC)は、メモリを効率的に管理するために「スイープ」というフェーズを持っています。スイープフェーズでは、GCは不要になったオブジェクトが占めていたメモリ領域を再利用可能にします。
MSpan_EnsureSwept
関数は、特定のメモリ領域(MSpan
)がGCによって確実にスイープされていることを保証する役割を担っています。しかし、この関数が呼び出された際に、Goランタイムのプリエンプション(横取り)機構によって処理が中断される可能性があり、その結果、MSpan
が完全にスイープされる前にアクセスされてしまうという潜在的な競合状態が存在しました。
コミットメッセージには「この問題に関連するクラッシュは観察されていないが、クラッシュしない理由もない」と述べられており、これは理論的にはメモリ破損を引き起こす可能性があるものの、実際の運用環境での再現が困難であったことを示唆しています。このコミットは、このような潜在的な問題を未然に防ぎ、ランタイムの堅牢性を高めることを目的としています。
前提知識の解説
このコミットの理解には、以下のGoランタイムの概念に関する知識が不可欠です。
- Goランタイム: Goプログラムの実行を管理する低レベルのシステム。ガベージコレクション、スケジューリング、メモリ管理などを担当します。
- ガベージコレクション (GC): Goの自動メモリ管理システム。不要になったメモリを自動的に解放し、再利用可能にします。GCは複数のフェーズ(マーク、スイープなど)で構成されます。
- MSpan: Goランタイムのメモリ管理における基本的な単位。連続したページ(通常は8KB)の集合であり、特定のサイズのオブジェクトを割り当てるために使用されます。各
MSpan
は、その状態(例えば、スイープ済みか否か)を追跡します。 - スイープ (Sweep): GCのフェーズの一つで、マークフェーズで到達不能と判断されたオブジェクトが占めるメモリを解放し、
MSpan
を再利用可能な状態に戻します。 sweepgen
:mheap
(Goのヒープ全体を管理する構造体)と各MSpan
が持つ世代カウンタ。GCが実行されるたびにmheap.sweepgen
が増加し、MSpan
がスイープされるとそのsweepgen
がmheap.sweepgen
と一致するように更新されます。これにより、MSpan
がどのGCサイクルでスイープされたかを追跡します。m
(Machine): GoランタイムにおけるOSスレッドを表す構造体。各m
は、Goルーチンを実行するためのコンテキストを提供します。m->locks
:m
構造体内のカウンタ。このカウンタが0より大きい場合、現在のOSスレッド(m
)上でのGoルーチンのプリエンプション(横取り)が無効になります。これは、ランタイムのクリティカルセクションで、Goルーチンが中断されることによる競合状態やデッドロックを防ぐために使用されます。- プリエンプション (Preemption): Goスケジューラが、実行中のGoルーチンを中断し、別のGoルーチンにCPUを割り当てるプロセス。これにより、並行性が実現されます。しかし、ランタイムの内部状態を操作するようなクリティカルな処理中にプリエンプションが発生すると、データの一貫性が損なわれる可能性があります。
runtime·atomicload
/runtime·cas
: Goランタイムが提供するアトミック操作。atomicload
はメモリ位置から値をアトミックに読み込み、cas
(Compare-And-Swap) は指定されたメモリ位置の値を、現在の値が期待値と一致する場合にのみ新しい値にアトミックに更新します。これらは競合状態を防ぐために使用されます。runtime·throw
: Goランタイム内部で回復不能なエラーが発生した場合に、プログラムをクラッシュさせる関数。これは通常、ランタイムの不変条件が破られたことを示すために使用されます。addspecial
/removespecial
:mheap.c
内の関数で、特定のメモリ領域(MSpan
)に「スペシャル」なオブジェクト(例えば、ファイナライザを持つオブジェクトやプロファイリング情報を持つオブジェクト)を追加したり削除したりする際に使用されます。これらの操作は、GCのスイープ状態に依存します。
技術的詳細
このコミットの核心は、MSpan_EnsureSwept
関数が常にその目的を達成することを保証することにあります。以前の実装では、MSpan_EnsureSwept
が呼び出された際に、その処理中にプリエンプションが発生し、MSpan
が完全にスイープされる前に他のGoルーチンによってアクセスされる可能性がありました。これにより、メモリ破損の潜在的なリスクが生じていました。
修正の主なポイントは以下の通りです。
-
MSpan_EnsureSwept
のプリエンプション無効化の強制:mgc0.c
のMSpan_EnsureSwept
関数に、呼び出し元がプリエンプションを無効にしていることを確認するチェックが追加されました。具体的には、m->locks == 0 && m->mallocing == 0
の場合にruntime·throw
が呼び出され、プログラムがクラッシュします。これは、MSpan_EnsureSwept
がクリティカルセクション内で、かつプリエンプションが確実に無効化された状態で実行されるべきであるという新しい不変条件を強制するものです。- 以前は
MSpan_EnsureSwept
内部でm->locks++
とm->locks--
を行っていましたが、これは削除されました。これにより、MSpan_EnsureSwept
自身がプリエンプションの有効/無効を制御するのではなく、呼び出し元がその責任を負うという設計に変更されました。
-
addspecial
とremovespecial
におけるm->locks
の管理:mheap.c
のaddspecial
関数とremovespecial
関数は、MSpan_EnsureSwept
を呼び出す前にm->locks++
を実行し、MSpan_EnsureSwept
の呼び出しとspan->specialLock
の解放後にm->locks--
を実行するように変更されました。- これにより、
addspecial
とremovespecial
がMSpan_EnsureSwept
を呼び出す際には、常にプリエンプションが無効化された状態(m->locks > 0
)が保証されます。これは、MSpan_EnsureSwept
の新しい要件を満たすものです。
-
freeallspecials
におけるスイープ状態のチェック:mheap.c
のruntime·freeallspecials
関数に、処理対象のMSpan
が現在のsweepgen
と一致しているか(つまり、適切にスイープされているか)を確認するチェックが追加されました。if(span->sweepgen != runtime·mheap.sweepgen)
の場合にruntime·throw
が呼び出されます。これは、freeallspecials
が呼び出される時点でMSpan
が完全にスイープされているべきであるという、ランタイムの別の不変条件を強化するものです。
これらの変更により、MSpan_EnsureSwept
が常に安全なコンテキストで実行され、MSpan
のスイープ状態に関する不整合が防止されることで、潜在的なメモリ破損のリスクが排除されます。
コアとなるコードの変更箇所
src/pkg/runtime/mgc0.c
--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1694,16 +1694,19 @@ runtime·MSpan_EnsureSwept(MSpan *s)
{
uint32 sg;
+ // Caller must disable preemption.
+ // Otherwise when this function returns the span can become unswept again
+ // (if GC is triggered on another goroutine).
+ if(m->locks == 0 && m->mallocing == 0)
+ runtime·throw("MSpan_EnsureSwept: m is not locked");
+
sg = runtime·mheap.sweepgen;
if(runtime·atomicload(&s->sweepgen) == sg)
return;
- m->locks++;
if(runtime·cas(&s->sweepgen, sg-2, sg-1)) {
runtime·MSpan_Sweep(s);
- m->locks--;
return;
}
- m->locks--;
// unfortunate condition, and we don't have efficient means to wait
while(runtime·atomicload(&s->sweepgen) != sg)
runtime·osyield();
src/pkg/runtime/mheap.c
--- a/src/pkg/runtime/mheap.c
+++ b/src/pkg/runtime/mheap.c
@@ -653,6 +653,7 @@ addspecial(void *p, Special *s)
// Ensure that the span is swept.
// GC accesses specials list w/o locks. And it's just much safer.
+ m->locks++;
runtime·MSpan_EnsureSwept(span);
offset = (uintptr)p - (span->start << PageShift);
@@ -665,6 +666,7 @@ addspecial(void *p, Special *s)
while((x = *t) != nil) {
if(offset == x->offset && kind == x->kind) {
runtime·unlock(&span->specialLock);
+ m->locks--;
return false; // already exists
}
if(offset < x->offset || (offset == x->offset && kind < x->kind))
@@ -676,6 +678,7 @@ addspecial(void *p, Special *s)
s->next = x;
*t = s;
runtime·unlock(&span->specialLock);
+ m->locks--;
return true;
}
@@ -695,6 +698,7 @@ removespecial(void *p, byte kind)
// Ensure that the span is swept.
// GC accesses specials list w/o locks. And it's just much safer.
+ m->locks++;
runtime·MSpan_EnsureSwept(span);
offset = (uintptr)p - (span->start << PageShift);
@@ -707,11 +711,13 @@ removespecial(void
if(offset == s->offset && kind == s->kind) {
*t = s->next;
runtime·unlock(&span->specialLock);
+ m->locks--;
return s;
}
t = &s->next;
}
runtime·unlock(&span->specialLock);
+ m->locks--;
return nil;
}
@@ -805,6 +811,8 @@ runtime·freeallspecials(MSpan *span, void *p, uintptr size)
Special *s, **t, *list;
uintptr offset;
+ if(span->sweepgen != runtime·mheap.sweepgen)
+ runtime·throw("runtime: freeallspecials: unswept span");
// first, collect all specials into the list; then, free them
// this is required to not cause deadlock between span->specialLock and proflock
list = nil;
コアとなるコードの解説
src/pkg/runtime/mgc0.c
の変更点
-
プリエンプションチェックの追加:
+ // Caller must disable preemption. + // Otherwise when this function returns the span can become unswept again + // (if GC is triggered on another goroutine). + if(m->locks == 0 && m->mallocing == 0) + runtime·throw("MSpan_EnsureSwept: m is not locked");
このコードは、
MSpan_EnsureSwept
が呼び出された時点で、現在のOSスレッド(m
)のプリエンプションが無効になっていることを強制します。m->locks
が0の場合、プリエンプションは有効な状態です。m->mallocing
はメモリ割り当て中であることを示し、この場合もプリエンプションは無効化されているべきです。もしプリエンプションが無効化されていない状態でこの関数が呼び出された場合、runtime·throw
が実行され、プログラムがクラッシュします。これは、MSpan_EnsureSwept
が非常にクリティカルな操作であり、中断されるとメモリ状態の不整合を引き起こす可能性があるためです。 -
m->locks
操作の削除:- m->locks++; if(runtime·cas(&s->sweepgen, sg-2, sg-1)) { runtime·MSpan_Sweep(s); - m->locks--; return; } - m->locks--;
以前は
MSpan_EnsureSwept
関数内でm->locks
をインクリメント/デクリメントしていましたが、この変更によりそれらが削除されました。これは、プリエンプションの無効化の責任がMSpan_EnsureSwept
の呼び出し元に移されたことを意味します。MSpan_EnsureSwept
は、呼び出された時点で既にプリエンプションが無効化されていることを前提とするようになりました。
src/pkg/runtime/mheap.c
の変更点
-
addspecial
関数におけるm->locks
の追加:+ m->locks++; runtime·MSpan_EnsureSwept(span); // ... runtime·unlock(&span->specialLock); + m->locks--; return false; // already exists // ... runtime·unlock(&span->specialLock); + m->locks--; return true;
addspecial
関数は、MSpan_EnsureSwept
を呼び出す直前にm->locks++
を実行し、関連するロック(span->specialLock
)を解放した後にm->locks--
を実行するように変更されました。これにより、MSpan_EnsureSwept
の呼び出しからspecialLock
の解放までの間、プリエンプションが確実に無効化されます。これは、MSpan_EnsureSwept
の新しい要件を満たすための変更です。 -
removespecial
関数におけるm->locks
の追加:+ m->locks++; runtime·MSpan_EnsureSwept(span); // ... runtime·unlock(&span->specialLock); + m->locks--; return s; // ... runtime·unlock(&span->specialLock); + m->locks--; return nil;
removespecial
関数もaddspecial
と同様に、MSpan_EnsureSwept
の呼び出し前後にm->locks
を適切に操作するように変更されました。これにより、removespecial
がMSpan_EnsureSwept
を呼び出す際も、プリエンプションが無効化された安全なコンテキストが保証されます。 -
freeallspecials
関数におけるスイープ状態のチェック:+ if(span->sweepgen != runtime·mheap.sweepgen) + runtime·throw("runtime: freeallspecials: unswept span");
freeallspecials
関数は、MSpan
に関連するスペシャルオブジェクトを解放する前に、そのMSpan
が現在のGC世代(runtime·mheap.sweepgen
)で完全にスイープされていることを確認するチェックが追加されました。もしスイープされていないMSpan
が渡された場合、これはランタイムの不整合を示すため、runtime·throw
が実行されます。これは、メモリ破損を防ぐための追加の安全策です。
これらの変更は、Goランタイムのメモリ管理におけるクリティカルな部分の堅牢性を高め、潜在的な競合状態やメモリ破損のリスクを低減することを目的としています。
関連リンク
参考にした情報源リンク
- コミットメッセージ (
./commit_data/18621.txt
) - Goソースコード (
src/pkg/runtime/mgc0.c
,src/pkg/runtime/mheap.c
) - Goランタイムのドキュメントおよび関連するGoのIssue(一般的なGoランタイムの概念理解のため)