CakePHP 3.x の PHP 5.6向けUUID実装では衝突がおきるかもねというお話

標準

この記事は、 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)
        );
    }

src/Utility/Text.php L61-83

と実装されており、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

コメントを残す

Page optimized by WP Minify WordPress Plugin