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

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

このコミットは、GoランタイムにおけるPlan 9環境でのGOTRACEBACK環境変数の処理に関するバグ修正です。具体的には、GOTRACEBACKの値を読み取る際にGetenv()関数が動的なメモリ確保(malloc)を行うことで発生する潜在的な問題を解決し、代わりに静的なバッファを使用するように変更しています。

コミット

commit a07f6adda8663f86701356bca341112846ed251f
Author: David du Colombier <0intro@gmail.com>
Date:   Wed Apr 9 06:41:14 2014 +0200

    runtime: fix GOTRACEBACK on Plan 9
    
    Getenv() should not call malloc when called from
    gotraceback(). Instead, we return a static buffer
    in this case, with enough room to hold the longest
    value.
    
    LGTM=rsc
    R=rsc
    CC=golang-codereviews
    https://golang.org/cl/85680043

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

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

元コミット内容

runtime: fix GOTRACEBACK on Plan 9

Getenv() should not call malloc when called from
gotraceback(). Instead, we return a static buffer
in this case, with enough room to hold the longest
value.

LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/85680043

変更の背景

この変更の背景には、Goプログラムがクラッシュした際にスタックトレースを生成するメカニズムと、その際のメモリ管理の安全性に関する考慮があります。

GOTRACEBACK環境変数は、Goプログラムがパニック(ランタイムエラー)を起こした際に、どの程度の詳細なスタックトレースを出力するかを制御するために使用されます。例えば、GOTRACEBACK=allを設定すると、すべてのゴルーチンのスタックトレースが出力されます。

問題は、このGOTRACEBACKの値を読み取るためにGoランタイム内部のGetenv()関数が呼び出される際に、動的なメモリ確保関数であるmallocが使用されていた点にありました。プログラムがパニックを起こしている状況、つまりすでに不安定な状態にある中で、動的なメモリ確保を行うことは非常に危険です。

具体的には、以下のような問題が考えられます。

  1. メモリ破損: パニックの原因がメモリ破損である場合、mallocが不正なメモリ領域にアクセスしようとして、さらなるクラッシュや未定義の動作を引き起こす可能性があります。
  2. デッドロック: mallocが内部的にロックを使用している場合、パニック発生時にすでにそのロックが保持されていたり、あるいはロックの取得に失敗したりすることで、デッドロックが発生し、スタックトレースの出力自体が停止してしまう可能性があります。
  3. リソース不足: システムが極度のリソース不足に陥っている状況でパニックが発生した場合、mallocがメモリを確保できずに失敗し、適切なスタックトレースが出力されない可能性があります。

特にPlan 9のような、リソース管理が厳格で、低レベルな処理が重視される環境では、このような動的なメモリ確保の副作用はより顕著になる可能性があります。

