[インデックス 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/rand
の Read
メソッドに長さがゼロのバイトスライス([]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の場合や pbBuffer
が NULL
(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]
を評価しようとすると、実行時パニック(インデックス範囲外エラー)が発生する可能性があります。たとえパニックが発生しなかったとしても、CryptGenRandom
がNULL
ポインタや無効なポインタを受け取った場合に、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
関数が追加されました。
n, err := Reader.Read(make([]byte, 0))
: 長さゼロのバイトスライスをReader.Read
に渡した場合の挙動をテストします。期待される結果はn=0
かつerr=nil
です。もしこれらが満たされない場合、テストは失敗します。n, err = Reader.Read(nil)
:nil
バイトスライスをReader.Read
に渡した場合の挙動をテストします。Goではnil
スライスも長さゼロのスライスとして扱われることが多いため、これも同様にn=0
かつerr=nil
が期待されます。
これらのテストケースは、crypto/rand
の Read
メソッドが 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=0
とerr=nil
を返して終了します。
これにより、以下の問題が解決されます。
- クラッシュの回避: 長さゼロのバイトスライスに対して
&b[0]
を評価しようとしたり、CryptGenRandom
が無効なポインタを受け取ったりすることによる潜在的なクラッシュを防ぎます。 io.Reader
慣例への準拠:io.Reader
インターフェースの期待される挙動(ゼロ長リードで(0, nil)
を返す)に明示的に準拠します。これにより、クロスプラットフォームでの一貫した動作が保証されます。
この変更により、CryptGenRandom
は実際に乱数を生成する必要がある(つまり len(b) > 0
の場合)にのみ呼び出されるようになり、堅牢性が向上しました。
関連リンク
- Go CL 6496099: https://golang.org/cl/6496099 (Goのコードレビューシステムにおけるこの変更のページ)
- Go
crypto/rand
パッケージドキュメント: https://pkg.go.dev/crypto/rand - Go
io
パッケージドキュメント: https://pkg.go.dev/io
参考にした情報源リンク
- Microsoft Docs -
CryptGenRandom
function: https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-cryptgenrandom - Go
io.Reader
interface documentation (for expected behavior of zero-length reads): https://pkg.go.dev/io#Reader (特に "Implementations of Read are discouraged from returning a non-zero number of bytes with a non-nil error, or a zero number of bytes with a nil error unless EOF is reached." の部分が関連) - Go Slices: usage and internals: https://go.dev/blog/slices (スライスの内部構造、特に長さゼロのスライスや
nil
スライスの挙動について)
[インデックス 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/rand
の Read
メソッドに長さがゼロのバイトスライス([]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の場合や pbBuffer
が NULL
(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]
を評価しようとすると、実行時パニック(インデックス範囲外エラー)が発生する可能性があります。たとえパニックが発生しなかったとしても、CryptGenRandom
がNULL
ポインタや無効なポインタを受け取った場合に、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
関数が追加されました。
n, err := Reader.Read(make([]byte, 0))
: 長さゼロのバイトスライスをReader.Read
に渡した場合の挙動をテストします。期待される結果はn=0
かつerr=nil
です。もしこれらが満たされない場合、テストは失敗します。n, err = Reader.Read(nil)
:nil
バイトスライスをReader.Read
に渡した場合の挙動をテストします。Goではnil
スライスも長さゼロのスライスとして扱われることが多いため、これも同様にn=0
かつerr=nil
が期待されます。
これらのテストケースは、crypto/rand
の Read
メソッドが 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=0
とerr=nil
を返して終了します。
これにより、以下の問題が解決されます。
- クラッシュの回避: 長さゼロのバイトスライスに対して
&b[0]
を評価しようとしたり、CryptGenRandom
が無効なポインタを受け取ったりすることによる潜在的なクラッシュを防ぎます。 io.Reader
慣例への準拠:io.Reader
インターフェースの期待される挙動(ゼロ長リードで(0, nil)
を返す)に明示的に準拠します。これにより、クロスプラットフォームでの一貫した動作が保証されます。
この変更により、CryptGenRandom
は実際に乱数を生成する必要がある(つまり len(b) > 0
の場合)にのみ呼び出されるようになり、堅牢性が向上しました。
関連リンク
- Go CL 6496099: https://golang.org/cl/6496099 (Goのコードレビューシステムにおけるこの変更のページ)
- Go
crypto/rand
パッケージドキュメント: https://pkg.go.dev/crypto/rand - Go
io
パッケージドキュメント: https://pkg.go.dev/io
参考にした情報源リンク
- Microsoft Docs -
CryptGenRandom
function: https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-cryptgenrandom - Go
io.Reader
interface documentation (for expected behavior of zero-length reads): https://pkg.go.dev/io#Reader (特に "Implementations of Read are discouraged from returning a non-zero number of bytes with a non-nil error, or a zero number of bytes with a nil error unless EOF is reached." の部分が関連) - Go Slices: usage and internals: https://go.dev/blog/slices (スライスの内部構造、特に長さゼロのスライスや
nil
スライスの挙動について)