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

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

このコミットは、Goランタイムにおける文字列処理のバグを修正するものです。具体的には、新しい文字列ルーチンでMaxstringという内部変数が適切に更新されなかったために、長い文字列が不正であると誤って判断され、「[string too long]」というエラーが不必要に発生する問題を解決します。この修正により、Goプログラムが長い文字列を正しく扱えるようになります。

コミット

commit 97c8b24d01e6d60938fc499fc544ff3da9e9f726
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue Jul 8 22:37:18 2014 +0400

    runtime: fix spurious "[string too long]" error
    Maxstring is not updated in the new string routines,
    this makes runtime think that long strings are bogus.
    Fixes #8339.
    
    LGTM=crawshaw, iant
    R=golang-codereviews, crawshaw, iant
    CC=golang-codereviews, khr, rsc
    https://golang.org/cl/110930043

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

https://github.com/golang/go/commit/97c8b24d01e6d60938fc499fc544ff3da9e9f726

元コミット内容

runtime: fix spurious "[string too long]" error
Maxstring is not updated in the new string routines,
this makes runtime think that long strings are bogus.
Fixes #8339.

LGTM=crawshaw, iant
R=golang-codereviews, crawshaw, iant
CC=golang-codereviews, khr, rsc
https://golang.org/cl/110930043

変更の背景

Goランタイムは、効率的なメモリ管理とデータ構造の整合性を維持するために、内部的に様々な最適化とチェックを行っています。その一つに、文字列の長さを追跡するメカニズムがあったと考えられます。このコミットの背景にある問題は、Goの新しい文字列生成ルーチン(おそらく文字列の連結やその他の操作を行う関数)が導入された際に、ランタイムが内部的に管理しているMaxstringという変数が適切に更新されていなかったことです。

Maxstringは、これまでにランタイムが処理した文字列の中で最も長いものの長さを記録するような役割を担っていたと推測されます。この値が最新の文字列の長さを反映していない場合、新しく生成された、しかし実際には有効な長い文字列が、Maxstringの値と比較された際に「長すぎる」あるいは「不正である」と誤って判断され、「[string too long]」というエラーが不必要に発生していました。これは、プログラムが正しく動作しているにもかかわらず、ランタイムの内部的な整合性チェックが誤作動を起こしている状態でした。

このバグは、Goプログラムが長い文字列を扱う際に予期せぬパニックやエラーを引き起こす可能性があり、Goアプリケーションの安定性と信頼性に影響を与えるため、修正が必要とされました。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念とC言語の知識が役立ちます。

  • Goランタイム (Go Runtime): Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。ランタイムは、ガベージコレクション、スケジューリング、メモリ管理、プリミティブ型の操作(文字列、スライスなど)といった低レベルな機能を提供します。Goの文字列やスライスは、ランタイムによって効率的に管理されるデータ構造です。
  • 文字列 (String) の内部表現: Goの文字列は、内部的には読み取り専用のバイトスライスとして表現されます。これは、ポインタと長さ(バイト数)のペアで構成されます。文字列の連結などの操作は、新しい文字列を生成し、その内容をコピーすることで行われます。
  • rawstring 関数: src/pkg/runtime/stubs.goc に存在する rawstring 関数は、Goランタイム内部で新しい文字列を割り当てるための低レベルな関数です。Goのユーザーコードから直接呼び出されることはなく、コンパイラや他のランタイム関数によって利用されます。この関数は、指定されたサイズのメモリを確保し、そのメモリを指す文字列とスライスの構造体を返します。
  • runtime·mallocgc: Goのガベージコレクタと連携してメモリを割り当てるランタイム関数です。
  • FlagNoScan|FlagNoZero: runtime·mallocgc に渡されるフラグで、割り当てられたメモリがガベージコレクタによってスキャンされないこと(ポインタを含まないため)と、ゼロ初期化されないことを示します。
  • runtime·maxstring: このコミットの核心となる内部変数です。Goのソースコード全体を検索しても、この変数が直接的に公開されているドキュメントはほとんどありません。しかし、コミットメッセージとコードの変更から、これはGoランタイムがこれまでに生成または処理した文字列の中で、最も長い文字列の長さを追跡するために使用する内部的な「最大文字列長」の記録であると推測されます。これは、文字列の整合性チェックや、特定の最適化のヒントとして利用されていた可能性があります。
  • runtime·casp (Compare-And-Swap Pointer): Goランタイム内部で使用されるアトミック操作の一つです。これは、特定のメモリ位置(ポインタ)の値が期待する値と一致する場合にのみ、そのメモリ位置の値を新しい値に更新します。並行処理環境において、複数のゴルーチンが同時に同じメモリを操作しようとした際に、データの整合性を保つために使用されます。ここでは、runtime·maxstring の値をアトミックに更新するために使われています。
  • C言語とGoのランタイム: Goランタイムの一部はC言語(またはGoの擬似C言語であるgocファイル)で記述されています。これは、低レベルなシステムコールやメモリ操作を行うためです。stubs.gocファイルは、Goの関数とC言語の関数を橋渡しする役割を担っています。

技術的詳細

