[インデックス 14100] ファイルの概要
このコミットは、Go言語の標準ライブラリである io/ioutil
パッケージ内の tempfile.go
ファイルに対するものです。io/ioutil
パッケージは、一時ファイルや一時ディレクトリの作成、ファイルの内容の読み書き、ディレクトリのリスト表示など、I/O操作に関するユーティリティ機能を提供します。
tempfile.go
は、特に ioutil.TempFile
や ioutil.TempDir
といった関数が内部で使用する、一時ファイル名や一時ディレクトリ名を生成するロジックを含んでいます。これらの関数は、予測可能でない一意な名前を生成するために、内部で擬似乱数ジェネレータを利用しています。
コミット
このコミットは、io/ioutil
パッケージの一時ファイル名生成ロジックにおいて発生していたデータ競合(data race)を修正するものです。具体的には、一時ファイル名のサフィックス生成に使用されるグローバルな乱数シード変数 rand
へのアクセスが複数のゴルーチンから同時に行われた際に発生する競合状態を、sync.Mutex
を用いて排他制御することで解決しています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/49a5c28a183bbcdf4a9f89377391db1b9c4ed60f
元コミット内容
commit 49a5c28a183bbcdf4a9f89377391db1b9c4ed60f
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Oct 9 21:08:53 2012 +0400
io/ioutil: fix data race on rand
Fixes #4212.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6641050
---
src/pkg/io/ioutil/tempfile.go | 4 ++++\n 1 file changed, 4 insertions(+)
diff --git a/src/pkg/io/ioutil/tempfile.go b/src/pkg/io/ioutil/tempfile.go
index 42d2e67586..257e05d215 100644
--- a/src/pkg/io/ioutil/tempfile.go
+++ b/src/pkg/io/ioutil/tempfile.go
@@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"strconv"
+ "sync"
"time"
)
@@ -16,18 +17,21 @@ import (
// chance the file doesn\'t exist yet - keeps the number of tries in
// TempFile to a minimum.
var rand uint32
+var randmu sync.Mutex
func reseed() uint32 {
return uint32(time.Now().UnixNano() + int64(os.Getpid()))
}
func nextSuffix() string {
+\trandmu.Lock()\n \tr := rand
\tif r == 0 {
\t\tr = reseed()
\t}
\tr = r*1664525 + 1013904223 // constants from Numerical Recipes
\trand = r
+\trandmu.Unlock()\n \treturn strconv.Itoa(int(1e9 + r%1e9))[1:]
}\n
変更の背景
このコミットは、Go言語のIssue #4212「io/ioutil
: TempFile
has a data race on rand
」を修正するために行われました。
Go言語の io/ioutil
パッケージの TempFile
関数は、一時ファイルを作成する際に、ファイル名の一部として乱数に基づいたサフィックスを生成します。この乱数生成には、グローバル変数 rand
(型は uint32
) が使用されていました。
問題は、TempFile
関数が複数のゴルーチン(Goにおける軽量スレッド)から同時に呼び出される可能性がある点にありました。rand
変数はグローバルであり、複数のゴルーチンが同時に rand
の値を読み取り、更新しようとすると、データ競合が発生します。データ競合が発生すると、rand
の値が予期せぬ状態になり、結果として生成される一時ファイル名が衝突したり、予測可能なパターンになったりする可能性がありました。これは、セキュリティ上の問題や、ファイルシステム上での予期せぬ動作を引き起こす可能性があります。
このコミットは、このデータ競合を特定し、sync.Mutex
を導入することで、rand
変数へのアクセスを排他的に制御し、安全な乱数生成を保証することを目的としています。
前提知識の解説
データ競合 (Data Race)
データ競合とは、複数の並行実行される処理(この場合はGoのゴルーチン)が、同期メカニズムなしに同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生する競合状態の一種です。データ競合が発生すると、プログラムの動作が非決定論的になり、予期せぬ結果(クラッシュ、不正なデータ、セキュリティ脆弱性など)を引き起こす可能性があります。
Go言語では、データ競合は「Goメモリモデル」で定義されており、go run -race
コマンドで競合検出器を有効にして実行することで検出できます。
ミューテックス (Mutex)
ミューテックス(Mutex: Mutual Exclusionの略)は、並行プログラミングにおいて共有リソースへのアクセスを制御するための同期プリミティブです。ミューテックスは、一度に1つのゴルーチン(またはスレッド)のみが特定のコードセクション(クリティカルセクション)を実行できるようにすることで、データ競合を防ぎます。
Go言語では、sync
パッケージがミューテックスを提供します。
sync.Mutex
: ミューテックスの型です。Lock()
: ミューテックスをロックします。既にロックされている場合は、ロックが解放されるまで呼び出し元のゴルーチンはブロックされます。Unlock()
: ミューテックスをアンロックします。
ミューテックスを使用する一般的なパターンは、共有リソースにアクセスする直前に Lock()
を呼び出し、アクセスが完了した直後に Unlock()
を呼び出すことです。これにより、クリティカルセクションが排他的に実行されることが保証されます。
io/ioutil
パッケージ
io/ioutil
パッケージは、Go言語の標準ライブラリの一部であり、I/O操作に関する便利なユーティリティ関数を提供していました。Go 1.16以降、このパッケージの機能の多くは io
パッケージと os
パッケージに移行され、io/ioutil
パッケージ自体は非推奨となっています。しかし、このコミットが行われた2012年当時は、GoのI/Oユーティリティの中心的なパッケージでした。
このコミットに関連する主な関数は以下の通りです。
ioutil.TempFile(dir, pattern string) (f *os.File, err error)
: 指定されたディレクトリに一意な名前の一時ファイルを作成し、そのファイルとエラーを返します。ioutil.TempDir(dir, pattern string) (name string, err error)
: 指定されたディレクトリに一意な名前の一時ディレクトリを作成し、そのパスとエラーを返します。
これらの関数は、内部で nextSuffix()
関数を呼び出し、一時ファイル名やディレクトリ名の一部として使用されるランダムなサフィックスを生成していました。
擬似乱数生成 (Pseudo-random number generation)
擬似乱数とは、完全にランダムではないが、統計的にはランダムに見えるように生成された数値のシーケンスです。通常、擬似乱数ジェネレータは、初期シード値に基づいて次の乱数を計算します。同じシード値からは常に同じシーケンスが生成されます。
このコミットの文脈では、rand
という uint32
型のグローバル変数が乱数シードとして機能していました。nextSuffix()
関数内で、この rand
変数に対して線形合同法(Linear Congruential Generator, LCG)のような単純なアルゴリズムが適用され、新しい乱数が生成され、rand
に再代入されていました。
r = r*1664525 + 1013904223
という計算は、まさにLCGの典型的な形式であり、Numerical Recipes
という科学計算の有名な書籍に記載されている定数を使用しています。
技術的詳細
このコミットの技術的な核心は、io/ioutil/tempfile.go
内のグローバル変数 rand
へのアクセスを同期することです。
-
問題の特定:
tempfile.go
には、一時ファイル名のサフィックスを生成するためのnextSuffix()
関数があります。この関数は、グローバル変数rand
を読み取り、それに基づいて新しい乱数を計算し、その結果を再びrand
に書き込みます。var rand uint32 // グローバル変数 func nextSuffix() string { r := rand // 読み取り if r == 0 { r = reseed() } r = r*1664525 + 1013904223 // 計算 rand = r // 書き込み return strconv.Itoa(int(1e9 + r%1e9))[1:] }
TempFile
やTempDir
が複数のゴルーチンから同時に呼び出されると、複数のゴルーチンが同時にnextSuffix()
を実行しようとします。このとき、rand
の読み取りと書き込みがアトミック(不可分)に行われないため、以下のようなシナリオでデータ競合が発生します。- ゴルーチンAが
rand
を読み取る (r := rand
)。 - ゴルーチンBが
rand
を読み取る (r := rand
)。 - ゴルーチンAが
rand
を更新する (rand = r
)。 - ゴルーチンBが
rand
を更新する (rand = r
)。
この結果、ゴルーチンAまたはBのどちらか一方の更新が失われ、
rand
の状態が不正になる可能性があります。 - ゴルーチンAが
-
解決策の適用: この問題を解決するために、コミットでは
sync.Mutex
を導入し、rand
変数へのアクセスを排他的に保護しています。var randmu sync.Mutex
:rand
変数を保護するためのミューテックスrandmu
が新しく宣言されました。randmu.Lock()
:nextSuffix()
関数の冒頭でrandmu.Lock()
が呼び出されます。これにより、このミューテックスがロックされ、他のゴルーチンが同じミューテックスをロックしようとしてもブロックされるようになります。randmu.Unlock()
:rand
変数へのアクセス(読み取りと書き込み)が完了した直後にrandmu.Unlock()
が呼び出されます。これにより、ミューテックスが解放され、ブロックされていた他のゴルーチンが処理を続行できるようになります。
この変更により、
nextSuffix()
関数内のrand
変数に対する操作(読み取り、計算、書き込み)は、常に一度に1つのゴルーチンによってのみ実行されることが保証されます。これにより、データ競合が完全に排除され、TempFile
やTempDir
が並行環境で安全に動作するようになります。
コアとなるコードの変更箇所
--- a/src/pkg/io/ioutil/tempfile.go
+++ b/src/pkg/io/ioutil/tempfile.go
@@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"strconv"
+ "sync" // syncパッケージのインポートを追加
"time"
)
@@ -16,18 +17,21 @@ import (
// chance the file doesn\'t exist yet - keeps the number of tries in
// TempFile to a minimum.
var rand uint32
+var randmu sync.Mutex // rand変数を保護するためのミューテックスを追加
func reseed() uint32 {
return uint32(time.Now().UnixNano() + int64(os.Getpid()))
}
func nextSuffix() string {
+\trandmu.Lock() // クリティカルセクションの開始時にロック
\tr := rand
\tif r == 0 {
\t\tr = reseed()
\t}
\tr = r*1664525 + 1013904223 // constants from Numerical Recipes
\trand = r
+\trandmu.Unlock() // クリティカルセクションの終了時にアンロック
\treturn strconv.Itoa(int(1e9 + r%1e9))[1:]
}\n
コアとなるコードの解説
変更は src/pkg/io/ioutil/tempfile.go
ファイルに集中しています。
-
sync
パッケージのインポート:import ("sync")
が追加されました。これは、ミューテックス機能を提供するsync
パッケージを使用するために必要です。 -
randmu sync.Mutex
の宣言: グローバル変数rand
の直下に、var randmu sync.Mutex
という行が追加されました。これは、rand
変数へのアクセスを保護するためのミューテックスインスタンスを宣言しています。sync.Mutex
はゼロ値(初期状態)で利用可能であり、明示的な初期化は不要です。 -
nextSuffix()
関数内でのロックとアンロック:nextSuffix()
関数は、一時ファイル名のサフィックスを生成する際にrand
変数を読み書きするクリティカルセクションです。randmu.Lock()
: 関数の冒頭でrandmu.Lock()
が呼び出されます。これにより、この関数が実行されている間、randmu
ミューテックスがロックされます。他のゴルーチンが同時にnextSuffix()
を呼び出そうとしても、このロックが解放されるまでブロックされます。randmu.Unlock()
:rand
変数の更新 (rand = r
) が完了した直後にrandmu.Unlock()
が呼び出されます。これにより、ミューテックスが解放され、他のゴルーチンがnextSuffix()
を実行できるようになります。
このシンプルなミューテックスの追加により、rand
変数に対する読み書き操作がアトミックに(中断されることなく)実行されることが保証され、並行アクセスによるデータ競合が効果的に防止されました。
関連リンク
- Go Issue #4212:
io/ioutil
:TempFile
has a data race onrand
- Gerrit Change-ID:
https://golang.org/cl/6641050
- https://go-review.googlesource.com/c/go/+/6641050 (これは元のGerritのURLですが、現在はGoのコードレビューシステムにリダイレクトされます)
参考にした情報源リンク
- Go言語の公式ドキュメント:
sync
パッケージ - Go言語の公式ドキュメント:
io/ioutil
パッケージ (非推奨) - Go言語の公式ドキュメント: Goメモリモデル
- Wikipedia: 線形合同法
- Numerical Recipes (書籍): 擬似乱数生成に関する章
- https://www.numerical-recipes.com/ (具体的なページは特定していませんが、乱数生成の定数に関する言及があります)
- データ競合に関する一般的な情報 (並行プログラミングの概念)
- ミューテックスに関する一般的な情報