PHP
64bit
32bit

32bitOSから64bitOSに切り替えたときにバグが発現したお話

More than 1 year has passed since last update.

オンプレミスからクラウドに移行したときにOSのビット数が変わったことによって(元々よろしくない実装をしていたものの)バグが発現したお話です。
そういう移行作業をする予定がない、PHPのarray_mergeの挙動に自信がある人、配列のキー(添字)の挙動に自信のある人は本記事を読む必要はないと思います。

PHPにおけるarray_mergeの仕様

PHP: array_merge - Manual

入力配列の中にある数値添字要素の添字の数値は、 結果の配列ではゼロから始まる連続した数値に置き換えられます。

挙動確認用コード
<?php
$data1 = array(100 => 'hoge', 300 => 'fuga');
$data2 = array(200 => 'bar', 400 => 'piyo');
var_dump(array_merge($data1, $data2));
# array(4) {
#   [0]=>
#   string(4) "hoge"
#   [1]=>
#   string(4) "fuga"
#   [2]=>
#   string(3) "bar"
#   [3]=>
#   string(4) "piyo"
# }
# キーが数値だったので0から振り直されている

配列のキーの仕様

PHP: 配列 - Manual

key は、整数 または 文字列です。

integer として妥当な形式の文字列は integer 型にキャストされます。

挙動確認用コード
<?php
$data = ['50' => 'hoge'];
var_dump(gettype(array_keys($data)[0]));
# string(7) "integer"
# '50'はintegerにキャストできるのでintegerになっています

バグの再現

勘の良い人はもう気付いたかもしれませんね。
キャスト、ビット数、array_merge…

以下のコードはPHP4系でも動くようにしています。

sample.php(問題のあったコードの再現例)
<?php
# データを一意に特定するためのIDを配列で保持
$requestTargetIds = array('12345678901234', '98765432109876');

# 上記の配列を引数に、異なる2つのデータリソースから配列を取得した
# その配列のキーがID、というデータ形式だった
$data1 = getData1($requestTargetIds); # array('98765432109876' => 'data1');
$data2 = getData2($requestTargetIds); # array('12345678901234' => 'data2');

# merge
$result = array_merge($data1, $data2);

# 元のデータの順に並び替えたかったのか、foreachでソートしようとしてた
$sortedResult = array();
foreach ($requestTargetIds as $id) {
    if (isset($result[$id])) {
        $sortedResult[] = $result[$id];
    }
}
var_dump($sortedResult);

※再現例なので変なところもあるかもしれませんが目をつぶってください。

実行結果です。

result@32bit
$ uname -m
i686
$ php sample.php
array(2) {
  [0]=>
  string(5) "data2"
  [1]=>
  string(5) "data1"
}
result@64bit
$ uname -m
x86_64
$ php sample.php
array(0) {
}

はい、つらい:joy:

まとめ

PHP: 整数 - Manual

整数のサイズはプラットフォームに依存しますが、 約 20 億 (32 ビット符号付) が一般的な値です。 64 ビットプラットフォームでの通常の最大値は、およそ 9*10^18 (900京) になります。

キーが32bitだとintegerの範囲内に収まらないため、キャストされず文字列として扱われるためarray_mergeしても問題がありませんでした。
しかし64bit環境になったことでintegerの範囲内に収まってしまい、キャストされてarray_mergeをしたときにキーが振り直され、foreach内の処理に引っかからなくなった…
というものです。

薄々お気づきかもしれませんが、元々の実装がPHP4時代に記述されたかなりレガシーなコードで、かつ危険な実装をしていました。
それを諸々の事情でオンプレミスからクラウドに移行したタイミングでビット数が変わり発現した形になったのです。

まだ32bitで動いているサーバをお持ちの方は、こんな危険な実装をしていないか、一度ご確認いただければと思います:innocent: