[インデックス 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
をその新しい長さに更新していませんでした。
その結果、以下のようなシナリオが発生しました。
- Goプログラムが非常に長い文字列を生成する。
rawstring
関数がこの長い文字列のためにメモリを割り当てる。- しかし、
runtime·maxstring
は古い(より短い)値を保持したままになる。 - 後続のランタイムのチェックで、この新しく生成された長い文字列の長さが、更新されていない
runtime·maxstring
と比較される。 - 比較の結果、新しく生成された文字列が
runtime·maxstring
よりも「長すぎる」と判断され、ランタイムが「[string too long]」という誤ったエラーを報告する。
このコミットは、rawstring
関数内で、新しく割り当てられる文字列の長さが現在のruntime·maxstring
よりも大きい場合に、runtime·maxstring
をアトミックに更新するロジックを追加することで、この問題を解決しています。アトミック操作(runtime·casp
)を使用することで、複数のゴルーチンが同時に文字列を生成しようとした場合でも、runtime·maxstring
の更新が安全かつ正確に行われることが保証されます。
また、このコミットには、このバグを再現し、修正が正しく機能することを確認するための新しいテストケースが追加されています。このテストケースは、非常に長い文字列を連結し、その文字列をパニックメッセージとして出力することで、以前であれば「[string too long]」エラーが発生していた状況をシミュレートしています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、主に以下の2つのファイルにあります。
-
src/pkg/runtime/stubs.goc
:rawstring
関数内に、runtime·maxstring
を更新するロジックが追加されました。- 具体的には、新しく割り当てられる文字列のサイズ (
size
) が現在のruntime·maxstring
(ms
) よりも大きい場合に、runtime·maxstring
をsize
に更新する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
-
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
を使って確保し、そのメモリを指すString
とSlice
の構造体を初期化します。
追加されたコードブロックは以下の通りです。
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)
: もしsize
がms
より大きい場合、runtime·casp
が呼び出されます。&runtime·maxstring
:runtime·maxstring
変数のアドレス。(void*)ms
:runtime·maxstring
の現在の値として期待する値(ループの最初に読み込んだms
)。(void*)size
:runtime·maxstring
に設定したい新しい値(新しく割り当てられる文字列のsize
)。runtime·casp
は、runtime·maxstring
がms
と等しい場合にのみ、その値をsize
に更新し、成功した場合は真を返します。もし他のゴルーチンがruntime·maxstring
を先に更新してしまった場合、runtime·casp
は失敗し、偽を返します。
||
: 論理OR演算子により、size
がms
以下であるか、または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.goc
とsrc/pkg/runtime/string_test.go
) - Goの文字列の内部表現に関する一般的な情報 (Go言語の公式ドキュメントやブログ記事)
- アトミック操作 (Compare-And-Swap) に関する一般的な情報 (並行処理の教科書やオンラインリソース)
- GoのIssueトラッカーの検索結果 (直接的なIssue #8339は見つからなかったが、関連する情報収集に使用)
- Stack Overflow や Go Forum などでの
maxstring
や Goの文字列長に関する議論 (内部的な概念の理解を深めるために参照)