このコミットは、このような不安定な状況下でのmalloc呼び出しを回避し、GOTRACEBACKの処理をより堅牢にするために行われました。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識があると役立ちます。

  • Goランタイム (Go Runtime): Go言語で書かれたプログラムは、Goランタイムと呼ばれる低レベルなシステムによって実行が管理されます。これは、ガベージコレクション、ゴルーチンのスケジューリング、システムコール、メモリ管理など、プログラムの実行に必要な多くの基盤機能を提供します。C言語で書かれた部分も多く含まれており、OSとのインタフェースを担当します。

  • GOTRACEBACK 環境変数: Goプログラムの実行時に設定できる環境変数の一つです。プログラムがパニックを起こした際に、Goランタイムが生成するスタックトレースの出力レベルを制御します。

    • 0 または none: スタックトレースを出力しない。
    • 1 または single: 現在のゴルーチンのスタックトレースのみを出力する(デフォルト)。
    • 2 または all: すべてのゴルーチンのスタックトレースを出力する。
    • crash: すべてのゴルーチンのスタックトレースを出力し、さらにOS固有のクラッシュダンプを生成しようとする(通常はデバッグ目的)。 この変数は、デバッグや問題診断において非常に重要です。
  • Plan 9 (オペレーティングシステム): ベル研究所で開発された分散オペレーティングシステムです。Go言語の開発者の一部(特にRob PikeやKen Thompson)がPlan 9の開発にも深く関わっていたため、Go言語の設計思想に大きな影響を与えています。Plan 9は、すべてのリソースをファイルとして扱うというユニークな設計思想を持ち、シンプルさと堅牢性を重視しています。Goランタイムには、Plan 9を含む様々なOS向けの特定の実装が含まれています。

  • malloc (メモリ確保関数): C言語の標準ライブラリ関数で、指定されたサイズのメモリブロックをヒープから動的に確保するために使用されます。Goランタイムの低レベルな部分では、C言語で書かれたコードがmallocのような関数を直接使用することがあります。動的なメモリ確保は柔軟性を提供しますが、メモリの断片化、パフォーマンスオーバーヘッド、そして特にマルチスレッド環境やエラー処理中にはデッドロックやメモリ破損のリスクを伴うことがあります。

  • スタックトレース (Stack Trace): プログラムの実行中にエラーや例外が発生した際に、その時点までに呼び出された関数のリストと、それぞれの呼び出し元の情報(ファイル名、行番号など)を示すものです。これにより、エラーがプログラムのどの部分で、どのような関数の呼び出し順序で発生したのかを特定でき、デバッグ作業に不可欠な情報となります。

技術的詳細

このコミットの技術的な核心は、GoランタイムがGOTRACEBACK環境変数の値を読み取る際のメモリ確保戦略の変更にあります。

  1. 問題の特定: Goランタイムのsrc/pkg/runtime/env_plan9.cファイルにあるruntime·getenv関数は、環境変数の値を取得する役割を担っています。この関数は、取得する値のサイズに応じてruntime·mallocを呼び出し、動的にメモリを確保していました。 しかし、GOTRACEBACK環境変数は、プログラムがすでにパニック状態にある、つまり非常に不安定な状況で参照されることが想定されます。このような状況下でmallocを呼び出すことは、前述の通り、メモリ破損、デッドロック、またはさらなるクラッシュを引き起こす潜在的なリスクを伴います。これは、mallocが内部的に複雑なデータ構造やロックを使用しているため、不安定な状態ではそれらが正しく機能しない可能性があるためです。

  2. 解決策の導入: このコミットでは、runtime·getenv関数が"GOTRACEBACK"という特定の環境変数を読み取ろうとしている場合に限り、動的なmalloc呼び出しを回避するロジックが導入されました。

    • 静的バッファの導入: static byte b[128]; という128バイトの静的(グローバルスコープに似た)バッファが導入されました。このバッファは、プログラムの実行開始時に一度だけ確保され、以降は再利用されます。
    • 条件付きメモリ確保: runtime·strcmp((byte*)s, (byte*)"GOTRACEBACK") == 0 という条件分岐が追加され、要求された環境変数が"GOTRACEBACK"であるかどうかをチェックします。
    • 静的バッファの利用: もし"GOTRACEBACK"であれば、動的なmallocの代わりに、新しく導入された静的バッファbを指すようにポインタpを設定します。
    • バッファのクリア: runtime·memclr(b, sizeof b); を呼び出して、静的バッファの内容をゼロクリアします。これは、以前のGOTRACEBACKの値が残っていて、新しい値がそれよりも短い場合に、古い値の残骸が混入するのを防ぐためです。
    • サイズチェック: if(n >= sizeof b) というチェックが追加され、GOTRACEBACKの値が静的バッファのサイズ(128バイト)を超える場合は、nil(ヌルポインタ)を返します。これは、バッファオーバーフローを防ぐための安全策です。GOTRACEBACKの値が128バイトを超えることは通常想定されませんが、万が一の場合に備えています。

この変更により、GOTRACEBACKの処理中にmallocが呼び出されることがなくなり、Goプログラムがパニック状態にある際のランタイムの堅牢性が向上しました。

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

変更はsrc/pkg/runtime/env_plan9.cファイルに集中しています。

