PHP5.1から実装されたrealpath cacheについて(2) - その動きと影響を受ける関数
前回に引き続き、realpath cacheについてのはなしです。どうキャッシュされるのか、動きを追っかけてみたいと思います。
(前回と同じく、コードは5.2.6をベースにしています)
たとえば、こんな感じのコードのとき。
<?php $filename = "/var/www/data.txt"; $data = file_get_contents($filename); ?>
相対パスだと、
<?php $filename = "data.txt"; // 第2引数がtrueだと相対パス時にinclude_pathを走査する $data = file_get_contents($filename, true); ?>
ファイルアクセスの前段にはstream wrapper
実際にコードを読む人のために補足しておくと(いねーよ)、PHPでファイルアクセスを伴う関数の多くは、ファイル実体にアクセスする前に、stream wrapperというものを介して抽象化されています。
PHPでは、fopen("http://www.yahoo.co.jp", "r") とか、 include "http://example.com/evli.inc" とかできて、リモートファイルインジェクション脆弱性的に「これはひどい」と叩かれる素敵な機能を持っているわけですが*1、これはこのストリームラッパーが間に挟まることで、実現しています。
ここにあるように、各種wrapperが標準で用意されており (PHP5.2からは data:ラッパーとかも標準で対応)、PECLでもssh2などが用意されており、stream_wrapper_register()によるユーザ定義のwrapperも作ることができます。
php_stream_open_wrapper_ex() (main/stream/stream.c)などで、streamに対しパスを指定して操作をしようとすると、
/path/to/file.txt http://example.com/hogemoge/ ftp://ftp.example.com/file
といったパスが渡ってきたとき、プロトコル部があり且つ、そのstream wrapperが登録されていれば、そいつに投げ、何もなければ通常のファイルアクセスのstream wrapper (plain files stream)に投げます。
realpath cacheにたどり着くおおまかな流れ
上のfile_get_contents()を呼び出したときの、realpath cacheに関係する部分に到達するまでの大まかな流れについては以下のようになります。回りくどいのう。
- zif_file_get_contents() (ext/standard/file.c)が呼び出される
- php_stream_open_wrapper_ex() (main/stream/stream.c)でストリームをオープンしにいく
- 普通のファイルということで、php_plain_files_stream_opener() が呼び出される
- php_plain_files_stream_opener() (_php_stream_fopen_with_path()へのマクロ) を include_pathを持って呼び出す
- 絶対パスでない場合、php_stream_fopen_rel() (_php_stream_fopen()へのマクロ)をinclude_pathの先頭から1つずつ渡しながら、開くまで叩く
- _php_stream_fopen()の中で、ファイルパスを展開するためexpand_filepath() (main/fopen_wrappers.c)をコール
- virtual_file_ex() (TSRM/tsrm_virtual_cwd.c)を呼び出し
cache処理の大まかな流れ
そして、realpath cacheに追加する処理は、virtual_file_ex()の中に存在しています。PHPのコード中、ここにしかないので、これを経由する処理では、キャッシュされるということになります。この関数が何をしてるかというと、おおざっぱに言えば、パスの解決処理。
その中で、こんな感じでキャッシュを操作しています
- 元パスを元に、キャッシュを格納しているhashを走査
- 見つかって且つ有効期限内であれば、それを使う
- なければ、realpath()をコール
- キャッシュのhashに追加する
まぁ、ありがちなパターン。
キャッシュ操作用の関数
外から利用できるcache操作用I/Fは、全クリアと、単一削除の関数のみです。
CWD_API void realpath_cache_clean(TSRMLS_D); CWD_API void realpath_cache_del(const char *path, int path_len TSRMLS_DC);
キャッシュ追加などは内部のみで利用となっていて、これらを呼んでいるのは、virtual_file_ex()だけなので、キャッシュの追加処理はすべて、この関数経由で行われることになります。
static inline unsigned long realpath_cache_key(const char *path, int path_len) static inline void realpath_cache_add(const char *path, int path_len, const char *realpath, int realpath_len, time_t t TSRMLS_DC) static inline realpath_cache_bucket* realpath_cache_find(const char *path, int path_len, time_t t TSRMLS_DC)
キャッシュをクリアするタイミング
PHPから、このキャッシュをクリアするタイミングとしてはこんな感じ
関数呼び出しでクリアできるのです。chroot()は必要なのはいいとして、clearstatcache()は、ドキュメントにそんな説明書いてないけどね。
まとめ
- PHPには、stream wrapperというstreamを抽象化するしくみをもっている
- include_pathの後ろの方にある相対パスはコストが高いので避けるべき
- chroot(), clearstatcache(), rename(), rmdir(), unlink()を呼び出したタイミングで、realpath cacheはクリアされる
- 叩いているとキャッシュの恩恵が得られずにパフォーマンスが低下するので注意
次のエントリでは、clearstatcache()の話を書きます。
*1:「これはひどい」と言われないように、セキュリティについて補足しておくと、外部URLなどのデータ読み込みは、allow_url_fopen=offで制限でき(PHP4以降)、制限ゆるめにinclude/requireについて無効にしたい場合は、allow_url_include=offで制限できます。(PHP5.2以降。デフォルトで無効にされるので前よりマシになった)