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

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

このコミットでは、Go言語の標準ライブラリである crypto/rand パッケージにおいて、Windows環境でのゼロ長リード(長さ0のバイトスライスを読み込もうとする操作)がクラッシュする問題を修正しています。具体的には、以下の2つのファイルが変更されました。

  • src/pkg/crypto/rand/rand_test.go: ゼロ長リードの挙動を検証するための新しいテストケースが追加されました。
  • src/pkg/crypto/rand/rand_windows.go: Windows固有の乱数生成器 CryptGenRandom を呼び出す前に、入力バッファの長さがゼロである場合に早期リターンするガード句が追加されました。

コミット

crypto/rand パッケージにおけるWindows環境でのゼロ長リード時のクラッシュを修正。

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

https://github.com/golang/go/commit/b752ab2e90a29e8c56a19457c736065085d4ab09

元コミット内容

crypto/rand: zero length reads shouldn't crash on Windows.

R=golang-dev, dave
CC=golang-dev
https://golang.org/cl/6496099

変更の背景

Go言語の crypto/rand パッケージは、暗号学的に安全な乱数を生成するためのインターフェースを提供します。内部的には、オペレーティングシステムが提供する乱数生成器を利用します。Windows環境では、CryptGenRandom というAPIが使用されます。

このコミットがなされる以前、crypto/randRead メソッドに長さがゼロのバイトスライス([]byte{}nil)が渡された場合、Windows固有の実装である rand_windows.go 内で syscall.CryptGenRandom が呼び出されていました。通常、I/O操作においてゼロ長のバッファを渡すことは、何も読み込まないことを意味し、エラーなく成功するべき挙動です。しかし、特定の条件下または CryptGenRandom の内部的な挙動により、ゼロ長のバッファが渡された際にクラッシュ(プログラムの異常終了)が発生する可能性がありました。

このクラッシュは、Goプログラムの安定性と信頼性を損なう重大な問題であり、特に暗号関連の処理では予測不能な挙動は許容されません。そのため、このエッジケースを適切に処理し、クラッシュを防ぐための修正が必要とされました。

前提知識の解説

crypto/rand パッケージ

crypto/rand はGo言語の標準ライブラリの一部で、暗号学的に安全な乱数ジェネレータを提供します。これは、鍵生成、セッションIDの生成、ソルトの生成など、セキュリティが重要な場面で利用されます。このパッケージの主要なインターフェースは io.Reader を実装した Reader 変数であり、rand.Read(b []byte) メソッドを通じて乱数をバイトスライス b に書き込みます。

io.Reader インターフェース

Go言語の io パッケージで定義されている Reader インターフェースは、データを読み込むための汎用的なインターフェースです。

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

Read メソッドは、p に最大 len(p) バイトのデータを読み込み、読み込んだバイト数 n とエラー err を返します。慣例として、len(p) が0の場合、Read(0, nil) を返すことが期待されます。これは、何も読み込むデータがないため、エラーではないということを示します。

syscall.CryptGenRandom

CryptGenRandom は、Microsoft Windowsオペレーティングシステムが提供する暗号サービスプロバイダ(CSP)APIの一部です。この関数は、暗号学的に強力な乱数を生成するために使用されます。そのシグネチャは通常、以下のような形式です(Goの syscall パッケージではGoの型にマッピングされます)。

BOOL CryptGenRandom(
  HCRYPTPROV hProv,
  DWORD      dwLen,
  BYTE       *pbBuffer
);
  • hProv: 暗号サービスプロバイダのハンドル。
  • dwLen: 生成する乱数のバイト数。
  • pbBuffer: 生成された乱数を格納するためのバッファへのポインタ。

この関数は、dwLen で指定されたバイト数の乱数を pbBuffer が指すメモリ領域に書き込みます。問題は、dwLen が0の場合や pbBufferNULL(Goでは nil スライスに対応)の場合に、CryptGenRandom がどのように振る舞うかという点にありました。一部のWindowsバージョンや特定のCSPの実装では、これらのエッジケースで予期せぬエラーやクラッシュを引き起こす可能性がありました。