このコミットの技術的な核心は、Goランタイムが内部的に管理するruntime·maxstringという変数の更新ロジックの修正にあります。

Goランタイムは、文字列を効率的に管理するために、内部的に様々なヒューリスティックやチェックを使用しています。runtime·maxstringは、その一環として、これまでにランタイムが遭遇した文字列の最大長を記録する変数であると推測されます。この変数は、例えば、文字列の長さが異常に大きい場合に、メモリ破損や不正な操作を検出するための健全性チェック(sanity check)として利用されていた可能性があります。

問題は、新しい文字列生成ルーチンが導入された際に、このruntime·maxstringが適切に更新されなかったことです。具体的には、rawstring関数(Goランタイムが新しい文字列を割り当てる際に使用する低レベル関数)が、新しく割り当てられた文字列の長さが現在のruntime·maxstringよりも大きい場合でも、runtime·maxstringをその新しい長さに更新していませんでした。

その結果、以下のようなシナリオが発生しました。

  1. Goプログラムが非常に長い文字列を生成する。
  2. rawstring関数がこの長い文字列のためにメモリを割り当てる。
  3. しかし、runtime·maxstringは古い(より短い)値を保持したままになる。
  4. 後続のランタイムのチェックで、この新しく生成された長い文字列の長さが、更新されていないruntime·maxstringと比較される。
  5. 比較の結果、新しく生成された文字列がruntime·maxstringよりも「長すぎる」と判断され、ランタイムが「[string too long]」という誤ったエラーを報告する。

このコミットは、rawstring関数内で、新しく割り当てられる文字列の長さが現在のruntime·maxstringよりも大きい場合に、runtime·maxstringをアトミックに更新するロジックを追加することで、この問題を解決しています。アトミック操作(runtime·casp)を使用することで、複数のゴルーチンが同時に文字列を生成しようとした場合でも、runtime·maxstringの更新が安全かつ正確に行われることが保証されます。

また、このコミットには、このバグを再現し、修正が正しく機能することを確認するための新しいテストケースが追加されています。このテストケースは、非常に長い文字列を連結し、その文字列をパニックメッセージとして出力することで、以前であれば「[string too long]」エラーが発生していた状況をシミュレートしています。

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

このコミットにおけるコアとなるコードの変更は、主に以下の2つのファイルにあります。

  1. src/pkg/runtime/stubs.goc:

    • rawstring 関数内に、runtime·maxstring を更新するロジックが追加されました。
    • 具体的には、新しく割り当てられる文字列のサイズ (size) が現在の runtime·maxstring (ms) よりも大きい場合に、runtime·maxstringsize に更新する for ループと runtime·casp が追加されています。
    --- a/src/pkg/runtime/stubs.goc
    +++ b/src/pkg/runtime/stubs.goc
    @@ -24,6 +24,7 @@ package runtime
     
     #pragma textflag NOSPLIT
     func rawstring(size intgo) (s String, b Slice) {
    +\tuintptr ms;
     \tbyte *p;
     
     \tp = runtime·mallocgc(size, 0, FlagNoScan|FlagNoZero);\
    @@ -32,6 +33,11 @@ func rawstring(size intgo) (s String, b Slice) {
     \tb.array = p;\
     \tb.len = size;\
     \tb.cap = size;\
    +\tfor(;;) {\
    +\t\tms = runtime·maxstring;\
    +\t\tif((uintptr)size <= ms || runtime·casp((void**)&runtime·maxstring, (void*)ms, (void*)size))\
    +\t\t\tbreak;\
    +\t}\
     }\
     
     #pragma textflag NOSPLIT
    
  2. src/pkg/runtime/string_test.go:

    • TestLargeStringConcat という新しいテスト関数が追加されました。
    • このテストは、strings.Repeat を使用して非常に長い文字列(4KB)を生成し、それらを連結してパニックメッセージとして出力するGoプログラムを実行します。
    • 出力が期待される長い文字列で始まることを確認することで、ランタイムが長い文字列を正しく処理し、不必要なエラーを発生させないことを検証します。
    --- a/src/pkg/runtime/string_test.go
    +++ b/src/pkg/runtime/string_test.go
    @@ -6,6 +6,7 @@ package runtime_test
     
     import (
     	"runtime"
    +\t"strings"
     	"testing"
     )
     
    @@ -122,3 +123,25 @@ func TestStringW(t *testing.T) {
     		}
     	}
     }\
    +\n+func TestLargeStringConcat(t *testing.T) {\
    +\toutput := executeTest(t, largeStringConcatSource, nil)\
    +\twant := "panic: " + strings.Repeat("0", 1<<10) + strings.Repeat("1", 1<<10) +\
    +\t\tstrings.Repeat("2", 1<<10) + strings.Repeat("3", 1<<10)\
    +\tif !strings.HasPrefix(output, want) {\
    +\t\tt.Fatalf("output does not start with %q:\\n%s", want, output)\
    +\t}\
    +}\
    +\n+var largeStringConcatSource = `\
    +package main\
    +import "strings"\
    +func main() {\
    +\ts0 := strings.Repeat("0", 1<<10)\
    +\ts1 := strings.Repeat("1", 1<<10)\
    +\ts2 := strings.Repeat("2", 1<<10)\
    +\ts3 := strings.Repeat("3", 1<<10)\
    +\ts := s0 + s1 + s2 + s3\
    +\tpanic(s)\
    +}\
    +`
    

