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

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

このコミットは、Goランタイムにおける runtime.GoroutineProfile 関数がクラッシュする可能性のあるバグを修正するものです。特に、システムコールから復帰中のゴルーチンに対してプロファイルを取得する際に発生する可能性のある、スタックポインタの誤った参照に起因する問題を解決しています。この修正はGo 1.2.1の候補とされており、安定性向上に寄与します。

コミット

commit bc135f6492035024e778fe7dedb451ebaa06d3e8
Author: Russ Cox <rsc@golang.org>
Date:   Fri Dec 13 15:44:57 2013 -0500

    runtime: fix crash in runtime.GoroutineProfile
    
    This is a possible Go 1.2.1 candidate.
    
    Fixes #6946.
    
    R=iant, r
    CC=golang-dev
    https://golang.org/cl/41640043

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

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

元コミット内容

runtime: fix crash in runtime.GoroutineProfile

このコミットは、runtime.GoroutineProfile 関数におけるクラッシュを修正します。これはGo 1.2.1の候補となる修正であり、Issue #6946を解決します。

変更の背景

runtime.GoroutineProfile は、Goプログラム内の全ゴルーチンのスタックトレースを収集し、プロファイリング情報を提供する関数です。しかし、特定の条件下、特にゴルーチンがシステムコール(syscall)の実行中またはシステムコールから復帰する際に、この関数がクラッシュする可能性がありました。

従来の scanstack 関数(mgc0.c 内)や saveg 関数(mprof.goc 内)、runtime·tracebackothers 関数(proc.c 内)では、ゴルーチンのスタックをスキャンする際に、ゴルーチンのスケジューラ情報 (gp->sched.pc, gp->sched.sp, gp->sched.lr) を直接使用していました。システムコール中のゴルーチンは、通常のGoルーチンとは異なるスタック状態を持つことがあり、特にシステムコールから復帰する際にスタックポインタ (sp) が一時的に不安定になることがあります。この不安定な状態の spruntime·gentraceback に渡すと、不正なメモリ参照が発生し、クラッシュにつながる可能性がありました。

Issue #6946は、この runtime.GoroutineProfile がシステムコールから復帰中のゴルーチンに対してプロファイルを取得しようとした際に発生するクラッシュを報告していました。この問題は、Go 1.2.1のリリースにおいて優先的に修正すべき重要なバグと認識されました。

前提知識の解説

  • ゴルーチン (Goroutine): Go言語における軽量な実行スレッドです。Goランタイムによって管理され、OSのスレッドに多重化されて実行されます。
  • スタックトレース (Stack Trace): プログラムの実行中に、現在実行中の関数から呼び出し元の関数へと遡って、関数の呼び出し履歴を一覧表示したものです。デバッグやプロファイリングに不可欠な情報です。
  • プロファイリング (Profiling): プログラムの実行時のパフォーマンス特性(CPU使用率、メモリ使用量、関数呼び出し回数など)を測定・分析することです。runtime.GoroutineProfile は、ゴルーチンのスタックトレースを収集することで、どの関数がどのゴルーチンで実行されているかを把握し、パフォーマンスボトルネックの特定に役立ちます。
  • システムコール (System Call): アプリケーションがオペレーティングシステム (OS) のサービス(ファイルI/O、ネットワーク通信、メモリ管理など)を利用するためのインターフェースです。Goのゴルーチンがシステムコールを実行する際、その実行コンテキストは通常のGoコードの実行時とは異なる状態になることがあります。
  • runtime·gentraceback: Goランタイム内部で使用される関数で、指定されたプログラムカウンタ (PC)、スタックポインタ (SP)、リンクレジスタ (LR) を基に、ゴルーチンのスタックトレースを生成します。
  • uintptr: Goにおけるポインタ型で、任意のポインタ値を保持できる整数型です。アドレスを表現する際に使用されます。
  • ~(uintptr)0: これは uintptr 型のビット反転した0、つまり全てのビットが1である値を意味します。Goランタイム内部で、特定の関数に「特別な意味を持つ値」として渡されることがあります。このコミットでは、runtime·gentraceback にこの値を渡すことで、「PC/SP/LRの値を直接使用するのではなく、ゴルーチン構造体 (G* gp) から適切な値を読み取ってスタックトレースを生成してほしい」というシグナルとして利用されています。

