ちりもつもればミルキーウェイ

好奇心に可処分時間が奪われる

Go1.19のcrypto/randではunix環境のReadで内部バッファがなくなるので色々しらべた

はじめに

先日なんとなしに go.1.19 release note よんでたら、crypto/randの項目で興味深い一文を見つけた

Read no longer buffers random data obtained from the operating system between calls.

当時の僕も気になっていたよう

もともと CVE-2021-3538 をちゃんと読む - ちりもつもればミルキーウェイ を調べたときにunix系OSの場合はcrypto/randが /dev/urandom からの読み取りを内部でバッファリングしてることは認識していた。

素朴に考えるとbufなくすとパフォーマンス落ちたりしそうだけどな〜とおもったので背景とか調べた次第

該当のパッチ

github.com

パッチの概要

commitメッセージに詳しいので一部抜粋

The kernel's RNG is fast enough, and buffering means taking locks, which
we don't want to do. So just remove all buffering. This also means the
randomness we get is "fresher". That also means we don't need any
locking, making this potentially faster if multiple cores are hitting
GetRandom() at the same time on newer Linuxes.

雑に和訳すると「カーネルの乱数生成器は十分早いし、まあそもそもバッファ取ろうとするとlockが必要になるから素直にバッファ無しで呼び出したほうがよいくね?そしたらlockへるから複数CPUでGetRandom()するときはなんなら速くね?」みたいな感じ。

ほんまか?

わからんがこれが通ったってことは一定の妥当性があるんだろう。

ベンチマークとかが見当たらないが、まあmutexでのlockが不要になるとかんがえると、複数回ガチャガチャ採番するusecaseではこっちのほうが早いのかも

読んでみる

該当パッチにはいくつか差分があるけど、Readまわりに絞って読む

まず reader.used の扱いが大きく変わった
いままではboolだったが、

  • 0: 未利用
  • 1: 利用中, random値の提供元の f を掴んでない
  • 2: 利用中, f を必ず掴んでいる

という扱いに変わったよう。

エントロピー貯まるまでblockする処理は lockからusedへのCASに変わってる

ほんで random提供元をまだ掴んでいないかの判定はusedのatomicな読み込みになってる

f 掴んでなければ、openしまくらないようにlockとってから /dev/urandom をopenしてhideAgainReaderで包んで f に突っ込むよう。

あと!!地味に fからの読み込みがio.ReadFull使うように変更された のでunix実装で altGetRandom が設定されてなければ CVE-2021-3538 みたいな悲劇は起きなくなるぞ!!!!

コメントされててそうなったんだな。謝謝
https://go-review.googlesource.com/c/go/+/390038

For a future CL, I wonder if we should wrap this in a io.ReadFull. We don't promise that Reader.Read won't return a short read (while rand.Read does make that promise) but it feels like a ~free way to defuse a footgun. No one wants to start processing partial reads from this Reader, no?

buffer使ってるとbufへのRead()は排他的にしないと内部でもってるposとかおかしなことになってぶっ壊れうるけど、/dev/urandom自体は複数スレッドからぽんぽこreadしてよいってことらしい。一応同一fdへの読み取りは 中でRWLock取られてる し安心

mutexだと常に排他だけど、RWLockはwriteされない限りlockしないので、(/dev/urandomにおいては横から書き込みとか無いだろうから)実質lockがなくなった感じだな!

まとめ

buffer使わなくなるとmutexでのlockが不要になって、/dev/urandom読み取りのRWLockだけになるぞ!

そうすると(/dev/urandomには書き込みがなされないだろうから)関連するatomic処理はまあどうしても消せないがlockのオーバーヘッドはだいたい0みたいなもんで良さげっぽい!

あとついでに、このパッチによってunixの場合は /dev/urandom からの読み出しが Read() じゃなくて io.ReadFull() になったのでうれしいね!!
これはコメントからこういう方針ですすむオーラを感じたので、他OSのrandom sourceにおいても io.ReadFull() におきかえるのは contirbute チャンスかもしれん

[追記] のちにcrypto/randひとしきり読んだけど、見る限り全部 io.ReadFull() を使っていたのでチャンスなかった。まあ雑につかってあれ?読み込めてないじゃん!がほぼ無いということなのでよいことではある