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

[インデックス 16140] ファイルの概要

コミット

commit 5bb3a66a973ea87494b9197091e8c1f122080627
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Mon Apr 8 23:46:54 2013 +0200

    sync, sync/atomic: do not corrupt race detector after a nil dereference.
    
    The race detector uses a global lock to analyze atomic
    operations. A panic in the middle of the code leaves the
    lock acquired.
    
    Similarly, the sync package may leave the race detectro
    inconsistent when methods are called on nil pointers.
    
    R=golang-dev, r, minux.ma, dvyukov, rsc, adg
    CC=golang-dev
    https://golang.org/cl/7981043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/5bb3a66a973ea87494b9197091e8c1f122080627

元コミット内容

sync, sync/atomic: nilポインタのデリファレンス後にレース検出器を破損させないようにする。

レース検出器はアトミック操作を解析するためにグローバルロックを使用します。コードの途中でパニックが発生すると、ロックが取得されたままになります。

同様に、syncパッケージは、nilポインタに対してメソッドが呼び出されたときに、レース検出器を矛盾した状態にする可能性があります。

変更の背景

このコミットは、Go言語のランタイムにおけるレース検出器の堅牢性に関する重要なバグを修正するものです。具体的には、syncパッケージ(ミューテックス、条件変数、RWMutex、WaitGroupなど)やsync/atomicパッケージ(アトミック操作)の関数がnilポインタに対して呼び出された際に発生する問題に対処しています。

従来のGoの設計では、nilポインタに対するメソッド呼び出しやデリファレンスはランタイムパニックを引き起こします。しかし、レース検出器が有効な環境(-raceフラグ付きでビルドされたプログラム)では、このパニックの発生タイミングが問題となっていました。

レース検出器は、共有メモリへのアクセスを監視し、データ競合(レースコンディション)を検出するためのツールです。この検出器は、アトミック操作や同期プリミティブ(ミューテックスなど)の内部で、その状態を管理するためにグローバルなロックを使用します。

問題は、nilポインタデリファレンスによるパニックが、レース検出器がそのグローバルロックを取得した「後」に発生した場合に生じました。パニックが発生すると、通常のコードフローが中断され、レース検出器が取得したロックが解放されないままになってしまう可能性がありました。これにより、レース検出器がロックされたままになり、その後のアトミック操作や同期操作がすべてハングアップ(デッドロック)するという深刻な問題を引き起こしていました。

このコミットの目的は、nilポインタデリファレンスによるパニックが、レース検出器の内部ロックが取得される「前」に確実に発生するようにすることで、このデッドロック状態を回避することです。

前提知識の解説

Goのレース検出器 (Race Detector)

Go言語には、並行処理におけるデータ競合(レースコンディション)を検出するための組み込みツールであるレース検出器があります。これは、Goプログラムを-raceフラグ付きでビルドすることで有効になります。有効にすると、ランタイムがメモリアクセスを監視し、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも一方が書き込み操作である場合に警告を発します。レース検出器は、内部的にシャドウメモリやロックを使用して、これらのアクセスを追跡します。

syncパッケージ

Goの標準ライブラリに含まれるsyncパッケージは、基本的な同期プリミティブを提供します。

  • sync.Mutex: 排他ロック。複数のゴルーチンが同時に共有リソースにアクセスするのを防ぎます。Lock()でロックを取得し、Unlock()で解放します。
  • sync.RWMutex: 読み書きロック。複数の読み取りは許可しますが、書き込みは排他的に行われます。RLock()/RUnlock()(読み取りロック)とLock()/Unlock()(書き込みロック)があります。
  • sync.Cond: 条件変数。ミューテックスと組み合わせて使用され、特定の条件が満たされるまでゴルーチンを待機させたり、条件が満たされたときにゴルーチンを再開させたりします。Wait()Signal()Broadcast()などのメソッドがあります。
  • sync.WaitGroup: 複数のゴルーチンの完了を待つためのメカニズム。カウンタを持ち、Add()でカウンタを増やし、Done()で減らし、Wait()でカウンタがゼロになるまでブロックします。