技術的詳細

このコミットの主要な変更点は、runtime.GoroutineProfile がゴルーチンのスタックトレースを収集する際に、スタックポインタ (SP) やプログラムカウンタ (PC) の取得方法を変更したことです。

以前の実装では、scanstacksavegruntime·tracebackothers といった関数が、プロファイル対象のゴルーチン gp のスケジューラ情報 (gp->sched.pc, gp->sched.sp, gp->sched.lr) を直接 runtime·gentraceback に渡していました。

しかし、ゴルーチンがシステムコールを実行している最中や、システムコールからGoコードに復帰する過渡期にある場合、gp->sched.sp が指すスタックポインタが一時的に無効な状態になることがありました。特に、システムコールから復帰する際に、スタックが再調整される前にプロファイルが取得されると、不正なスタックポインタが runtime·gentraceback に渡され、結果としてクラッシュを引き起こしていました。

この修正では、runtime·gentraceback 関数に渡すPCとSPの値として、直接 gp->sched.pcgp->sched.sp を渡すのではなく、~(uintptr)0 という特殊な値を渡すように変更されました。この ~(uintptr)0 は、runtime·gentraceback に対する「シグナル」として機能します。

runtime·gentraceback 関数(traceback_arm.c および traceback_x86.c で実装)は、この ~(uintptr)0 というシグナルを受け取ると、PCとSPの値を直接使用する代わりに、引数として渡されたゴルーチン構造体 gp から、より安全で安定したスタック情報を取得するように変更されました。具体的には、ゴルーチンがシステムコールスタック (gp->syscallstack) を持っている場合は gp->syscallpcgp->syscallsp を、そうでない場合は gp->sched.pcgp->sched.sp を使用します。これにより、システムコール中のゴルーチンや、システムコールから復帰中のゴルーチンに対しても、常に正しいスタックポインタとプログラムカウンタを使用してスタックトレースを生成できるようになり、クラッシュが回避されます。

また、この修正には runtime_unix_test.go という新しいテストファイルが追加されています。このテストは、runtime.GoroutineProfile がシステムコールから復帰中のゴルーチンに対してクラッシュしないことを検証するために設計されています。複数のゴルーチンを起動し、それぞれが syscall.Close(-1) のような高速なシステムコールを繰り返し実行するループに入ります。同時に、メインゴルーチンが runtime.GoroutineProfile を繰り返し呼び出し、競合状態を意図的に発生させて、以前のクラッシュが再現されないことを確認します。

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

src/pkg/runtime/mgc0.c

scanstack 関数が変更されました。

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1515,27 +1515,7 @@ scanframe(Stkframe *frame, void *arg)
 static void
 scanstack(G* gp, void *scanbuf)
 {
 -	uintptr pc;
 -	uintptr sp;
 -	uintptr lr;
 -
 -	if(gp->syscallstack != (uintptr)nil) {
 -		// Scanning another goroutine that is about to enter or might
 -		// have just exited a system call. It may be executing code such
 -		// as schedlock and may have needed to start a new stack segment.
 -		// Use the stack segment and stack pointer at the time of
 -		// the system call instead, since that won't change underfoot.
 -		sp = gp->syscallsp;
 -		pc = gp->syscallpc;
 -		lr = 0;
 -	} else {
 -		// Scanning another goroutine's stack.
 -		// The goroutine is usually asleep (the world is stopped).
 -		sp = gp->sched.sp;
 -		pc = gp->sched.pc;
 -		lr = gp->sched.lr;
 -	}
 -	runtime·gentraceback(pc, sp, lr, gp, 0, nil, 0x7fffffff, scanframe, scanbuf, false);
 +	runtime·gentraceback(~(uintptr)0, ~(uintptr)0, 0, gp, 0, nil, 0x7fffffff, scanframe, scanbuf, false);
 }

src/pkg/runtime/mprof.goc

saveg 関数と GoroutineProfile 関数内の saveg 呼び出しが変更されました。

