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

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

このコミットは、Go言語の標準ライブラリである io/ioutil パッケージ内の tempfile.go ファイルに対するものです。io/ioutil パッケージは、一時ファイルや一時ディレクトリの作成、ファイルの内容の読み書き、ディレクトリのリスト表示など、I/O操作に関するユーティリティ機能を提供します。

tempfile.go は、特に ioutil.TempFileioutil.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 へのアクセスを同期することです。

  1. 問題の特定: 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:]
    }
    

    TempFileTempDir が複数のゴルーチンから同時に呼び出されると、複数のゴルーチンが同時に nextSuffix() を実行しようとします。このとき、rand の読み取りと書き込みがアトミック(不可分)に行われないため、以下のようなシナリオでデータ競合が発生します。

    • ゴルーチンAが rand を読み取る (r := rand)。
    • ゴルーチンBが rand を読み取る (r := rand)。
    • ゴルーチンAが rand を更新する (rand = r)。
    • ゴルーチンBが rand を更新する (rand = r)。

    この結果、ゴルーチンAまたはBのどちらか一方の更新が失われ、rand の状態が不正になる可能性があります。

  2. 解決策の適用: この問題を解決するために、コミットでは sync.Mutex を導入し、rand 変数へのアクセスを排他的に保護しています。

    • var randmu sync.Mutex: rand 変数を保護するためのミューテックス randmu が新しく宣言されました。
    • randmu.Lock(): nextSuffix() 関数の冒頭で randmu.Lock() が呼び出されます。これにより、このミューテックスがロックされ、他のゴルーチンが同じミューテックスをロックしようとしてもブロックされるようになります。
    • randmu.Unlock(): rand 変数へのアクセス(読み取りと書き込み)が完了した直後に randmu.Unlock() が呼び出されます。これにより、ミューテックスが解放され、ブロックされていた他のゴルーチンが処理を続行できるようになります。

    この変更により、nextSuffix() 関数内の rand 変数に対する操作(読み取り、計算、書き込み)は、常に一度に1つのゴルーチンによってのみ実行されることが保証されます。これにより、データ競合が完全に排除され、TempFileTempDir が並行環境で安全に動作するようになります。

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

--- 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 ファイルに集中しています。

  1. sync パッケージのインポート: import ("sync") が追加されました。これは、ミューテックス機能を提供する sync パッケージを使用するために必要です。

  2. randmu sync.Mutex の宣言: グローバル変数 rand の直下に、var randmu sync.Mutex という行が追加されました。これは、rand 変数へのアクセスを保護するためのミューテックスインスタンスを宣言しています。sync.Mutex はゼロ値(初期状態)で利用可能であり、明示的な初期化は不要です。

  3. nextSuffix() 関数内でのロックとアンロック: nextSuffix() 関数は、一時ファイル名のサフィックスを生成する際に rand 変数を読み書きするクリティカルセクションです。

    • randmu.Lock(): 関数の冒頭で randmu.Lock() が呼び出されます。これにより、この関数が実行されている間、randmu ミューテックスがロックされます。他のゴルーチンが同時に nextSuffix() を呼び出そうとしても、このロックが解放されるまでブロックされます。
    • randmu.Unlock(): rand 変数の更新 (rand = r) が完了した直後に randmu.Unlock() が呼び出されます。これにより、ミューテックスが解放され、他のゴルーチンが nextSuffix() を実行できるようになります。

このシンプルなミューテックスの追加により、rand 変数に対する読み書き操作がアトミックに(中断されることなく)実行されることが保証され、並行アクセスによるデータ競合が効果的に防止されました。

関連リンク

参考にした情報源リンク