TOP > 読み物 > PHP3のバグじゃなかろか?

PHP3のバグじゃなかろか?

2000-12-28
山田 智史

それってどういうこと?

今年の夏に気づいて、暇を見ながら少しずつ調べていたのですが、 やっぱり PHP3 のバグのようです。 具体的な現象は

『.htaccess に記述していない設定に、 不正な値が設定される恐れがある』

というものです。 文字列値設定の場合は、参照しているポインタの先が不正になっているため、 場合によってはサーバが crash したりする(といっても子プロセスだけだと思いますが) 可能性もあります。

なお、本現象は PHP のバグレポートページ (の PHP3 用レポートページ) において、
ID# 3319
ID# 4287
ID# 4682
などで報告されているものと同一のようです。

それってマズいんじゃないの?

本現象は必ず発生するものではなく、サーバの設定など条件がいくつかあります。 それは以下のような場合です。

  • DSO版PHP3 を使っている(最新版を含む最近のバージョン)
  • .htaccess に PHP3 の設定が書けるようになっている(AllowOverride してある)
  • PHP3 の設定が記述された .htaccess が複数存在する
  • その中に、ある .htaccess には記述してるけど別のには記述していない、 という設定がある

つまり、例えばとあるディレクトリA の .htaccess には、php3_i18n_http_output と php3_include_path があり、別のディレクトリB の .htaccess には、 php3_i18n_http_output はあるが、php3_include_path はない、 というようなケースです。

このような環境の場合、ディレクトリA をアクセスした直後にディレクトリB をアクセスすると、ディレクトリB ではディレクトリA をアクセスしたときの設定値がデフォルト値として使われてしまいます。

ちなみに同じ子プロセスにて異なるディレクトリが読み込まれる必要があるので、 子プロセスの数が少ない方が発生しやすいと思います。

とりあえず apc_tags を使った簡単なデモをご用意しましたのでまずはお試しください。

ディレクトリCの .htaccess では php3_asp_tags の設定がないため、 直前に参照したディレクトリでの値が設定される可能性があります。

ディレクトリA → ディレクトリC、 ディレクトリB → ディレクトリC とアクセスしてみてください。

なお、お持ち帰り用にまとめたものもご用意しております。このデモでは何となくイヤだったので使いませんでしたが、何でしたら適宜 phpinfo() とか getmypid() を追加していただくとよりわかりやすいかと思います。

↑のようなケースでは ASP 互換タグが利用できなくなってしまうだけ (それはそれで問題ですが)、その他に考えられるケースとしては gpc_order を指定してる .htaccess と指定していない .htaccess があれば 後者のディレクトリでは、不正な文字列が gpc_order として認識され、 最終的に QUERY_STRING から CGI 変数が正しく渡されずに、スクリプト内で その変数の値を参照することができなくなってしまいます。

また、include_path を指定してる .htaccess と指定していない .htaccess があれば、その指定がない .htaccess を参照するスクリプトで不正な値がパス名として使われ、 include() でそんなファイルねーよ、とか怒られてしまったり、 require() できずに終わってしまったり、 はじめに書いたように不正な値のところにアクセスしてサーバ(子プロセス)ごと落ちたり、 といったことが挙げられます。

なんでそーなるの?

詳しくは PHP3 のソース(特に mod_php3.c)をご覧下さい ^^;

だけではアレなので、具体的に説明します。 説明に使うソースは php-3.0.18-i18n-ja-2 に含まれるもの(かなり以前から更新されてませんが...)とします。ちなみに使うのは mod_php3.c だけです。

まず、.htaccess が Apache に読み取られ、解析されます。 ここで PHP3 に関する設定があれば、PHP3 モジュールの設定初期化ルーチン php3_create_dir() が呼び出されます。ここです。

   266	/*
   267	 * Create the per-directory config structure with defaults from php3_ini_master
   268	 */
   269	static void *php3_create_dir(pool * p, char *dummy)
   270	{
   271	    php3_ini_structure *new;
   272	    static int first_time = 1;
   273	
   274		php3_module_startup();
   275	    new = (php3_ini_structure *) palloc(p, sizeof(php3_ini_structure));
   276	    
   277	    if(first_time) {
   278	        memcpy(new,&php3_ini_master,sizeof(php3_ini_structure));
   279	        first_time=0;
   280	    } else {
   281	        memcpy(new,&php3_ini,sizeof(php3_ini_structure));
   282	    }
   283	    return new;
   284	}
   285	

ここで、281行目で php3_ini という変数が指す領域の内容を新たに確保した領域に memcpy() してるのを覚えておいてください。ちなみに php3_ini はグローバルな構造体です。

