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

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

このコミットは、Go言語の crypto/rand パッケージのテストコード rand_test.go における変更です。具体的には、乱数生成器からの読み取り処理を io.ReadFull を使用するように修正し、特定の環境(Solaris 11より前のバージョン)で発生する /dev/urandom からの読み取り制限の問題に対応しています。

コミット

commit 990f3af72b36ac8d9a78b8b8621d248af490f8be
Author: Ian Lance Taylor <iant@golang.org>
Date:   Tue Apr 24 21:36:42 2012 -0700

    crypto/rand: use io.ReadFull in test
    
    On Solaris versions before Solaris 11, the kernel will not
    return more than 1040 on a single read from /dev/urandom.
    
    R=golang-dev, agl, bradfitz, rsc, iant, dchest
    CC=golang-dev
    https://golang.org/cl/6113046

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

https://github.com/golang/go/commit/990f3af72b36ac8d9a78b8b8621d248af490f8be

元コミット内容

crypto/rand パッケージのテスト TestRead 関数において、乱数生成器 Reader からバイト列を読み取る際に、Read メソッドを直接呼び出していました。

n, err := Read(b)
if n != len(b) || err != nil {
    t.Fatalf("Read(buf) = %d, %s", n, err)
}

変更の背景

この変更の背景には、Solaris 11より前のバージョンのオペレーティングシステムにおける /dev/urandom の挙動に関する特定の問題があります。Solarisの古いカーネルでは、/dev/urandom から一度に読み取れるバイト数が1040バイトに制限されていました。

crypto/rand パッケージの Reader は、内部的にシステムが提供する乱数源(Linuxでは通常 /dev/urandom)を利用しています。io.Reader インターフェースの Read メソッドは、要求されたバイト数すべてを読み取るとは限りません。特に、ブロックしない乱数源のような場合、利用可能なバイト数だけを返すことがあります。

このため、テストコードで Read(b) を呼び出した際に、b のサイズが1040バイトを超えていると、Solarisの環境では一度の Read 呼び出しで b の全バイトを埋めることができず、テストが失敗する可能性がありました。これは、テストが n == len(b) を期待しているためです。

この問題を解決し、テストが異なるOS環境でも安定して動作するようにするために、io.ReadFull の使用が導入されました。io.ReadFull は、指定されたバイト数を完全に読み取るまで繰り返し Read を呼び出すユーティリティ関数です。

前提知識の解説

crypto/rand パッケージ

Go言語の crypto/rand パッケージは、暗号学的に安全な乱数を生成するためのインターフェースを提供します。このパッケージは、オペレーティングシステムが提供する乱数源(例: Linuxの /dev/urandom、Windowsの CryptGenRandom)を利用して、高品質な乱数を生成します。Reader というグローバル変数があり、これは io.Reader インターフェースを実装しており、暗号学的に安全な乱数バイトを読み取ることができます。

io.Reader インターフェース

Go言語の io パッケージは、I/Oプリミティブを提供します。Reader インターフェースは、データを読み取るための基本的な抽象化です。

type Reader interface {
    Read(p []byte) (n int, err error)
}

Read メソッドは、p に最大 len(p) バイトを読み取り、読み取ったバイト数 n とエラーを返します。重要なのは、Readlen(p) バイトすべてを読み取るとは限らないという点です。特に、非ブロックI/Oや、利用可能なデータが少ない場合、要求されたバイト数よりも少ないバイト数を返すことがあります。エラーが nil でない限り、n は0より大きくなることがあります。

/dev/urandom

Unix系システムにおける /dev/urandom は、カーネルが提供する擬似乱数生成器(PRNG)です。これは、エントロピープールから乱数を生成し、必要に応じて再シードされます。/dev/urandom はブロックしないため、常に乱数を供給できますが、エントロピーが枯渇した場合でも、高品質な乱数を生成し続けるために、以前のエントロピーから予測可能な乱数を生成する可能性があります(ただし、暗号学的な目的には十分安全とされています)。

io.ReadFull 関数

io パッケージの ReadFull 関数は、io.Reader から正確に指定されたバイト数を読み取ることを保証するユーティリティ関数です。

func ReadFull(r Reader, buf []byte) (n int, err error)

ReadFull は、r.Read(buf) を繰り返し呼び出し、buf が完全に埋まるまでデータを読み取ろうとします。もし buf が完全に埋まる前に EOF に達した場合、または他のエラーが発生した場合、ErrUnexpectedEOF または元のエラーを返します。これにより、呼び出し元は、要求したバイト数がすべて読み取られたことを確実に期待できます。

技術的詳細