ゼロ長リード

プログラミングにおいて、ゼロ長リードとは、読み込み操作に対して長さがゼロのバッファを渡すことを指します。これは、ファイル、ネットワークソケット、乱数ジェネレータなど、様々なI/Oソースに対して行われる可能性があります。一般的なI/Oインターフェースの設計では、ゼロ長リードはエラーではなく、単に0バイトが読み込まれたことを示す (0, nil) を返すことが期待されます。これにより、呼び出し元は特別なエラー処理をすることなく、通常のI/Oループを継続できます。

技術的詳細

このコミットの技術的詳細は、Windowsの CryptGenRandom APIの挙動と、Goの io.Reader インターフェースの慣例との間の不一致に起因します。

Goの io.Reader インターフェースの慣例では、Read(p []byte) メソッドに len(p) == 0 のバイトスライスが渡された場合、n=0, err=nil を返すことが期待されます。これは、何も読み込むデータがないため、成功とみなされるべきだからです。

しかし、crypto/rand のWindows実装である rngReader.Read メソッドは、内部で syscall.CryptGenRandom を呼び出していました。CryptGenRandom は、dwLen パラメータに生成するバイト数を、pbBuffer パラメータにそのバイトを格納するバッファへのポインタを期待します。

問題は、len(b) が0の場合に syscall.CryptGenRandom(r.prov, uint32(len(b)), &b[0]) を呼び出すと発生しました。

  • uint32(len(b))0 になります。これは問題ありません。
  • しかし、&b[0] の部分が問題となる可能性がありました。Goにおいて、長さ0のスライス b に対して &b[0] を評価しようとすると、実行時パニック(インデックス範囲外エラー)が発生する可能性があります。たとえパニックが発生しなかったとしても、CryptGenRandomNULL ポインタや無効なポインタを受け取った場合に、Windows APIの内部でアクセス違反などのクラッシュを引き起こす可能性がありました。

このコミットは、この潜在的なクラッシュを防ぐために、CryptGenRandom を呼び出す前に明示的なチェックを追加しました。len(b) == 0 の場合、CryptGenRandom を呼び出すことなく、Goの io.Reader の慣例に従って (0, nil) を即座に返します。これにより、Windows APIの不適切な呼び出しを回避し、プログラムの安定性を確保しています。

また、この修正を検証するために、rand_test.go に新しいテストケース TestReadEmpty が追加されました。このテストは、Reader.Read に長さ0のバイトスライスと nil スライスをそれぞれ渡した場合に、期待される (0, nil) の結果が返されることを確認します。これにより、将来的に同様の回帰バグが発生するのを防ぎます。

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

src/pkg/crypto/rand/rand_test.go

--- a/src/pkg/crypto/rand/rand_test.go
+++ b/src/pkg/crypto/rand/rand_test.go
@@ -30,3 +30,14 @@ func TestRead(t *testing.T) {
 		t.Fatalf("Compressed %d -> %d", len(b), z.Len())
 	}
 }
+
+func TestReadEmpty(t *testing.T) {
+	n, err := Reader.Read(make([]byte, 0))
+	if n != 0 || err != nil {
+		t.Fatalf("Read(make([]byte, 0)) = %d, %v", n, err)
+	}
+	n, err = Reader.Read(nil)
+	if n != 0 || err != nil {
+		t.Fatalf("Read(make(nil) = %d, %v", n, err)
+	}
+}

src/pkg/crypto/rand/rand_windows.go

--- a/src/pkg/crypto/rand/rand_windows.go
+++ b/src/pkg/crypto/rand/rand_windows.go
@@ -35,6 +35,10 @@ func (r *rngReader) Read(b []byte) (n int, err error) {
 		}
 	}
 	r.mu.Unlock()