--- a/src/pkg/runtime/env_plan9.c
+++ b/src/pkg/runtime/env_plan9.c
@@ -12,6 +12,7 @@ runtime·getenv(int8 *s)
 	intgo len;
 	byte file[128];
 	byte *p;
+	static byte b[128]; // 新しく追加された静的バッファ
 
 	len = runtime·findnull((byte*)s);
 	if(len > sizeof file-6)
@@ -25,7 +26,14 @@ runtime·getenv(int8 *s)
 	if(fd < 0)
 		return nil;
 	n = runtime·seek(fd, 0, 2);
-	p = runtime·malloc(n+1); // 変更前: 無条件にmallocを呼び出す
+	if(runtime·strcmp((byte*)s, (byte*)"GOTRACEBACK") == 0){ // GOTRACEBACKの場合の条件分岐
+		// should not call malloc
+		if(n >= sizeof b) // バッファサイズを超える場合はnilを返す
+			return nil;
+		runtime·memclr(b, sizeof b); // 静的バッファをクリア
+		p = b; // 静的バッファを使用
+	}else
+		p = runtime·malloc(n+1); // GOTRACEBACK以外の場合はこれまで通りmalloc
 	r = runtime·pread(fd, p, n, 0);
 	runtime·close(fd);
 	if(r < 0)

コアとなるコードの解説

src/pkg/runtime/env_plan9.c内のruntime·getenv関数は、Goランタイムが環境変数の値を取得するために使用する低レベルなC言語関数です。

変更前は、この関数は環境変数の値の長さに応じて、常にruntime·malloc(n+1)を呼び出して動的にメモリを確保していました。ここでnは環境変数の値の長さです。

このコミットによる変更は、以下のロジックを追加しています。

  1. static byte b[128];: これは、runtime·getenv関数が呼び出されるたびに再利用される、128バイトの静的(staticキーワードにより、プログラムの実行期間中メモリに存在し続ける)バッファを宣言しています。このバッファは、GOTRACEBACK環境変数の値を格納するために特別に用意されました。

  2. if(runtime·strcmp((byte*)s, (byte*)"GOTRACEBACK") == 0): この条件文は、現在取得しようとしている環境変数の名前が文字列"GOTRACEBACK"と一致するかどうかをチェックしています。runtime·strcmpはGoランタイム内部の文字列比較関数です。

  3. if(n >= sizeof b): もし環境変数が"GOTRACEBACK"であり、かつその値の長さnが静的バッファbのサイズ(128バイト)以上である場合、この条件が真となります。これは、静的バッファに収まらないほど長い値が設定されている場合に、バッファオーバーフローを防ぐための安全策です。この場合、nil(ヌルポインタ)を返して、値の取得を諦めます。

  4. runtime·memclr(b, sizeof b);: 静的バッファbをゼロクリアしています。これは、runtime·getenvが複数回呼び出され、GOTRACEBACKの値を読み取る際に、以前の呼び出しでバッファに残っていたデータが新しい値に混入するのを防ぐためです。これにより、常にクリーンな状態でバッファが使用されます。

  5. p = b;: pは環境変数の値を格納するメモリ領域を指すポインタです。GOTRACEBACKの場合、このポインタを動的に確保されたメモリではなく、静的バッファbの先頭を指すように変更しています。これにより、mallocの呼び出しが回避されます。

  6. else p = runtime·malloc(n+1);: もし取得しようとしている環境変数が"GOTRACEBACK"ではない場合、これまでの動作通りruntime·malloc(n+1)を呼び出して、動的にメモリを確保します。

この一連の変更により、GOTRACEBACKの処理というクリティカルなパスにおいて、動的なメモリ確保に伴う潜在的なリスクが排除され、ランタイムの安定性が向上しました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (src/pkg/runtime/env_plan9.c)
  • Go言語のドキュメント(GOTRACEBACK環境変数に関する情報)
  • 一般的なオペレーティングシステムとメモリ管理の概念
  • Plan 9オペレーティングシステムに関する一般的な知識