[インデックス 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
とエラーを返します。重要なのは、Read
は len(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.Reader
の Read
メソッドの挙動と、特定のOS環境におけるシステム乱数源の特性の組み合わせに起因する問題点を解決することにあります。
-
io.Reader.Read
の非保証性: 前述の通り、io.Reader
インターフェースのRead
メソッドは、呼び出し元が指定したバッファのサイズ(len(p)
)と同じバイト数を常に読み取るとは限りません。これは、ネットワークI/O、ファイルI/O、または乱数源のような様々なI/Oソースにおいて一般的な挙動です。例えば、ネットワークソケットから読み取る場合、利用可能なデータが少ないと、要求されたバイト数よりも少ないバイト数が返されることがあります。 -
Solaris
/dev/urandom
の制限: Solaris 11より前のバージョンでは、カーネルが/dev/urandom
から一度に読み取れるバイト数に1040バイトという内部的な制限がありました。これは、Read
システムコールが一度に返すことができる最大バイト数に影響を与えます。したがって、crypto/rand.Reader
が内部的に/dev/urandom
を使用している場合、テストコードがRead(b)
を呼び出し、len(b)
が1040バイトを超えていると、Read
は1040バイトしか返さず、残りのバイトは読み取られません。 -
テストの失敗: 元のテストコードでは、
n != len(b)
またはerr != nil
の場合にt.Fatalf
を呼び出してテストを失敗させていました。Solarisの環境でlen(b)
が1040バイトを超えると、n
は1040となり、len(b)
と一致しないため、テストが意図せず失敗してしまいます。これは、コードのバグではなく、テスト環境の特性によるものです。 -
io.ReadFull
による解決:io.ReadFull
は、この問題を解決するための適切なツールです。ReadFull
は、内部でio.Reader
のRead
メソッドを繰り返し呼び出し、指定されたバッファが完全に埋まるまでデータを読み取ります。これにより、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点が変更されています。
"io"
パッケージのインポートが追加されました。n, err := Read(b)
の行がn, err := io.ReadFull(Reader, b)
に変更されました。- エラーメッセージも
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言語の
crypto/rand
パッケージのドキュメント: https://pkg.go.dev/crypto/rand - Go言語の
io
パッケージのドキュメント: https://pkg.go.dev/io - Go言語の
io.ReadFull
関数のドキュメント: https://pkg.go.dev/io#ReadFull - このコミットのGo Gerritレビューページ: https://golang.org/cl/6113046
参考にした情報源リンク
- Go言語のソースコード (特に
src/pkg/crypto/rand/rand_test.go
とsrc/io/io.go
) - Solarisの
/dev/urandom
の挙動に関する一般的な情報(Web検索による) io.Reader
インターフェースのRead
メソッドのセマンティクスに関するGo言語のドキュメントと慣習- Go言語のコミット履歴と関連する議論