+
+	if len(b) == 0 {
+		return 0, nil
+	}
 	err = syscall.CryptGenRandom(r.prov, uint32(len(b)), &b[0])
 	if err != nil {
 		return 0, os.NewSyscallError("CryptGenRandom", err)

コアとなるコードの解説

src/pkg/crypto/rand/rand_test.go の変更

TestReadEmpty 関数が追加されました。

  1. n, err := Reader.Read(make([]byte, 0)): 長さゼロのバイトスライスを Reader.Read に渡した場合の挙動をテストします。期待される結果は n=0 かつ err=nil です。もしこれらが満たされない場合、テストは失敗します。
  2. n, err = Reader.Read(nil): nil バイトスライスを Reader.Read に渡した場合の挙動をテストします。Goでは nil スライスも長さゼロのスライスとして扱われることが多いため、これも同様に n=0 かつ err=nil が期待されます。

これらのテストケースは、crypto/randRead メソッドが io.Reader インターフェースの慣例に従って、ゼロ長リードに対して適切に (0, nil) を返すことを保証します。

src/pkg/crypto/rand/rand_windows.go の変更

rngReader.Read メソッドの内部に、以下のガード句が追加されました。

	if len(b) == 0 {
		return 0, nil
	}

このコードは、syscall.CryptGenRandom を呼び出す直前に配置されています。

  • len(b) == 0 の条件が真(つまり、読み込むべきバイトスライスの長さがゼロ)の場合、
  • 関数は即座に n=0err=nil を返して終了します。

これにより、以下の問題が解決されます。

  1. クラッシュの回避: 長さゼロのバイトスライスに対して &b[0] を評価しようとしたり、CryptGenRandom が無効なポインタを受け取ったりすることによる潜在的なクラッシュを防ぎます。
  2. io.Reader 慣例への準拠: io.Reader インターフェースの期待される挙動(ゼロ長リードで (0, nil) を返す)に明示的に準拠します。これにより、クロスプラットフォームでの一貫した動作が保証されます。

この変更により、CryptGenRandom は実際に乱数を生成する必要がある(つまり len(b) > 0 の場合)にのみ呼び出されるようになり、堅牢性が向上しました。

関連リンク

参考にした情報源リンク

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

このコミットでは、Go言語の標準ライブラリである crypto/rand パッケージにおいて、Windows環境でのゼロ長リード(長さ0のバイトスライスを読み込もうとする操作)がクラッシュする問題を修正しています。具体的には、以下の2つのファイルが変更されました。

  • src/pkg/crypto/rand/rand_test.go: ゼロ長リードの挙動を検証するための新しいテストケースが追加されました。
  • src/pkg/crypto/rand/rand_windows.go: Windows固有の乱数生成器 CryptGenRandom を呼び出す前に、入力バッファの長さがゼロである場合に早期リターンするガード句が追加されました。

コミット

crypto/rand パッケージにおけるWindows環境でのゼロ長リード時のクラッシュを修正。

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

https://github.com/golang/go/commit/b752ab2e90a29e8c56a19457c736065085d4ab09

元コミット内容

crypto/rand: zero length reads shouldn't crash on Windows.

R=golang-dev, dave
CC=golang-dev
https://golang.org/cl/6496099

変更の背景

Go言語の crypto/rand パッケージは、暗号学的に安全な乱数を生成するためのインターフェースを提供します。内部的には、オペレーティングシステムが提供する乱数生成器を利用します。Windows環境では、CryptGenRandom というAPIが使用されます。

このコミットがなされる以前、crypto/randRead メソッドに長さがゼロのバイトスライス([]byte{}nil)が渡された場合、Windows固有の実装である rand_windows.go 内で syscall.CryptGenRandom が呼び出されていました。通常、I/O操作においてゼロ長のバッファを渡すことは、何も読み込まないことを意味し、エラーなく成功するべき挙動です。しかし、特定の条件下または CryptGenRandom の内部的な挙動により、ゼロ長のバッファが渡された際にクラッシュ(プログラムの異常終了)が発生する可能性がありました。

このクラッシュは、Goプログラムの安定性と信頼性を損なう重大な問題であり、特に暗号関連の処理では予測不能な挙動は許容されません。そのため、このエッジケースを適切に処理し、クラッシュを防ぐための修正が必要とされました。

前提知識の解説

crypto/rand パッケージ

crypto/rand はGo言語の標準ライブラリの一部で、暗号学的に安全な乱数ジェネレータを提供します。これは、鍵生成、セッションIDの生成、ソルトの生成など、セキュリティが重要な場面で利用されます。このパッケージの主要なインターフェースは io.Reader を実装した Reader 変数であり、rand.Read(b []byte) メソッドを通じて乱数をバイトスライス b に書き込みます。

io.Reader インターフェース

Go言語の io パッケージで定義されている Reader インターフェースは、データを読み込むための汎用的なインターフェースです。

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

Read メソッドは、p に最大 len(p) バイトのデータを読み込み、読み込んだバイト数 n とエラー err を返します。慣例として、len(p) が0の場合、Read(0, nil) を返すことが期待されます。これは、何も読み込むデータがないため、エラーではないということを示します。

syscall.CryptGenRandom

CryptGenRandom は、Microsoft Windowsオペレーティングシステムが提供する暗号サービスプロバイダ(CSP)APIの一部です。この関数は、暗号学的に強力な乱数を生成するために使用されます。そのシグネチャは通常、以下のような形式です(Goの syscall パッケージではGoの型にマッピングされます)。

BOOL CryptGenRandom(
  HCRYPTPROV hProv,
  DWORD      dwLen,
  BYTE       *pbBuffer
);
  • hProv: 暗号サービスプロバイダのハンドル。
  • dwLen: 生成する乱数のバイト数。
  • pbBuffer: 生成された乱数を格納するためのバッファへのポインタ。

この関数は、dwLen で指定されたバイト数の乱数を pbBuffer が指すメモリ領域に書き込みます。問題は、dwLen が0の場合や pbBufferNULL(Goでは nil スライスに対応)の場合に、CryptGenRandom がどのように振る舞うかという点にありました。一部のWindowsバージョンや特定のCSPの実装では、これらのエッジケースで予期せぬエラーやクラッシュを引き起こす可能性がありました。

ゼロ長リード

プログラミングにおいて、ゼロ長リードとは、読み込み操作に対して長さがゼロのバッファを渡すことを指します。これは、ファイル、ネットワークソケット、乱数ジェネレータなど、様々なI/Oソースに対して行われる可能性があります。一般的なI/Oインターフェースの設計では、ゼロ長リードはエラーではなく、単に0バイトが読み込まれたことを示す (0, nil) を返すことが期待されます。これにより、呼び出し元は特別なエラー処理をすることなく、通常のI/Oループを継続できます。

技術的詳細

このコミットの技術的詳細は、Windowsの CryptGenRandom APIの挙動と、Goの io.Reader インターフェースの慣例との間の不一致に起因します。

Goの io.Reader インターフェースの慣例では、Read(p []byte) メソッドに len(p) == 0 のバイトスライスが渡された場合、n=0, err=nil を返すことが期待されます。これは、何も読み込むデータがないため、成功とみなされるべきだからです。

しかし、crypto/rand のWindows実装である rngReader.Read メソッドは、内部で syscall.CryptGenRandom を呼び出していました。CryptGenRandom は、dwLen パラメータに生成するバイト数を、pbBuffer パラメータにそのバイトを格納するバッファへのポインタを期待します。

問題は、len(b) が0の場合に syscall.CryptGenRandom(r.prov, uint32(len(b)), &b[0]) を呼び出すと発生しました。

  • uint32(len(b))0 になります。これは問題ありません。
  • しかし、&b[0] の部分が問題となる可能性がありました。Goにおいて、長さ0のスライス b に対して &b[0] を評価しようとすると、実行時パニック(インデックス範囲外エラー)が発生する可能性があります。たとえパニックが発生しなかったとしても、CryptGenRandomNULL ポインタや無効なポインタを受け取った場合に、Windows APIの内部でアクセス違反などのクラッシュを引き起こす可能性がありました。

このコミットは、この潜在的なクラッシュを防ぐために、CryptGenRandom を呼び出す前に明示的なチェックを追加しました。len(b) == 0 の場合、CryptGenRandom を呼び出すことなく、Goの io.Reader の慣例に従って (0, nil) を即座に返します。これにより、Windows APIの不適切な呼び出しを回避し、プログラムの安定性を確保しています。

また、この修正を検証するために、rand_test.go に新しいテストケース TestReadEmpty が追加されました。このテストは、Reader.Read に長さ0のバイトスライスと nil スライスをそれぞれ渡した場合に、期待される (0, nil) の結果が返されることを確認します。これにより、将来的に同様の回帰バグが発生するのを防ぎます。

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

src/pkg/crypto/rand/rand_test.go

--- a/src/pkg/crypto/rand/rand_test.go
+++ b/src/pkg/crypto/rand/rand_test.go
@@ -30,3 +30,14 @@ func TestRead(t *testing.T) {
 		t.Fatalf("Compressed %d -> %d", len(b), z.Len())
 	}
 }