コアとなるコードの解説

src/pkg/runtime/stubs.goc の変更

rawstring関数は、Goランタイムが新しい文字列オブジェクトのためにメモリを割り当てる際に呼び出される内部関数です。この関数は、指定されたsize(文字列のバイト長)のメモリをruntime·mallocgcを使って確保し、そのメモリを指すStringSliceの構造体を初期化します。

追加されたコードブロックは以下の通りです。

	uintptr ms;
	// ... (既存のコード) ...
	for(;;) {
		ms = runtime·maxstring;
		if((uintptr)size <= ms || runtime·casp((void**)&runtime·maxstring, (void*)ms, (void*)size))
			break;
	}
  • uintptr ms;: msというuintptr型のローカル変数を宣言します。これは、runtime·maxstringの現在の値を一時的に保持するために使用されます。
  • for(;;) { ... break; }: これは無限ループですが、条件が満たされるとbreakでループを抜けます。これは、アトミック操作(CAS: Compare-And-Swap)を安全に実行するための一般的なパターンです。
  • ms = runtime·maxstring;: runtime·maxstringの現在の値をmsに読み込みます。これは、CAS操作の「期待する値」となります。
  • if((uintptr)size <= ms || runtime·casp((void**)&runtime·maxstring, (void*)ms, (void*)size)): ここが核心部分です。
    • (uintptr)size <= ms: もし新しく割り当てられる文字列のサイズが、現在のruntime·maxstring以下である場合、runtime·maxstringを更新する必要はありません。この条件が真であれば、ループを抜けます。
    • runtime·casp((void**)&runtime·maxstring, (void*)ms, (void*)size): もしsizemsより大きい場合、runtime·caspが呼び出されます。
      • &runtime·maxstring: runtime·maxstring変数のアドレス。
      • (void*)ms: runtime·maxstringの現在の値として期待する値(ループの最初に読み込んだms)。
      • (void*)size: runtime·maxstringに設定したい新しい値(新しく割り当てられる文字列のsize)。
      • runtime·caspは、runtime·maxstringmsと等しい場合にのみ、その値をsizeに更新し、成功した場合は真を返します。もし他のゴルーチンがruntime·maxstringを先に更新してしまった場合、runtime·caspは失敗し、偽を返します。
  • ||: 論理OR演算子により、sizems以下であるか、またはruntime·caspが成功した場合に、if文の条件が真となり、breakでループを抜けます。
  • このループとCAS操作の組み合わせにより、複数のゴルーチンが同時にrawstringを呼び出し、runtime·maxstringを更新しようとした場合でも、競合状態を避け、runtime·maxstringが常に最大値を正確に反映するように保証されます。

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

TestLargeStringConcatは、この修正が正しく機能することを検証するための統合テストです。

  • largeStringConcatSourceという文字列変数に、Goのソースコードが定義されています。このコードは、strings.Repeatを使ってそれぞれ1KBの長さの文字列を4つ("0"が1024個、"1"が1024個など)生成し、それらを連結して最終的に4KBの非常に長い文字列sを作成します。そして、panic(s)によってこの長い文字列をパニックメッセージとして出力します。
  • executeTest(t, largeStringConcatSource, nil)は、このGoソースコードを別のプロセスとして実行し、その標準出力(パニックメッセージを含む)を取得するヘルパー関数です。
  • want変数には、期待されるパニックメッセージのプレフィックスが定義されています。これは、連結された長い文字列そのものです。
  • strings.HasPrefix(output, want)で、実際の出力が期待される文字列で始まるかどうかをチェックします。

このテストの目的は、修正前であれば「[string too long]」エラーでパニックしていたであろう状況で、正しく長い文字列がパニックメッセージとして出力されることを確認することです。これにより、runtime·maxstringの更新が正しく行われ、ランタイムが有効な長い文字列を不正と判断しなくなったことが検証されます。

関連リンク

  • Go Issue Tracker: 関連するIssue #8339は、現在の公開されているGoのIssueトラッカーでは直接見つけることができませんでしたが、コミットメッセージに明記されているため、過去に存在したか、内部的なIssueであった可能性があります。
  • Go CL (Change List): https://golang.org/cl/110930043 (このコミットに対応するGoのコードレビューページ)

参考にした情報源リンク

  • Goのソースコード (特に src/pkg/runtime/stubs.gocsrc/pkg/runtime/string_test.go)
  • Goの文字列の内部表現に関する一般的な情報 (Go言語の公式ドキュメントやブログ記事)
  • アトミック操作 (Compare-And-Swap) に関する一般的な情報 (並行処理の教科書やオンラインリソース)
  • GoのIssueトラッカーの検索結果 (直接的なIssue #8339は見つからなかったが、関連する情報収集に使用)
  • Stack Overflow や Go Forum などでの maxstring や Goの文字列長に関する議論 (内部的な概念の理解を深めるために参照)