PHP7.1以降なおったバグたち

  • 54
    いいね
  • 1
    コメント

2016/12/01に出たPHP7.1.0以降、2017/04/13に出た7.1.4までに修正されたバグのうち、目立ったものを取り出してみた。
メモリリークとかSegfaultとか長すぎるやつとかは確かめるのが面倒なのでスルー。

array_key_exists fails on arrays created by get_object_vars

オブジェクトから配列にしたらarray_key_existsでキーが見つからないという問題。

    $json = '{"2":1}';
    $obj = json_decode($json);
    $arr = get_object_vars($obj);
    $key = array_key_exists('2', $arr); // falseになる

これは$arr[2=>1]ではなく["2"=>1]になっているせいで発生する問題です。
元々7.2でなおすとか言ってた気がするんだけど7.1.3でなおってました

PHP hangs when an invalid value is dynamically passed to typehinted by-ref arg

特定の条件下で参照渡しタイプヒンティングに正しくない値を渡すとPHPがハングアップする問題。

    namespace Foo;

    set_error_handler(function () {
        throw new \Exception();
    });

    call_user_func(function (array &$ref) {}, 'not_an_array_variable');

7.1.0から7.1.2という狭いバージョン内でだけ発生します。

やたら発生条件が細かくて、正しい引数を渡したりnamespaceを消したりset_error_handlerを消したり&を消したりcall_user_funcを普通の関数にしたりするだけで発生しなくなります。
むしろどうやって発見したんだこれ。

ield from1251('dfd') ("from1251" is a valid function name) fails

↓のコードがシンタックスエラーになる。

    function from1($a){
        return $a;
    }
    function foo(){
        yield from1('foo');
    }

何処も間違ってないのにParse error: syntax error, unexpected '('が発生します。

どうやらこれ、ジェネレータ委譲の検出が誤爆しているようです。

なお関数名をfromaに変えると発生しなくなるので、fromのあと数値が来るとおかしくなるようです。
7.1.4で修正されました。

ArrayObject can not notice changes

ArrayObject内のオブジェクトが呼ばれたことを検出できない。

    class MyArrayObject extends ArrayObject{
        function __construct($input=[]){
            parent::__construct($input,ArrayObject::ARRAY_AS_PROPS);
        }
        function offsetSet($x,$v){
            echo "offsetSet('{$x}')\n";
            return parent::offsetSet($x,$v);
        }
        function offsetGet($x){
            echo "offsetGet('{$x}')\n";
            return parent::offsetGet($x);
        }
        function __get($x){
            echo "__get('{$x}')\n";
        }
        function __set($x,$v){
            echo "__set('{$x}')\n";
        }
        function offsetExists($x){
            echo "offsetExists('{$x}')\n";
            return parent::offsetExists($x);
        }
    }
    $x=new MyArrayObject;
    $x->hello=5;
    $x['hi']=10;
    $x['hello']++;
    @$x->a->b=7;
    $x->a->b++;

PHPでは存在しない$x->aに対して$x->a->bとアクセスすると、いきなりstdClass$x->aが生えます

ArrayObject::ARRAY_AS_PROPSを使って作成したArrayObjectに対してそうアクセス場合、これまではoffsetGetも__getも反応しませんでした。
変更が検知できないため、想定していない値を入れ放題になってしまいます。
そのため7.1.4でoffsetGetが反応するようになりました。

ArrayObject::ARRAY_AS_PROPSを指定していない場合は元々__getが反応する状態だったため問題なかったようです。

ていうかHHVMではARRAY_AS_PROPSが効かないのか。

Swatch time value incorrect for dates before 1970

1970年以前のスウォッチ・インターネットタイムずれることがある

    var_dump(gmdate('Y-m-d H:i:s B', -1130636800)); // 1934-03-04 22:13:20 968
    var_dump(gmdate('Y-m-d H:i:s B',  1457129600)); // 2016-03-04 22:13:20 967

    var_dump(gmdate('Y-m-d H:i:s B', -1130644800)); // 1934-03-04 20:00:00 875
    var_dump(gmdate('Y-m-d H:i:s B',  1457121600)); // 2016-03-04 20:00:00 875 正常な場合もある

PHP5以来ずっとこうなっていて、7.1.4で初めて修正されました。
そもそもスウォッチ・インターネットタイムなんて誰も使ってないからどうでもいいよね。

stream_get_contents maxlength>-1 returns empty string on windows

stream_get_contentsの第二引数$maxlengthを入れると、値が取得できない。

    $fd = stream_socket_client("udp://8.8.8.8:53", $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT);
    stream_set_blocking($fd, 0);
    stream_socket_sendto($fd,base64_decode("1oIBAAABAAAAAAAAB2V4YW1wbGUDb3JnAAABAAE="));
    sleep(1);
    var_dump(stream_get_contents($fd, 100)); // string(0)
    var_dump(stream_get_contents($fd)); // string(45)
    stream_socket_shutdown($fd,STREAM_SHUT_RDWR);

