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

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

このコミットは、GoランタイムにおけるCPUプロファイリング機能のバグ修正に関するものです。具体的には、プロファイラがオフになっている状態でCPUプロファイリングを停止しようとした際に発生するパニック(panic)を修正します。

影響を受けるファイルは以下の通りです。

  • src/pkg/runtime/cpuprof.c: CPUプロファイリングのロジックをC言語で実装しているファイルです。今回の修正の主要な変更箇所が含まれます。
  • src/pkg/runtime/runtime_test.go: Goランタイムのテストファイルです。今回の修正に関連する新しいテストケースが追加されています。

コミット

commit aeeda707ffdcd29efdec510ffe40061384b0dfdf
Author: Emil Hessman <c.emil.hessman@gmail.com>
Date:   Mon Jan 6 09:53:55 2014 -0800

    runtime: Fix panic when trying to stop CPU profiling with profiler turned off
    
    Fixes #7063.
    
    R=golang-codereviews, iant
    CC=golang-codereviews
    https://golang.org/cl/47950043

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

https://github.com/golang/go/commit/aeeda707ffdcd29efdec510ffe40061384b0dfdf

元コミット内容

runtime: Fix panic when trying to stop CPU profiling with profiler turned off

Fixes #7063.

R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/47950043

変更の背景

このコミットの背景には、GoランタイムのCPUプロファイリング機能における特定のシナリオでの安定性の問題がありました。GoのCPUプロファイラは、プログラムの実行中にCPUがどの関数に時間を費やしているかをサンプリングすることで、パフォーマンスのボトルネックを特定するのに役立ちます。

問題は、プロファイラが既に停止している(または一度も開始されていない)状態で、プロファイリングを停止する操作(runtime.SetCPUProfileRate(0)など)が呼び出された場合に発生していました。この状況下で、プロファイラの内部状態を管理するprof構造体へのアクセスが不正な状態で行われ、結果としてGoプログラムがパニックを起こしてクラッシュするというバグが存在していました。

このパニックは、プロファイラがアクティブでないにもかかわらず、プロファイラの状態を示すprof->onフラグをチェックしようとした際に、profポインタ自体がnilである可能性を考慮していなかったために発生しました。この修正は、このようなエッジケースを適切に処理し、Goプログラムの堅牢性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムおよびプロファイリングに関する基本的な知識が必要です。

  1. CPUプロファイリング: CPUプロファイリングは、プログラムがCPU時間をどこで消費しているかを分析する手法です。Goでは、runtime/pprofパッケージを通じてCPUプロファイリング機能が提供されています。これは、一定の間隔で実行中のスタックトレースをサンプリングし、どの関数がCPUを最も多く使用しているかを特定することで機能します。 プロファイリングを開始するには、通常pprof.StartCPUProfileを呼び出し、停止するにはpprof.StopCPUProfileを呼び出します。これらの関数は内部的にruntime.SetCPUProfileRateを呼び出してプロファイリングのサンプリングレートを設定します。

  2. runtime.SetCPUProfileRate: これはGoランタイム内部の関数で、CPUプロファイリングのサンプリングレートを設定するために使用されます。引数hzは、1秒あたりのサンプリング回数を指定します。hzが0の場合、プロファイリングは停止されます。

  3. panic(パニック): Goにおけるパニックは、プログラムの実行中に回復不可能なエラーが発生したことを示すメカニズムです。パニックが発生すると、通常のプログラムフローは中断され、遅延関数(defer)が実行された後、プログラムはクラッシュします。今回のバグは、プロファイリングの停止処理中にランタイム内部でパニックが発生するというものでした。これは、通常ユーザーが直接扱うエラーではなく、ランタイム自体のバグを示しています。

  4. ポインタとnil: Go(およびC言語)において、ポインタはメモリ上の特定のアドレスを指す変数です。ポインタが何も指していない状態をnil(Go)またはNULL(C)と呼びます。nilポインタをデリファレンス(ポインタが指す先の値にアクセスしようとすること)すると、ランタイムエラーやパニックが発生します。今回のバグは、まさにnilポインタのデリファレンスが原因で引き起こされていました。

技術的詳細

この修正の核心は、CPUプロファイリングの状態を管理するprofという内部構造体へのアクセスを安全に行うことです。

GoランタイムのCPUプロファイリングは、内部的にprofというグローバルな(またはそれに近い)構造体によって管理されています。この構造体には、プロファイリングが現在アクティブであるかどうかを示すonというフラグが含まれています。

修正前のコードでは、runtime·SetCPUProfileRate関数内でプロファイリングを停止するロジックにおいて、以下のような条件分岐がありました。

} else if(prof->on) {
    runtime·setcpuprofilerate(nil, 0);
    prof->on = false;
}

このelse if(prof->on)という条件は、プロファイリングを停止する際に「もしプロファイラがオンであれば」という意図で書かれています。しかし、問題はprofポインタ自体がnilである可能性がある場合に、prof->onにアクセスしようとすると、nilポインタのデリファレンスが発生し、パニックを引き起こす点にありました。

