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

[インデックス 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がスイープされるとそのsweepgenmheap.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ルーチンによってアクセスされる可能性がありました。これにより、メモリ破損の潜在的なリスクが生じていました。

修正の主なポイントは以下の通りです。

  1. MSpan_EnsureSweptのプリエンプション無効化の強制:

    • mgc0.cMSpan_EnsureSwept関数に、呼び出し元がプリエンプションを無効にしていることを確認するチェックが追加されました。具体的には、m->locks == 0 && m->mallocing == 0の場合にruntime·throwが呼び出され、プログラムがクラッシュします。これは、MSpan_EnsureSweptがクリティカルセクション内で、かつプリエンプションが確実に無効化された状態で実行されるべきであるという新しい不変条件を強制するものです。
    • 以前はMSpan_EnsureSwept内部でm->locks++m->locks--を行っていましたが、これは削除されました。これにより、MSpan_EnsureSwept自身がプリエンプションの有効/無効を制御するのではなく、呼び出し元がその責任を負うという設計に変更されました。
  2. addspecialremovespecialにおけるm->locksの管理:

    • mheap.caddspecial関数とremovespecial関数は、MSpan_EnsureSweptを呼び出す前にm->locks++を実行し、MSpan_EnsureSweptの呼び出しとspan->specialLockの解放後にm->locks--を実行するように変更されました。
    • これにより、addspecialremovespecialMSpan_EnsureSweptを呼び出す際には、常にプリエンプションが無効化された状態(m->locks > 0)が保証されます。これは、MSpan_EnsureSweptの新しい要件を満たすものです。
  3. freeallspecialsにおけるスイープ状態のチェック:

    • mheap.cruntime·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を適切に操作するように変更されました。これにより、removespecialMSpan_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ランタイムの概念理解のため)