こんにちは。普段、他のモダンな言語(GoやNode.js、Rustなど)で非同期処理や並行プログラミングをバリバリ書いているあなたなら、PHPの「同期的な世界観」に少し息苦しさを感じたことがあるかもしれませんね。
「PHPだって昔に比べれば高速になったけれど、結局1リクエスト1スレッド(あるいはプロセス)の重い世界なんでしょ?」
そう思っていませんか?
実は、PHP 8.1で導入された Fiber(ファイバー) を正しく理解し、Zendエンジンの裏側の挙動まで見通すことができれば、PHPは単なる「Webのグループレンダリング言語」から、洗練された協調的並行処理(Cooperative Concurrency)を奏でる美しいエンジンへと姿を変えます。
今回は、特にCPUバウンドかつ重厚長大な処理の代表格である「暗号ハッシュ計算(Argon2idなど)」をFiberで並行化し、かつセキュリティの要であるサイドチャネル攻撃(タイミング攻撃)への耐性をどう担保するか、PHPの内部構造に踏み込みながら丁寧に紐解いていきましょう。
ここを理解すると、PHPの裏側が驚くほど綺麗に見えるようになりますよ。
—
1. Zend VMとFiberの本当の姿:なぜ「重い処理」でFiberが活きるのか?
まず、Node.jsのEvent LoopやGoのGoroutineと、PHPのFiberの決定的な違いを押さえておきましょう。
GoのGoroutineはGoランタイムがスケジューリングするプリエンプティブ(あるいはそれに近い)な並行処理ですが、PHPのFiberは「完全に制御がプログラマの手に委ねられた協調的マルチタスク(Coroutine)」です。つまり、Fiber自身が `Fiber::suspend()` を叩かない限り、Zend VMの実行コンテキストは切り替わりません。
ここで多くの開発者が誤解します。
「CPUをガッツリ使う重い処理(例えばArgon2idによるパスワードハッシュ生成など)は、どうせCPUバウンドなんだから、Fiberを使っても意味がないのでは?」と。
確かに、単一の重い処理を1つのFiberで回してもスレッド数が増えるわけではありません。しかし、「複数の独立したリクエストや、バッチ処理における複数の重い暗号計算を、単一プロセス内のイベントループ(AmpやReactPHPなど)と協調させて非同期にインターリーブ(織り交ぜて実行)させる」とき、Fiberは真価を発揮します。
Zend VMのコールスタックの切り替え
PHPでFiberが実行されるとき、Zendエンジンは通常のコールスタックとは別に、Fiber専用のスタックフレーム(`zend_execute_data` やヒープ上の実行コンテキスト)を割り当てます。
`Fiber::suspend()` が呼ばれると、現在のZend VMのポインタ状態が退避され、別のFiberのコンテキストにスイッチします。この切り替えはOSスレッドのコンテキストスイッチを伴わないため、極めて軽量です。
—
2. Argon2idの並行化と「タイミング攻撃」の罠
Webアプリケーションでユーザー登録や認証を行う際、パスワードハッシュには現在 `Argon2id` が黄金律とされています。これはサイドチャネル攻撃(特にGPUやASICを用いた総当たり攻撃)に対抗するため、意図的に大量のメモリ(Memory-hard)とCPU時間を消費するように設計されています。
しかし、ここに大きな罠があります。
もしあなたが、複数のユーザー登録リクエストを同時に処理する際、あるいは大量のデータストリームを検証する際に、この重いArgon2id計算を同期的に直列実行していたらどうなるでしょうか?
1つの計算に200ミリ秒かかるとして、5人分なら1秒間、Zendプロセスがその計算スレッドを占有(ブロック)してしまいます。これではイベントループの恩恵を完全に殺してしまいます。
そこで、計算コストの高い暗号処理を細切れ(チャンク)に分割、あるいは非同期のタスクキューに載せ、Fiberを介して協調的に処理を進めるアプローチが有効になります。
しかし、ここで忘れてはならないのがサイドチャネル攻撃への耐性です。特に「タイミング攻撃(Timing Attack)」は、処理にかかる時間の微小な差異から秘密情報(ハッシュの比較結果や内部の分岐条件)を暴く手法です。Fiberのコンテキストスイッチや、PHPのユーザースペースでの非同期制御を挟むことで、皮肉にも「実行時間の揺らぎ」が大きくなり、新たな脆弱性を生むリスクがあります。
—
3. 実装:Fiberと非同期イベントループによるArgon2idの安全な並行処理
百聞は一見に如かず。実際に、Fiberを使って複数の重い暗号計算を非同期に並行処理しつつ、タイミング攻撃を防ぐためのセキュアな比較を組み込んだコードの骨組みを見てみましょう。
ここでは、外部の非同期ライブラリ(Amp等)のイベントループの概念をシンプルに模した、協調的スケジューラの例で解説します。
/
class SimpleScheduler
{
/ @var \SplQueue
private \SplQueue $queue;
public function __construct()
{
$this->queue = new \SplQueue();
}
public function add(Fiber $fiber): void
{
$this->queue->enqueue($fiber);
}
public function run(): void
{
while (!$this->queue->isEmpty()) {
$fiber = $this->queue->dequeue();
if (!$fiber->isTerminated()) {
try {
// Fiberを再開(または開始)する
if (!$fiber->isStarted()) {
$fiber->start();
} else {
$fiber->resume();
}
// まだ終了していなければ、キューの末尾に戻して次のループへ(協調的マルチタスク)
if (!$fiber->isTerminated()) {
$this->queue->enqueue($fiber);
}
} catch (\Throwable $e) {
echo “エラー発生: ” . $e->getMessage() . “\n”;
}
}
}
}
}
/
- CPUバウンドな重い暗号処理をシミュレートしつつ、
- Fiberのサスペンドを挟んでイベントループをブロックしないようにする関数
/
function asyncArgon2idHash(string $password, SimpleScheduler $scheduler): Fiber
{
return new Fiber(function () use ($password, $scheduler) {
echo “-> 暗号計算開始 (PID: ” . getmypid() . “)\n”;
// Argon2idのコストパラメータ(実際には重い処理)
$options = [
‘memory_cost’ => PASSWORD_ARGON2_DEFAULT_MEMORY_COST,
‘time_cost’ => 2, // あえてコストをかける
‘threads’ => 2,
];
// PHPの内部関数 password_hash はブロッキングだが、
// ここでは処理をいくつかのステップに分割したと仮定して Fiber::suspend() を挟む
// ※実際にはC言語レベルのブロッキングを完全に避けることはできないため、
// 真の非同期化にはext-uvやpcntl等との組み合わせが必要になります。
Fiber::suspend(‘step1_memory_allocated’);
// 重いハッシュ生成の実行
$hash = password_hash($password, PASSWORD_ARGON2ID, $options);
Fiber::suspend(‘step2_hashed’);
return $hash;
});
}
// — 実行のシミュレーション —
$scheduler = new SimpleScheduler();
// 2つの異なるリクエストをFiberとして登録
$fiber1 = asyncArgon2idHash(‘UserPassword_A_123!’, $scheduler);
$fiber2 = asyncArgon2idHash(‘UserPassword_B_456!’, $scheduler);
$scheduler->add($fiber1);
$scheduler->add($fiber2);
echo “— イベントループ開始 —\n”;
$scheduler->run();
echo “— 全ての暗号処理が完了しました —\n”;
このコードのアーキテクチャ的解説
1. 協調的スケジューリング: `SimpleScheduler` がキューを管理し、複数のFiberを少しずつ進めます(インターリーブ)。これにより、1つの重い処理が完全にCPUを独占して他の処理をフリーズさせるのを防ぎます。
2. サイドチャネル攻撃への配慮(重要):
暗号処理の成否やハッシュの検証を行う際、絶対に避けて通れないのがタイミング攻撃です。PHPで文字列を比較するとき、通常の `==` や `===` 演算子は、一致しない文字を見つけた瞬間に比較を打ち切る(ショートサーキットする)ため、処理時間の差からパスワードの一部を推測される危険性があります。
必ずPHP組み込みの `hash_equals()` を使用してください。この関数は、入力の長さや内容に関わらず常に一定の時間で比較を行う(定数時間比較 / Constant-time comparison)ように実装されています。
// 【NGな例】タイミング攻撃に脆弱
// if ($userHash === $storedHash) { … }
// 【OKな例】サイドチャネル攻撃耐性を持つセキュアな比較
if (hash_equals($storedHash, $inputHash)) {
// 認証成功
}
—
4. プロダクション環境における極意とアーキテクトからの提言
PHPでFiberを用いた非同期・並行処理を本番環境(特にセキュアな暗号処理を伴うシステム)に導入する際、シニアエンジニアとして知っておくべき「現実の壁」と対策があります。
- PHPのネイティブ関数は基本「ブロッキング」である:
`password_hash()` や `openssl_encrypt()` などの重厚な暗号系関数は、内部のC拡張(Zend EngineやOpenSSLなど)で同期的に実行されます。つまり、PHPのユーザースペースでいくらFiberで綺麗にコードを書いても、Cの関数が実行されている間はそのスレッド(プロセス)のCPUコアはその処理に釘付けになります。
真の意味でブロッキングを回避しつつ並行化したい場合は、ext-uv や ReactPHP / Amp といった本格的なイベントループ拡張を活用し、I/Oバウンドな非同期処理と組み合わせるか、あるいは非常に重いCPUバウンド処理はWorkerプロセス(RoadRunnerやFrankenPHPなど)にオフロードするアーキテクチャ設計が不可欠です。
- メモリ安全性の担保:
Fiber間でグローバル変数や共有ステートを安易に使い回すと、予期せぬ競合状態(Race Condition)やデータの汚染を招きます。Fiberはあくまで「実行コンテキストの分離」であり、プロセス間共有メモリのような厳格な排他制御が自動で行われるわけではありません。状態は極力イミュータブル(不変)に保ちましょう。
—
まとめ
PHPのFiberは、単なるトレンドの機能ではありません。Zend VMの内部構造とメモリモデルを正しく理解すれば、モダンで洗練された非同期アプリケーションを構築するための強力な武器になります。
そして、Argon2idのような強力な暗号処理を扱うときこそ、システム全体のパフォーマンス(Fiberによる協調処理)と、セキュリティ(`hash_equals` によるサイドチャネル攻撃耐性)の両輪を完璧に噛み合わせる必要があります。
「PHPだからここまでしかできない」のではなく、「PHPのエンジン特性を知り尽くしているからこそ、ここまで美しくセキュアに書ける」。
そんなワンランク上のアーキテクトを目指して、ぜひ今日の知見を実際の設計やコードレビューに活かしてみてください。あなたの書くPHPコードの裏側が、より一層エレガントに輝くはずです。