次に、読み込まれた .htaccess の内容を反映させるため、 php3_merge_dir() が呼び出されます。こんな感じです。

   286	/*
   287	 * Merge in per-directory .conf directives
   288	 */
   289	static void *php3_merge_dir(pool *p, void *basev, void *addv) 
   290	{
   291		php3_ini_structure *new = (php3_ini_structure *) palloc(p, sizeof(php3_ini_structure));
   292		php3_ini_structure *base = (php3_ini_structure *) basev;
   293		php3_ini_structure *add = (php3_ini_structure *) addv;
   294	
   295		/* Start with the base config */
   296		memcpy(new,base,sizeof(php3_ini_structure));
   297	
   298		/* Now, add any fields that have changed in *add compared to the master config */
   299		if (add->smtp != base->smtp) new->smtp = add->smtp;
   300		if (add->sendmail_path != base->sendmail_path) new->sendmail_path = add->sendmail_path;
   301		if (add->sendmail_from != base->sendmail_from) new->sendmail_from = add->sendmail_from;
   			:
   			:

ここで、base というのが先ほどの create_dir() で確保した領域で(多分)、 add というのが .htaccess の中にあった設定です。 要はベース設定と違うものだけ追加設定の値を使う、ということのようです。

ちなみにこの構造体の各メンバはこの前後で初期化されます(タイミングよくわかってません)。文字列値なら動的に palloc(malloc() の Apache API 版) されたものが使われます。

で、おしまいにページの生成処理のとこですが、 send_php3() の中ではこんな処理してます。

   165	int send_php3(request_rec *r, int display_source_mode, int preprocessed, char *filename)
   166	{
   167		int fd, retval;
   168		php3_ini_structure *conf;
   169	
   170		/* We don't accept OPTIONS requests, but take everything else */
   171		if (r->method_number == M_OPTIONS) {
   172			r->allowed |= (1 << METHODS) - 1;
   173			return DECLINED;
   174		}
   175	
   176		/* Make sure file exists */
   177		if (filename == NULL && r->finfo.st_mode == 0) {
   178			return NOT_FOUND;
   179		}
   180	
   181		/* grab configuration settings */
   182		conf = (php3_ini_structure *) get_module_config(r->per_dir_config,
   183														&php3_module);
   184		/* copy to active configuration */
   185		memcpy(&php3_ini,conf,sizeof(php3_ini_structure));
   186	
			:
			:

↑の最後のところ、185行目で最終的な設定値を php3_ini という変数に書き戻しています。

どうでしょう? 何が問題かおわかりいただけますでしょうか? 例えばですよ、とあるディレクトリの .htaccess を読み込んだ結果、

php3_ini 構造体の中身
gpc_order0x000c525c
i18n_http_output0x00086920

という値が設定され、処理が行われたとします。 で、次のリクエストにて別の .htaccess を読み込んだときに、 その .htaccess には gpc_order の記述がなくても

php3_ini 構造体の中身
gpc_order0x000c525c
i18n_http_output0x00086920

が使われるということになりませんか? なりますよね? 多分なると思うんだけど...

百聞は一見に如かず。
実際にソースに細工して↑のような出力を行う DSO を作成し、 わかりやすいように

StartServers 1
MaxClients 1

にした Apache を走らせ、↑のデモページを見たときの出力を示しましょう。 (ちなみにデモページにある設定の php3_i18n_http_output は出力するようにしなかったのでここには出てきてません)

(ディレクトリAにアクセス)
php3_create_dir(&php3_ini) (↓は←のアドレス)
 [ 13938] base address: 0x200ee100 ([ ] 内の数字はプロセスID)
 0x000868c0: include_path=/usr/local/lib/php3 ←一回目の呼び出しなのでマスター設定が使われる
 0x200f95f0: gpc_order=GPC
php3_create_dir() end
php3_merge_dir(addv) (以下同)
 [ 13938] base address: 0x000aca5c
 0x000acbac: include_path=A
 0x000acb5c: gpc_order=GPC
php3_merge_dir() end
send_php3(&php3_ini)
 [ 13938] base address: 0x200ee100
 0x000acbac: include_path=A
 0x000acb5c: gpc_order=GPC
send_php3() end
(ディレクトリBにアクセス)
php3_create_dir(&php3_ini)
 [ 13938] base address: 0x200ee100
 0x000acbac: include_path=A ←ほら直前の値が残ってる
 0x000acb5c: gpc_order=GPC ←たまたま残ってるけどゴミ同然
php3_create_dir() end
php3_merge_dir(addv)
 [ 13938] base address: 0x000aca64
 0x000acb8c: include_path=B
 0x000acb5c: gpc_order=SJIS ←なのでこんなことになる
php3_merge_dir() end
send_php3(&php3_ini)
 [ 13938] base address: 0x200ee100
 0x000acb8c: include_path=B
 0x000acb5c: gpc_order=SJIS ←これはたまたま文字列に見えるけどもちろん保証はありません
send_php3() end
(再びディレクトリAにアクセス)
php3_create_dir(&php3_ini)
 [ 13938] base address: 0x200ee100
 0x000acb8c: include_path=B ←ここも
 0x000acb5c: gpc_order=SJIS
php3_create_dir() end
php3_merge_dir(addv)
 [ 13938] base address: 0x000aca5c
 0x000acbac: include_path=A
 0x000acb5c: gpc_order=GPC ←明示的に記述してあると上書きしてくれる
php3_merge_dir() end
send_php3(&php3_ini)
 [ 13938] base address: 0x200ee100
 0x000acbac: include_path=A
 0x000acb5c: gpc_order=GPC
send_php3() end

いかがでしょう? これってバグというか多分考慮もれだと思うんですが、どうなんでしょうね?

どうすりゃいいのYO!

ということで回避策ですが、まず

  • ↑(発生条件)のような運用をしない

ということが挙げられます ^^;

あとは

  • とある .htaccess で記述した PHP3 設定は他の全ての .htaccess にも記述する

ぐらいしか思いつきません。

ソースから作り直したい、という向きには上記 mod_php3.c の 277 行目を

   277	    if(1) {

のように書き換えてしまってもよさゲなのですが、 何でわざわざ一回目とそれ以外を切り分けているのか、 その意図が今のとこよくわかってないので実はあんまりよくないのかもしれません。

改版履歴

Sun Jan 7 15:29:34 JST 2001
デモの内容を変更(あ、やべ、百聞は一見に如かず、のとこが前のままだ...)
2001-01-07
対象は文字列型設定だけではないため、一部記述変更
2001-01-07
bugs.php.net にて同様の現象が報告されてる旨追記