PHPプログラムを書いたらマイナス21億行目あたりでエラーが出た

(2016/10/5 20:40 追記)誤解を招いている部分がありそうなので文末に補足を追記しました。巨大なプログラムを食わせただけでPHPが死ぬわけではありません。

毎度おなじみ、意図的に重箱の隅をつついてみたよって話です。あるPHPプログラムを実行したら次のようなエラーに遭遇しました。

$ php over-2g-lines.php
int(0)
PHP Fatal error:  Uncaught Error: Call to undefined function var____dump() in /Users/hnw/over-2g-lines.php:2150000004
Stack trace:
#0 {main}
  thrown in /Users/hnw/over-2g-lines.php on line -2144967292

21億5千万4行目で致命的エラーが発生したよ!という表示のあとでスタックトレースが表示されているんですが、スタックトレースの方ではマイナス21.4億行目あたりでエラーが出ていることになっています。行数がマイナスというのは不思議ですね。

種明かしするまでもないと思いますが、原因は32bit整数のオーバーフローです。PHPVM命令を管理するzend_op構造体のlinenoメンバは32bit符号無し整数で管理されているため、PHPは43億行目あたりまでしか正確にプログラムの行数をカウントできません。

さらに、printfの修飾子として%dなどとsignedの指定をしている場所があると上のように21.5億あたりでオーバーフローして負数になってしまいます。これはもちろんバグですが、修正したところで誰得な気がします。気が向いたらバグレポを出そうかなと思いつつ、今日はブログ記事にして寝ることにします。

ちなみに上記結果はPHP 7.0.9のものですが、PHP 5系(5.3.0以降)でも大差ないエラー表示になります。

再現方法

手元でも再現したい方のために、実験に使ったPHPプログラムをgistに上げておきました。次のようにすれば元のプログラムを手元に復元できます。

$ curl 'https://gist.githubusercontent.com/hnw/128439edf806daadbdf548b730d67627/raw/over-2g-lines.php.bz2.base64' | base64 -D | bzip2 -cd > over-2g-lines.php

gistに上がっているファイルは2KBほどですが、展開後のPHPファイルは約2GBになりますので、テキストエディタなどで開くと確実に大惨事です。ご注意ください。

ちなみにPHPファイルの内容は以下の通りです。

<?php

/* (21億5千万行の空行) */

$x=0;
var_dump($x);
var____dump($x);

補足というか蛇足というか

21.5億行を超えるPHPプログラムを動かすとPHPがエラーを返すように読み取った方がいるかもしれませんが、そうではありません。巨大なプログラムの後ろの方でわざとエラーを出してみた、というのが上の実験内容です。実際、21億5千万3行目のvar_dump($x)の結果は正しく出力されています(最初の実行結果を参照ください)。21.5億行目から43億行目の間あたりでエラーを起こした場合にエラー表示箇所の行数表示がマイナスになるバグがあるみたい、というのがお伝えしたかった内容でした。

そもそも、古代のBASIC以外のプログラミング言語にとって行数というのは重要なパラメータではなく、エラー発生時に発生箇所をわかりやすく表示するための補助情報に過ぎません。これはPHPにおいても同じで、どれだけ行数の多いプログラムを与えてもそれだけで死ぬことはありません。

もっとも、マトモな処理が21億行書かれたプログラムを与えるとプログラムのparseだけで大量のCPU時間とメモリを消費してしまい、別の理由で死ぬ可能性が高いと思います。これは他の言語でも同じでしょう。