--- a/src/pkg/runtime/mprof.goc
+++ b/src/pkg/runtime/mprof.goc
@@ -528,7 +528,7 @@ saveg(uintptr pc, uintptr sp, G *gp, TRecord *r)
 {
  	int32 n;
  	
 -	n = runtime·gentraceback((uintptr)pc, (uintptr)sp, 0, gp, 0, r->stk, nelem(r->stk), nil, nil, false);
 +	n = runtime·gentraceback(pc, sp, 0, gp, 0, r->stk, nelem(r->stk), nil, nil, false);
  	if(n < nelem(r->stk))
  	 	r->stk[n] = 0;
 }
@@ -556,7 +556,7 @@ func GoroutineProfile(b Slice) (n int, ok bool) {
  			for(gp = runtime·allg; gp != nil; gp = gp->alllink) {
  				if(gp == g || gp->status == Gdead)
  					continue;
 -				saveg(gp->sched.pc, gp->sched.sp, gp, r++);
 +				saveg(~(uintptr)0, ~(uintptr)0, gp, r++);
  			}
  		}

src/pkg/runtime/proc.c

runtime·tracebackothers 関数内の runtime·traceback 呼び出しが変更されました。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -276,7 +276,7 @@ runtime·tracebackothers(G *me)
  	if((gp = m->curg) != nil && gp != me) {
  		runtime·printf("\n");
  		runtime·goroutineheader(gp);
 -		runtime·traceback(gp->sched.pc, gp->sched.sp, gp->sched.lr, gp);
 +		runtime·traceback(~(uintptr)0, ~(uintptr)0, 0, gp);
  	}
  
  	for(gp = runtime·allg; gp != nil; gp = gp->alllink) {
@@ -290,7 +290,7 @@ runtime·tracebackothers(G *me)
  		 	runtime·printf("\tgoroutine running on other thread; stack unavailable\n");
  		 	runtime·printcreatedby(gp);
  		} else
 -		 	runtime·traceback(gp->sched.pc, gp->sched.sp, gp->sched.lr, gp);
 +		 	runtime·traceback(~(uintptr)0, ~(uintptr)0, 0, gp);
  	}
 }

src/pkg/runtime/traceback_arm.c および src/pkg/runtime/traceback_x86.c

runtime·gentraceback 関数に、~(uintptr)0 シグナルを処理するロジックが追加されました。

traceback_arm.c の変更点:

--- a/src/pkg/runtime/traceback_arm.c
+++ b/src/pkg/runtime/traceback_arm.c
@@ -20,6 +20,18 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,
  	Stktop *stk;
  	String file;
  
+	if(pc0 == ~(uintptr)0 && sp0 == ~(uintptr)0) { // Signal to fetch saved values from gp.
+		if(gp->syscallstack != (uintptr)nil) {
+			pc0 = gp->syscallpc;
+			sp0 = gp->syscallsp;
+			lr0 = 0;
+		} else {
+			pc0 = gp->sched.pc;
+			sp0 = gp->sched.sp;
+			lr0 = gp->sched.lr;
+		}
+	}
+
  	nprint = 0;
  	runtime·memclr((byte*)&frame, sizeof frame);
  	frame.pc = pc0;

traceback_x86.c の変更点:

--- a/src/pkg/runtime/traceback_x86.c
+++ b/src/pkg/runtime/traceback_x86.c
@@ -30,6 +30,16 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,
  	String file;
  
  	USED(lr0);
+	
+	if(pc0 == ~(uintptr)0 && sp0 == ~(uintptr)0) { // Signal to fetch saved values from gp.
+		if(gp->syscallstack != (uintptr)nil) {
+			pc0 = gp->syscallpc;
+			sp0 = gp->syscallsp;
+		} else {
+			pc0 = gp->sched.pc;
+			sp0 = gp->sched.sp;
+		}
+	}
  
  	nprint = 0;
  	runtime·memclr((byte*)&frame, sizeof frame);

src/pkg/runtime/runtime_unix_test.go

新しいテストファイルが追加されました。

// Copyright 2013 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Only works on systems with syscall.Close.
// We need a fast system call to provoke the race,
// and Close(-1) is nearly universally fast.

// +build darwin dragonfly freebsd linux netbsd openbsd plan9

