ファイルの存在を確認してから操作するまでの間に、別のプロセスがファイルを変更・削除する可能性がある。これを TOCTOU(Time Of Check To Time Of Use)競合または race condition と呼ぶ。
問題のあるコード
# ❌ TOCTOU 競合が発生するコード
import os
if os.path.exists('data.txt'):
# この間に別プロセスがファイルを削除する可能性がある
with open('data.txt') as f:
data = f.read()
# FileNotFoundError が発生する可能性
存在確認と実際の操作の間にタイムラグがあるため、マルチプロセス環境やサーバーアプリケーションでは問題になる。
EAFP スタイルで書く
Python では「許可を求めるより許しを請う方が簡単(EAFP: Easier to Ask for Forgiveness than Permission)」というスタイルが推奨される。
# ✅ EAFP スタイル
try:
with open('data.txt') as f:
data = f.read()
except FileNotFoundError:
data = None
ファイルのオープンと存在確認が一度の操作で行われるため、競合が発生しない。
ファイル削除の競合
ファイルを削除する場合も同様だ。
# ❌ 競合が発生するコード
if os.path.exists('temp.txt'):
os.remove('temp.txt') # 別プロセスが先に削除していたらエラー
例外処理で対応するか、Python 3.8 以降の missing_ok パラメータを使う。
# ✅ 正しい方法
try:
os.remove('temp.txt')
except FileNotFoundError:
pass
# または Python 3.8 以降
from pathlib import Path
Path('temp.txt').unlink(missing_ok=True)
ファイル作成の競合
新しいファイルを作成する際も、存在確認と作成の間に競合が起きる可能性がある。
# ❌ 競合が発生するコード
if not os.path.exists('new_file.txt'):
with open('new_file.txt', 'w') as f:
f.write('data')
# 別プロセスが同時に同じファイルを作成する可能性
排他的作成モード 'x' を使うと、ファイルが存在する場合はエラーになる。
# ✅ 排他的作成
try:
with open('new_file.txt', 'x') as f:
f.write('data')
except FileExistsError:
print('ファイルはすでに存在します')
ディレクトリ作成の競合
ディレクトリ作成でも同じ問題が起きる。
# ❌ 競合が発生するコード
if not os.path.exists('newdir'):
os.makedirs('newdir') # 別プロセスが先に作成していたらエラー
exist_ok=True を使うと、すでに存在していてもエラーにならない。
# ✅ 正しい方法
os.makedirs('newdir', exist_ok=True)
# または pathlib
from pathlib import Path
Path('newdir').mkdir(parents=True, exist_ok=True)
セキュリティ上の問題
TOCTOU 競合はセキュリティ脆弱性にもなりうる。攻撃者が存在確認と操作の間にシンボリックリンクをすり替えることで、意図しないファイルにアクセスさせる攻撃(symlink attack)が可能になる。
# ❌ シンボリックリンク攻撃に脆弱
if os.path.isfile('/tmp/safe_file'):
# この間に攻撃者がシンボリックリンクに置き換える
with open('/tmp/safe_file', 'w') as f:
f.write(sensitive_data)
# 実際には /etc/passwd などに書き込まれる可能性
セキュリティが重要な場面では、ファイルディスクリプタを先に取得してから操作を行う。