PHPのrename関数はファイルが存在すると上書きする仕様になっている。上書きrenameをさせたくなくてもだ。rename
の直前でfile_exists
でrename先のファイルが存在しないことをチェックしたとしても、上書きrenameの仕様を完全に解消することはできない。もちろん意図しない上書きが起こる可能性を下げることはできる。
file_put_contents('a', 'a');
echo "check if file does not exist\n";
file_exists('b') && exit(1); // ファイルが存在しないことチェック
echo "other program create file b\n";
file_put_contents('b', 'b'); // 他のプログラムが同時期にbを作ってしまう
echo "before rename\n";
rename('a', 'b'); // 他のプログラムが作ったbをaで上書きしてしまう
echo 'now file b content is ' . file_get_contents('b') . "\n";
unlink('b');
renameが内部でどんなシステムコールを使っているのか?
touch('a');
echo 'before rename';
rename('a', 'b');
echo 'after rename';
unlink('b');
write(1, "before rename", 13before rename) = 13
rename("a", "b") = 0
write(1, "after rename", 12after rename) = 12
Linuxのrename
インターフェイス
int rename(const char *oldpath, const char *newpath);
newpath が既に存在する場合、それは不可分操作で (atomically) 置き換えられる (ただし、いくつかの条件がある; 以下の「エラー」のセクションを参照)。 そのため、 newpath にアクセスしようとしている他のプロセスがファイルを見失うことはない (訳註: 常にアクセス可能である)。
── Man page of RENAME
PHPのrename
関数はLinuxのrename
システムコールをそのまま使っているようで、PHPのrename
の「newname が存在する場合、上書きされます」という仕様はLinuxの仕様を踏襲していると考えられる。
Linuxのシステムコールにはrenameat2
があり、これは上書きしないオプションが存在する。が、PHPではこれを使っていない。
考えられる対策
- ファイル名がかぶらないものにする(UUIDとか)
- linkで一時ファイルのハードリンクを作ってから一時ファイルを消す
続く