package runtime_test

import (
	"runtime"
	"sync"
	"sync/atomic"
	"syscall"
	"testing"
)

func TestGoroutineProfile(t *testing.T) {
	// GoroutineProfile used to use the wrong starting sp for
	// goroutines coming out of system calls, causing possible
	// crashes.
	defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(100))

	var stop uint32
	defer atomic.StoreUint32(&stop, 1) // in case of panic

	var wg sync.WaitGroup
	for i := 0; i < 4; i++ {
		wg.Add(1)
		go func() {
			for atomic.LoadUint32(&stop) == 0 {
				syscall.Close(-1)
			}
			wg.Done()
		}()
	}

	max := 10000
	if testing.Short() {
		max = 100
	}
	stk := make([]runtime.StackRecord, 100)
	for n := 0; n < max; n++ {
		_, ok := runtime.GoroutineProfile(stk)
		if !ok {
			t.Fatalf("GoroutineProfile failed")
		}
	}

	// If the program didn't crash, we passed.
	atomic.StoreUint32(&stop, 1)
	wg.Wait()
}

コアとなるコードの解説

  • scanstack 関数 (mgc0.c) の変更:

    • 以前は、ゴルーチンがシステムコール中か否かによって pc, sp, lr を条件分岐で取得し、その値を runtime·gentraceback に渡していました。
    • 変更後、この条件分岐が削除され、常に ~(uintptr)0pcsp の引数として runtime·gentraceback に渡すようになりました。これは、「PCとSPの実際の値は runtime·gentraceback 内部でゴルーチン構造体 gp から取得してほしい」という意図を伝えます。
  • saveg 関数 (mprof.goc) の変更:

    • saveg 関数は、プロファイル情報を保存する際に runtime·gentraceback を呼び出します。
    • GoroutineProfile 関数内で saveg を呼び出す際も、同様に ~(uintptr)0pcsp の引数として渡すように変更されました。
  • runtime·tracebackothers 関数 (proc.c) の変更:

    • この関数は、他のゴルーチンのスタックトレースをダンプする際に runtime·traceback を呼び出します。
    • ここでも、gp->sched.pcgp->sched.sp を直接渡す代わりに、~(uintptr)0 を渡すように変更されました。
  • runtime·gentraceback 関数 (traceback_arm.c, traceback_x86.c) の変更:

    • この関数は、スタックトレース生成の核心部分です。
    • 追加された if(pc0 == ~(uintptr)0 && sp0 == ~(uintptr)0) ブロックが、新しいシグナルを処理します。
    • このシグナルを受け取ると、gp->syscallstacknil でない(つまりシステムコールスタックを使用している)場合は、gp->syscallpcgp->syscallsp を使用します。これは、システムコール中のゴルーチンのスタック情報が syscallpcsyscallsp に保存されているためです。
    • それ以外の場合(通常のGoスタックを使用している場合)は、gp->sched.pcgp->sched.sp を使用します。
    • このロジックにより、runtime·gentraceback は常にゴルーチンの現在の状態に応じた適切なスタック情報を取得し、クラッシュを回避します。
  • runtime_unix_test.go の追加:

    • このテストは、runtime.GoroutineProfile がシステムコールから復帰中のゴルーチンに対してクラッシュしないことを確認するためのものです。
    • 複数のゴルーチンが syscall.Close(-1) を繰り返し呼び出すことで、システムコールへの出入りを頻繁に行います。
    • メインゴルーチンは runtime.GoroutineProfile を繰り返し呼び出し、この競合状態下でプロファイリングがクラッシュしないことを検証します。syscall.Close(-1) は、高速で副作用が少ないシステムコールであるため、テストのトリガーとして適しています。

これらの変更により、runtime.GoroutineProfile は、ゴルーチンがシステムコールを実行しているかどうかにかかわらず、安定してスタックトレースを収集できるようになり、Goランタイムの堅牢性が向上しました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (特に src/pkg/runtime ディレクトリ内のファイル)
  • Go言語のIssueトラッカー (Issue #6946 および関連するIssue)
  • Go言語のドキュメント (runtimeパッケージに関する情報)
  • Go言語のプロファイリングに関する一般的な情報