profnilになるシナリオとしては、プロファイリングが一度も開始されていない、または以前に停止された後に完全にリセットされた場合などが考えられます。このような状況でruntime.SetCPUProfileRate(0)が呼び出されると、profnilであるにもかかわらずprof->onにアクセスしようとしてパニックが発生していました。

今回の修正では、この条件にprof != nilというチェックを追加することで、この問題を解決しています。

} else if(prof != nil && prof->on) {
    runtime·setcpuprofilerate(nil, 0);
    prof->on = false;
}

これにより、profnilである場合はprof->onへのアクセスが試みられる前に条件全体がfalseと評価されるため、安全に処理がスキップされ、パニックが回避されます。これは、C言語における論理AND演算子(&&)のショートサーキット評価(左側のオペランドがfalseであれば右側のオペランドは評価されない)を利用した典型的な安全策です。

また、この修正を検証するために、runtime_test.goに新しいテストケースTestStopCPUProfilingWithProfilerOffが追加されました。このテストは、プロファイリングを一度も開始せずにSetCPUProfileRate(0)を呼び出すことで、修正が正しく機能し、パニックが発生しないことを確認します。

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

このコミットにおけるコアとなるコードの変更箇所は以下の2ファイルです。

  1. src/pkg/runtime/cpuprof.c:

    --- a/src/pkg/runtime/cpuprof.c
    +++ b/src/pkg/runtime/cpuprof.c
    @@ -168,7 +168,7 @@ runtime·SetCPUProfileRate(intgo hz)
     		runtime·noteclear(&prof->wait);
     
     		runtime·setcpuprofilerate(tick, hz);
    -	} else if(prof->on) {
    +	} else if(prof != nil && prof->on) {
     		runtime·setcpuprofilerate(nil, 0);
     		prof->on = false;
    

    この変更は、runtime·SetCPUProfileRate関数内の条件文にprof != nilというチェックを追加しています。

  2. src/pkg/runtime/runtime_test.go:

    --- a/src/pkg/runtime/runtime_test.go
    +++ b/src/pkg/runtime/runtime_test.go
    @@ -126,3 +126,8 @@ func TestRuntimeGogoBytes(t *testing.T) {
     
     	t.Fatalf("go tool nm did not report size for runtime.gogo")
     }
    +
    +// golang.org/issue/7063
    +func TestStopCPUProfilingWithProfilerOff(t *testing.T) {
    +	SetCPUProfileRate(0)
    +}
    

    この変更は、TestStopCPUProfilingWithProfilerOffという新しいテスト関数を追加しています。

コアとなるコードの解説

src/pkg/runtime/cpuprof.c の変更

変更前のコード:

} else if(prof->on) {

この行は、profというポインタが指す構造体のonフィールドにアクセスしようとしています。profnil(C言語ではNULL)である場合、このアクセスは不正なメモリ参照となり、プログラムがクラッシュする原因となります。

変更後のコード:

} else if(prof != nil && prof->on) {

この変更では、条件式にprof != nilというチェックが追加されています。C言語の論理AND演算子&&はショートサーキット評価を行うため、もしprofnilであれば、prof->onの部分は評価されません。これにより、nilポインタのデリファレンスが安全に回避され、パニックの発生を防ぐことができます。このコードブロックは、プロファイリングが既に停止している場合に、プロファイリングを停止する操作が呼び出された際の処理を安全に行うためのものです。

src/pkg/runtime/runtime_test.go の変更

// golang.org/issue/7063
func TestStopCPUProfilingWithProfilerOff(t *testing.T) {
	SetCPUProfileRate(0)
}

この新しいテスト関数は、golang.org/issue/7063で報告されたバグを再現し、修正が正しく機能することを確認するために追加されました。 テストの内容は非常にシンプルで、SetCPUProfileRate(0)を呼び出すだけです。SetCPUProfileRate(0)はCPUプロファイリングを停止する関数ですが、このテストでは事前にプロファイリングを開始していません。つまり、プロファイラがオフの状態から停止を試みるという、まさにバグが発生していたシナリオを再現しています。 このテストがパニックを起こさずに正常に完了すれば、バグが修正されたことを意味します。Goのテストフレームワークは、テスト関数内でパニックが発生した場合にテストを失敗とマークするため、このようなシンプルなテストで十分です。

関連リンク

  • Go issue #7063: コミットメッセージに記載されているイシュートラッカーの番号です。このコミットが解決した問題を示しています。
  • Go CL 47950043: このコミットに対応するGoのコードレビューシステム(Gerrit)のチェンジリスト番号です。

参考にした情報源リンク

  • コミット aeeda707ffdcd29efdec510ffe40061384b0dfdf の内容
  • Go言語のCPUプロファイリングに関する一般的な知識
  • C言語におけるポインタとNULLチェック、論理演算子のショートサーキット評価に関する一般的な知識
  • Go言語のパニックに関する一般的な知識