このコミットの技術的詳細は、io.ReaderRead メソッドの挙動と、特定のOS環境におけるシステム乱数源の特性の組み合わせに起因する問題点を解決することにあります。

  1. io.Reader.Read の非保証性: 前述の通り、io.Reader インターフェースの Read メソッドは、呼び出し元が指定したバッファのサイズ(len(p))と同じバイト数を常に読み取るとは限りません。これは、ネットワークI/O、ファイルI/O、または乱数源のような様々なI/Oソースにおいて一般的な挙動です。例えば、ネットワークソケットから読み取る場合、利用可能なデータが少ないと、要求されたバイト数よりも少ないバイト数が返されることがあります。

  2. Solaris /dev/urandom の制限: Solaris 11より前のバージョンでは、カーネルが /dev/urandom から一度に読み取れるバイト数に1040バイトという内部的な制限がありました。これは、Read システムコールが一度に返すことができる最大バイト数に影響を与えます。したがって、crypto/rand.Reader が内部的に /dev/urandom を使用している場合、テストコードが Read(b) を呼び出し、len(b) が1040バイトを超えていると、Read は1040バイトしか返さず、残りのバイトは読み取られません。

  3. テストの失敗: 元のテストコードでは、n != len(b) または err != nil の場合に t.Fatalf を呼び出してテストを失敗させていました。Solarisの環境で len(b) が1040バイトを超えると、n は1040となり、len(b) と一致しないため、テストが意図せず失敗してしまいます。これは、コードのバグではなく、テスト環境の特性によるものです。

  4. io.ReadFull による解決: io.ReadFull は、この問題を解決するための適切なツールです。ReadFull は、内部で io.ReaderRead メソッドを繰り返し呼び出し、指定されたバッファが完全に埋まるまでデータを読み取ります。これにより、Solarisの /dev/urandom のような、一度の Read 呼び出しで全バイトを返さない可能性のあるI/Oソースに対しても、テストが期待する「全バイトの読み取り」を保証できます。テストは、io.ReadFull が成功したかどうか、つまり n == len(b) かつ err == nil であるかをチェックすればよくなります。

この変更は、Goの標準ライブラリのテストが、様々なオペレーティングシステムや環境で堅牢に動作するようにするための重要な修正です。

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

src/pkg/crypto/rand/rand_test.go ファイルの TestRead 関数が変更されました。

--- a/src/pkg/crypto/rand/rand_test.go
+++ b/src/pkg/crypto/rand/rand_test.go
@@ -7,6 +7,7 @@ package rand
 import (
 	"bytes"
 	"compress/flate"
+	"io"
 	"testing"
 )
 
@@ -16,9 +17,9 @@ func TestRead(t *testing.T) {
 		n = 1e5
 	}
 	b := make([]byte, n)
-	n, err := Read(b)
+	n, err := io.ReadFull(Reader, b)
 	if n != len(b) || err != nil {
-		t.Fatalf("Read(buf) = %d, %s", n, err)
+		t.Fatalf("ReadFull(buf) = %d, %s", n, err)
 	}
 
 	var z bytes.Buffer

具体的には以下の2点が変更されています。

  1. "io" パッケージのインポートが追加されました。
  2. n, err := Read(b) の行が n, err := io.ReadFull(Reader, b) に変更されました。
  3. エラーメッセージも Read(buf) から ReadFull(buf) に更新されました。

コアとなるコードの解説

変更された行 n, err := io.ReadFull(Reader, b) は、crypto/rand パッケージのグローバルな乱数源である Reader から、バイトスライス b のサイズ分だけデータを読み取ることを試みます。

  • Reader: これは crypto/rand パッケージが提供する io.Reader インターフェースを実装した変数で、システムが提供する暗号学的に安全な乱数源へのアクセスを提供します。
  • b: これは make([]byte, n) で作成されたバイトスライスで、乱数バイトが読み込まれるバッファです。n はテストの実行環境に応じて1000または100000に設定されます。

io.ReadFull を使用することで、Reader が一度の Read 呼び出しで len(b) バイトすべてを返さなかったとしても、io.ReadFull が内部で繰り返し Reader.Read を呼び出し、最終的に b の全バイトが埋まるか、またはエラーが発生するまで読み取りを続けます。

これにより、Solarisの /dev/urandom のような特定の環境で発生する「一度に読み取れるバイト数の制限」の問題が回避され、テストが期待する「バッファが完全に埋まること」が保証されるようになります。テストは、io.ReadFull が成功した場合(n == len(b) かつ err == nil)にのみ通過するようになります。

この修正は、テストの堅牢性を高め、異なるプラットフォーム間での一貫したテスト結果を保証するために重要です。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (特に src/pkg/crypto/rand/rand_test.gosrc/io/io.go)
  • Solarisの /dev/urandom の挙動に関する一般的な情報(Web検索による)
  • io.Reader インターフェースの Read メソッドのセマンティクスに関するGo言語のドキュメントと慣習
  • Go言語のコミット履歴と関連する議論