一回目のstream_get_contentsが何故か0バイトになっています。
第二引数100を削除すると一回目で値が取れます。

3v4lでは外部接続できないのでいつから発生してるかはわかりませんが、PHP7.1.3で修正されました。

PHP on Linux should use /dev/urandom when getrandom is not available

Linuxプラットフォームでrandom_int()したときに、getrandomが入ってなければExceptionが出るから、その場合はdev/urandomを見るべきというもの。

7.1.3で修正されました。
ただgetrandomが入ったのは2014/10/05のカーネル3.17なので、カーネルがそれ以前なのにPHP7.1.3以降が入ってる環境なんてそうそうなさそうではある。

なおrandom_intのマニュアルだと、

On Linux, the » getrandom(2) syscall will be used if available.
On other platforms, /dev/urandom will be used.

と書かれてるから元の動作も間違っているというわけではない。

is_callable callable name reports misleading value for anonymous classes

無名クラスをis_callableしたときに、第三引数callable_nameが"クラス名::メソッド名"ではなく"class@anonymous"になってる

    trait callback{
      public function __invoke($p){}
    }

    class foo { use callback; }
    $inline = new class { use callback; };

    var_dump(
      is_callable($inline, false, $target),
      $target, // class@anonymous
      is_callable([$inline, '__invoke'], false, $target),
      $target, // class@anonymous
      is_callable(new foo, false, $target),
      $target // foo::__invoke
    );

7.1.3で修正され、"class@anonymous/in/kTN4e0x7fcc76b5b092::__invoke"のようなよくわからないクラス名が返ってくるようになりました。
コンパイル時に内部で発行された名前?

substr_count with length=0 broken

substr_countの第4引数$lengthが0のとき動作が正しくない。

    var_dump(substr_count('aaa', 'a', 0, 0)); // 3
    var_dump(substr_count('aaa', 'a', 0, 1)); // 1

何故か0ではなく3になる
7.1.3で修正されました。
そもそも7.1より前は0に対応してなかったのでそのままでよかったんじゃないかと思いますが、どうやらGeneralize support of negative string offsetsに対応したときにエンバグしたようです。

ReflectionFunction for imagepng is missing last two parameters

imagepng引数が前の2個しか取得できないという謎のバグ。
7.13で修正されました。

    $ref = new ReflectionFunction('imagepng'); 
    var_dump($ref->getParameters()); // $imと$toだけ

実際はReflectionのバグではなく、GDのZEND_BEGIN_ARG_INFO_EXマクロが正しく登録されていなかったせいのようです。
これは引数の型チェックをPHPにやらせるためのもので、型チェックが不要なら無くても問題は無いっぽいです。
Reflectionの正確性が努力目標だとは知らなかった。

gost-crypto hash incorrect if input data contains long 0xFF sequence

暗号化する文字列中に"0xFF"が約40文字以上あった場合、正しく暗号化されないというバグ。

    $test = str_repeat("\xFF", 40);
    echo hash('gost-crypto', $test);

7.1.2までは8e0be2995864c40f8111feaa9df6fc4830632fdf61e365d9feca87f1e485d1f7(間違い)で、7.1.3以降は正しく231d8bb980d3faa30fee6ec475df5669cf6c24bbce22f46d6737470043a99f8eとなります。

"new DateTime()" sometimes returns 1 second ago value on PHP 7.1.0

時々new DateTime()が1秒前の値を返すというバグ

    $prev_dt = new DateTime();
    while (true) {
        $dt = new DateTime();
        if ($prev_dt > $dt) {
            var_dump($prev_dt->format("Y-m-d H:i:s.u"));
            var_dump($dt->format("Y-m-d H:i:s.u"));
            break;
        }
        $prev_dt = $dt;
    }

本来無限ループにならないといけないはずですが、だいたい数秒で中断されます。

動作を見るかぎりでは、秒が1進むよりほんの少し前にマイクロ秒が0に戻るので、そのときにnew DateTime()されると1秒戻る、みたいな動作をしてるっぽい感じでした。
7.1.3で修正されました。

intval() with base 0 should detect binary

intval2進数に対応していない

    var_dump(intval('0b1101', 0)); // 0

そもそもマニュアルにも16進数と8進数しか対応しないと書いてあるので仕様な気もしますが、7.1.2でしれっと2進数に対応しました。
マニュアルの変更履歴はまだ書かれていません。

感想

まだ半分も見てないのに色々出てきますね。
大半は一生遭遇することもないマイナーな関数の変な使い方によるものですが、PHPの場合たまーにこういうところにしれっと致命的な修正が入ってきたりすることがあるから怖い。
幸い今回は大きなものは見当たらないっぽい様子でした。