これらの同期プリミティブは、レース検出器が有効な場合、その内部でレース検出器のAPI(raceDisable(), raceEnable(), raceRelease(), raceAcquire()など)を呼び出して、メモリアクセスの監視を調整します。

sync/atomicパッケージ

sync/atomicパッケージは、低レベルのアトミック操作を提供します。アトミック操作は、複数のゴルーチンから同時に実行されても、その操作全体が不可分(中断されない)であることを保証します。これにより、ロックを使用せずに特定の共有変数を安全に更新できます。例としては、AddInt32, CompareAndSwapInt32, LoadInt32, StoreInt32などがあります。これらの操作も、レース検出器が有効な場合は内部でレース検出器のAPIを呼び出します。

nilポインタデリファレンス

Goにおいて、nil値のポインタをデリファレンスしようとすると、ランタイムパニックが発生します。例えば、var p *int と宣言されたpがnilの状態で*p = 10のような操作を行うとパニックになります。これは、有効なメモリ位置を指していないポインタを介してメモリにアクセスしようとするためです。

panicrecover

Goのpanicは、プログラムの異常終了を示す組み込み関数です。通常、回復不可能なエラーが発生した場合に使用されます。panicが発生すると、現在のゴルーチンの実行が停止し、遅延関数(defer)が実行されながらスタックが巻き戻されます。recoverは、defer関数内で呼び出されることで、パニックから回復し、プログラムの実行を継続できるようにする組み込み関数です。

技術的詳細

このコミットの技術的な核心は、Goのレース検出器が内部的に使用するグローバルロックの取得タイミングと、nilポインタデリファレンスによるパニックの発生タイミングを調整することにあります。

レース検出器は、アトミック操作や同期プリミティブの監視を行う際に、その内部状態を保護するためにグローバルなセマフォ(mtx)を使用します。具体的には、runtime.RaceSemacquire(&mtx)でロックを取得し、runtime.RaceSemrelease(&mtx)で解放します。

問題は、syncsync/atomicパッケージの関数がnilポインタに対して呼び出された場合、その関数内でレース検出器のロックが取得された後にnilデリファレンスによるパニックが発生すると、ロックが解放されないままになり、レース検出器がデッドロック状態に陥る可能性があったことです。

この修正では、レース検出器のロックを取得する直前に、nilポインタデリファレンスを意図的に発生させるコードを追加しています。これは、_ = *val_ = c.m.state のような形式で記述されます。

  • _ = *val: これは、ポインタvalが指す値をデリファレンスし、その結果を破棄する操作です。もしvalがnilであれば、この行でパニックが発生します。
  • _ = c.m.state: syncパッケージの構造体(Cond, Mutex, RWMutex, WaitGroup)は、内部にsync.Mutexsync.RWMutexのインスタンス(通常はmwというフィールド名)を持っています。これらのミューテックスもまた、内部にstateというフィールドを持っています。_ = c.m.stateは、cがnilの場合にc.mがnilデリファレンスとなり、あるいはc.mがnilの場合にc.m.stateがnilデリファレンスとなり、パニックを引き起こします。

この「ダミーのデリファレンス」をレース検出器のロック取得の前に配置することで、以下の効果が得られます。

  1. 早期パニック: もし関数がnilポインタで呼び出された場合、レース検出器の内部ロックが取得される前に、このダミーデリファレンスによってパニックが確実に発生します。
  2. ロックの回避: パニックが早期に発生するため、レース検出器はグローバルロックを取得する機会がありません。
  3. デッドロックの防止: ロックが取得されていないため、パニックによってロックが解放されないという問題が発生せず、レース検出器がデッドロック状態に陥ることを防ぎます。

これにより、nilポインタデリファレンスによるパニックは引き続き発生しますが、レース検出器の健全性が保たれ、プログラム全体がハングアップする事態が回避されます。これは、Goのランタイムが予期せぬエラーに対してより堅牢になるための重要な改善です。

コアとなるコードの変更箇所

