Carpe Diem

備忘録

rand.Readerはいつエラーを返すのか

概要

golangのuuid生成で有名な satori/go.uuidこちらのコミットでUUIDv4を生成する際にエラーを返すように変わりました。
理由は

github.com

このissueに対応するためなのですが、内部で使っているrand.Read()はそもそもどんな時にエラーが起きるのだろう?と気になって調べてみました。

環境

rand.Reader

ドキュメントにはこう書いてあります。

Reader is a global, shared instance of a cryptographically secure random number generator.

On Linux and FreeBSD, Reader uses getrandom(2) if available, /dev/urandom otherwise. On OpenBSD, Reader uses getentropy(2). On other Unix-like systems, Reader reads from /dev/urandom. On Windows systems, Reader uses the CryptGenRandom API. On Wasm, Reader uses the Web Crypto API.

ref: rand - The Go Programming Language

つまりLinuxの場合getrandomを使用し、それが使えなかったら/dev/urandomから直接取得するとのこと。

ちょっと用語が増えてきたので、必要な前提知識を先に説明します。

乱数とエントロピープールの話

Linuxでは乱数を生成するデバイスファイル/dev/random/dev/urandomの2つが用意されています。
乱数はエントロピープールから取り出した乱数種(ランダムシード)を元に生成します。
名前が非常によく似ているこの2つのデバイスファイルは、エントロピープールが枯渇した時の挙動が異なります。

エントロピープールとは

Linuxカーネルの保持するバッファで複数の環境ノイズ源から乱雑な数値を集めたものです。 環境ノイズ源には、

  • 入力デバイスの発生イベント種別や間隔
  • 割り込み発生間隔
  • ディスクシーク時間

などがあります。

$ cat /proc/sys/kernel/random/entropy_avail

とすればエントロピープールの貯蓄状態を確認できます。
何回か叩けば値が変わることが分かりますが、これは前述した入力デバイスの間隔をノイズ源として増えています。

/dev/random

エントロピープールは有限で、

$ cat /dev/random

乱数を延々吐き出すと枯渇して処理をブロックしてしまいます。

f:id:quoll00:20190626141742g:plain

このように十分なエントロピーがないとちょっと出力しただけで止まってしまいます。
また/dev/randomではセキュリティの観点上一度使ったエントロピーは再利用されません。

/dev/urandom

/dev/urandomは過去に使った乱数種を再利用することで処理を続けます。
つまり/dev/randomと違って延々と乱数文字列を吐き出し続ける事が可能です。

$ cat /dev/urandom

と入力すると延々と乱数が生成されます。

f:id:quoll00:20190626141801g:plain

もちろん利便性は高いですが、同じ乱数種を利用するということはランダムでは無いとも言えます。
なので真の乱数ではなく擬似乱数的に生成されていると言われます。

基本的にこちらはブロックしませんが、ブート時などはエントロピープールの初期化が完了していない場合はブロックされることもあります。

getrandom

システムコールの1つです。
/dev/randomまたは/dev/urandomからデータを読み出すことと同じ機能で、デフォルトでは/dev/urandomから読み出します。オプションで/dev/randomからも引き出せます。

わざわざシステムコールとして用意されてるのはなぜ?

/dev/random/dev/urandomはデバイスファイルであるため、使用する際にはopenシステムコールで開く(=ファイルディスクリプタを割り当てる)必要があります。
なのでファイルディスクリプタを限界まで使用済みの状態では新たにファイルを開けないため、これら乱数の読み出しも不可能になります。
そうならないようシステムコールとして追加されました。

どんな時にエラーが起きるのか

話を戻します。

getrandomを使用し、それが使えなかったら/dev/urandomから直接取得

なので、これらがエラーを返した時にrand.Readerはエラーを返す事が分かります。

getrandomの時

ありうるのはEAGAINで、

EAGAIN - The requested entropy was not available, and getrandom() would have blocked if the GRND_NONBLOCK flag was not set.

ref: getrandom(2) - Linux manual page

とあります。つまり要求したエントロピーが利用できないケースですね。
実体は/dev/urandomなので基本的に枯渇しないですが、ブート時などエントロピープールの初期化が完了していない時は発生しそうです。

/dev/urandomの時

上記のケースに加え、/dev/urandomを直接使う時はファイルディスクリプタも関わってくるのでファイルディスクリプタが枯渇していればエラーが発生します。

まとめ

基本的にrand.Read()はエラーを返さないようです。
ただ特定のユースケースでは発生しうるため、用途がはっきりしていれば無視しても問題ないですがそうでない場合はちゃんとエラーハンドリングしましょう。

その他

satoriは長期間メンテされてないので使っている方は

github.com

へのリプレースをオススメします。

ソース