この記事は、 CakePHP Advent Calendar 2019 2日目の記事です。
1日目の記事は、hgsgtkさんの「来たるCakePHP 4.0 を知ろう – Qiita」でした。
TL;DR: PHP 5.6 を使っている人は paragonie/random_compat を入れましょう。
先月に、UUIDの衝突に関する話題が盛り上がりました。
10秒で衝突するUUIDの作り方 – Speaker Deck
この話のキモは、UUIDの生成に擬似乱数関数の mt_rand
を使用しているとシードにより乱数が固定化するので、同じUUIDが生成されてしまうということです。
特にプログラムコード中で不用意に mt_srand
を呼び出していると、衝突しやすくなります。
CakePHP にも Cake\Utility\Text::uuid
というメソッドがあり、UUIDv4が生成できるようになっています。データベーステーブルでidフィールドをchar(36)
で定義すると、レコード追加時にこのメソッドが呼ばれてUUIDがセットされるようになっています。
Cake\Utility\Text::uuid
は、
public static function uuid()
{
$random = function_exists('random_int') ? 'random_int' : 'mt_rand';
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
// 32 bits for "time_low"
$random(0, 65535),
$random(0, 65535),
// 16 bits for "time_mid"
$random(0, 65535),
// 12 bits before the 0100 of (version) 4 for "time_hi_and_version"
$random(0, 4095) | 0x4000,
// 16 bits, 8 bits for "clk_seq_hi_res",
// 8 bits for "clk_seq_low",
// two most significant bits holds zero and one for variant DCE1.1
$random(0, 0x3fff) | 0x8000,
// 48 bits for "node"
$random(0, 65535),
$random(0, 65535),
$random(0, 65535)
);
}
と実装されており、random_int
関数のない環境(< PHP 7.1)では、mt_rand関数が使用されます。
というわけで、Cake\Utility\Text::uuid()
を使用した 衝突のサンプルコード を書いてみました。
サンプルコード抜粋
use Cake\Utility\Text;
function make_seed()
{
list($usec, $sec) = explode(' ', microtime());
return $sec + $usec * 1000000;
}
$repeats = isset($argv[1]) ? (int)$argv[1] : 1;
for ($i = 0; $i < $repeats; $i++) {
mt_srand(make_seed()); // !! HAHAHAHAHHHAHA !!
echo Text::uuid() . "\n";
}
サンプルコードでは不用意な mt_srand
の使用例として、ありがちなマイクロ秒をシードとして与えています。
なお、シードとしてマイクロ秒を与えなくても mt_srand
が受け取るのは32bit整数の範囲なので、「10秒で衝突するUUIDの作り方」にあるように mt_srand
を呼び出している時点で65,536回試行すればだいたい衝突します。
この問題は、CakePHP 3.x を PHP5.6環境で利用している時に発生します。PHP 7.1以降では、random_int
関数が使用されるため mt_rand
に起因する問題は起こりません。
(CakePHP 4では、PHP 7.2以降のためこの問題は発生しません。)
Cake\Utility\Text::uuid()
の呼び出し前に、 mt_srand
が存在しない場合は衝突しにくくなりますが、分散環境でPID、時間が一致する場合は同じ乱数シードとなるので同じUUIDが生成されることがあります。
(参考: https://twitter.com/zeriyoshi/status/1199639847457046529)
というわけで、UUIDv4の生成にはmt_randを使わないようにする必要があります。そのためには、PHP 5.6でも random_int
等の関数を使うことができるようになる paragonie/random_compat パッケージを入れましょう。
以下のように、
composer require "paragonie/random_compat":"^2.0|9.99.99"
としてrandom_compatをインストールすれば、PHP7以降の環境では、何もしないバージョンである v9.99.99
が入りますので、将来的にPHPをバージョンアップする場合でも安心です。
補足
いちおう、この問題については Issue を上げていますので、将来のバージョン(3.9以降)では何らかの対策がされると思います。
Collision uuid in PHP 5.6 · Issue #13944 · cakephp/cakephp
Deprecation of mt_rand in uuid generation by nojimage · Pull Request #13958 · cakephp/cakephp
追記
3.9以降、paragonie/random_compatが必須となり、uuid生成でmt_randは使用されなくなります。
Deprecation of mt_rand in uuid generation by nojimage · Pull Request #13958 · cakephp/cakephp
NFLHH93oLXG
4hfsVudvbke
CO8vC4E3r8Q
crf1GUNeZAe
ZmCs2wIHyPM
FXdbOoNt72U
s1r4nq7eEbP
0TEilqHUvQi
2kI1rLxvUwI
zts0yn5Tnml
SH9nGkYgBWe
vm33oj0rlpd
twJz5810MYE
xEOrHUHcGEg
CYFffird9gx
hcK6HcQmC7W
U4WAm5tbKx0
MG7CNYogSyf
WL5bxO0nij6
qnDRvFBPHLI
6TSnKMkNeA0
q7RCovIeV3x
dzI6shtw5xj
7F3lR1CokEN
aiwGcCXckVH
BmQunS43uqq
MgbWIQ3ou2H
XVnl3jspcu4
e3gUwXgwEYv
oHZyEN0dd6P
7lJf9Gh9l1u
IaUji2EMnIf
xAcYp7DJ6bU
RkDcgh4A4en
g21uRi0WRAc