+
+func TestReadEmpty(t *testing.T) {
+	n, err := Reader.Read(make([]byte, 0))
+	if n != 0 || err != nil {
+		t.Fatalf("Read(make([]byte, 0)) = %d, %v", n, err)
+	}
+	n, err = Reader.Read(nil)
+	if n != 0 || err != nil {
+		t.Fatalf("Read(make(nil) = %d, %v", n, err)
+	}
+}

src/pkg/crypto/rand/rand_windows.go

--- a/src/pkg/crypto/rand/rand_windows.go
+++ b/src/pkg/crypto/rand/rand_windows.go
@@ -35,6 +35,10 @@ func (r *rngReader) Read(b []byte) (n int, err error) {
 		}
 	}
 	r.mu.Unlock()
+
+	if len(b) == 0 {
+		return 0, nil
+	}
 	err = syscall.CryptGenRandom(r.prov, uint32(len(b)), &b[0])
 	if err != nil {
 		return 0, os.NewSyscallError("CryptGenRandom", err)

コアとなるコードの解説

src/pkg/crypto/rand/rand_test.go の変更

TestReadEmpty 関数が追加されました。

  1. n, err := Reader.Read(make([]byte, 0)): 長さゼロのバイトスライスを Reader.Read に渡した場合の挙動をテストします。期待される結果は n=0 かつ err=nil です。もしこれらが満たされない場合、テストは失敗します。
  2. n, err = Reader.Read(nil): nil バイトスライスを Reader.Read に渡した場合の挙動をテストします。Goでは nil スライスも長さゼロのスライスとして扱われることが多いため、これも同様に n=0 かつ err=nil が期待されます。

これらのテストケースは、crypto/randRead メソッドが io.Reader インターフェースの慣例に従って、ゼロ長リードに対して適切に (0, nil) を返すことを保証します。

src/pkg/crypto/rand/rand_windows.go の変更

rngReader.Read メソッドの内部に、以下のガード句が追加されました。

	if len(b) == 0 {
		return 0, nil
	}

このコードは、syscall.CryptGenRandom を呼び出す直前に配置されています。

  • len(b) == 0 の条件が真(つまり、読み込むべきバイトスライスの長さがゼロ)の場合、
  • 関数は即座に n=0err=nil を返して終了します。

これにより、以下の問題が解決されます。

  1. クラッシュの回避: 長さゼロのバイトスライスに対して &b[0] を評価しようとしたり、CryptGenRandom が無効なポインタを受け取ったりすることによる潜在的なクラッシュを防ぎます。
  2. io.Reader 慣例への準拠: io.Reader インターフェースの期待される挙動(ゼロ長リードで (0, nil) を返す)に明示的に準拠します。これにより、クロスプラットフォームでの一貫した動作が保証されます。

この変更により、CryptGenRandom は実際に乱数を生成する必要がある(つまり len(b) > 0 の場合)にのみ呼び出されるようになり、堅牢性が向上しました。

関連リンク

参考にした情報源リンク