このコミットでは、主に以下のファイルにコードが追加されています。

  • src/pkg/runtime/race/testdata/atomic_test.go: nilポインタでのアトミック操作がレース検出器をデッドロックさせないことをテストする新しいテストケースTestNoRaceAtomicCrashが追加されました。
  • src/pkg/runtime/race/testdata/sync_test.go: nilポインタでのミューテックス操作がレース検出器を破損させないことをテストする新しいテストケースTestNoRaceNilMutexCrashが追加されました。
  • src/pkg/sync/atomic/race.go: sync/atomicパッケージ内のアトミック操作(CompareAndSwapUint32, CompareAndSwapUint64, CompareAndSwapPointer, CompareAndSwapUintptr, AddUint32, AddUint64, AddUintptr, LoadUint32, LoadUint64, LoadPointer, LoadUintptr, StoreUint32, StoreUint64, StorePointer, StoreUintptr)の各関数の冒頭に、引数として渡されたポインタのデリファレンス(例: _ = *val または _ = *addr)が追加されました。
  • src/pkg/sync/cond.go: sync.CondWait(), Signal(), Broadcast()メソッドの冒頭に、_ = c.m.stateが追加されました。
  • src/pkg/sync/mutex.go: sync.MutexUnlock()メソッドの冒頭に、_ = m.stateが追加されました。
  • src/pkg/sync/rwmutex.go: sync.RWMutexRLock(), RUnlock(), Lock(), Unlock()メソッドの冒頭に、_ = rw.w.stateが追加されました。
  • src/pkg/sync/waitgroup.go: sync.WaitGroupAdd(), Wait()メソッドの冒頭に、_ = wg.m.stateが追加されました。

コアとなるコードの解説

追加されたコードのパターンは非常にシンプルで、ほとんどが以下の形式です。

_ = *val // または _ = *addr

または

_ = c.m.state // または _ = m.state, _ = rw.w.state, _ = wg.m.state

これらの行は、Goのコンパイラによって最適化で削除されないように、デリファレンス結果をブランク識別子_に代入しています。これにより、ポインタがnilである場合に、この行でランタイムパニックが確実に発生します。

例えば、sync/atomic/race.goCompareAndSwapUint32関数では、以下のように変更されました。

--- a/src/pkg/sync/atomic/race.go
+++ b/src/pkg/sync/atomic/race.go
@@ -25,6 +25,7 @@ func CompareAndSwapInt32(val *int32, old, new int32) bool {
 }
 
 func CompareAndSwapUint32(val *uint32, old, new uint32) (swapped bool) {
+	_ = *val // 追加された行
 	swapped = false
 	runtime.RaceSemacquire(&mtx) // レース検出器のロック取得
 	runtime.RaceRead(unsafe.Pointer(val))

この変更により、valがnilの場合、_ = *valの行でパニックが発生し、その後のruntime.RaceSemacquire(&mtx)(レース検出器のグローバルロックを取得する処理)は実行されません。これにより、レース検出器がロックされたままになるというデッドロックの問題が回避されます。

同様に、sync/cond.goWait()メソッドでは、以下のように変更されました。

--- a/src/pkg/sync/cond.go
+++ b/src/pkg/sync/cond.go
@@ -57,6 +57,7 @@ func NewCond(l Locker) *Cond {
 //
 func (c *Cond) Wait() {
 	if raceenabled {
+		_ = c.m.state // 追加された行
 		raceDisable()
 	}
 	c.m.Lock()

sync.CondWait()メソッドは、cがnilの場合に呼び出されると、c.mがnilデリファレンスとなり、パニックが発生します。この_ = c.m.stateの追加により、cがnilの場合、raceDisable()が呼び出される前にパニックが発生します。raceDisable()はレース検出器の内部状態を操作する可能性があり、その前にパニックを発生させることで、レース検出器の不整合を防ぎます。

これらの変更は、Goのランタイムがnilポインタデリファレンスのような予期せぬ状況下でも、レース検出器の健全性を維持し、デッドロックを回避するための防御的なプログラミングの一例です。

関連リンク

参考にした情報源リンク

  • https://golang.org/cl/7981043 (Go Gerrit Code Review for this change)
  • Go言語の公式ドキュメントおよびソースコード
  • Go言語のレース検出器に関する一般的な情報源