はじめに
こんにちは。SRE チームの吉原 哲です。
RubyKaigi 2025 に参加してきました。中でも特別興味のあるセッションがありました。Maciej Mensfeld (@maciejmensfeld) 氏による Bringing Linux pidfd to Ruby です。彼は Karafka の作者で、以前社内の Karafka をアップグレードしたときお世話になった方です。そのときの話はこのブログエントリ (Kafka と Karafka を無事故・無停止でアップグレードした話) をご参照ください。今回 RubyKaigi へ参加するにあたって是非ともお話を聞きたいと思っていました。
本エントリでは pidfd
とは何なのか事前調査した内容と、発表の内容を織り交ぜながら解説します。
pidfd とは
Linux 5.3 で導入された pidfd (PID File Descriptor) は、プロセスを File Descriptor として扱う仕組みです。一度取得した pidfd はそのプロセスが死ぬまで決して他プロセスを指さないため、PID ベースでプロセス管理をする際の様々な問題を解決することができます。
PID でプロセス管理をする問題
以下にいくつか問題点をあげてみます。
シグナルレース
子プロセスが終了すると SIGCHILD
が送られますが、誰でも SIGCHILD
のハンドラを設定できます。
waitpid
を呼んだ最初のハンドラが勝利し、終了コードはそのとき一回だけしか取得できません。
誤 kill 問題
Unix 系 OS では長年、プロセスを PID (ただの整数) で識別してきました。しかし PID は有限なので、プロセスが終了するといずれ再利用されます。この再利用とkill pid
を投げるまでの僅かな遅延が重なると、別プロセスを誤って kill してしまう問題が存在します。
GDB のビルドボットテストにおいては、断続的に謎の FAIL が起きていました。原因は、バックグラウンドで送った kill
がたまたま再利用された PID に命中していたことでした。著者は sporadically kills the wrong process と記しており、高頻度で fork/exit を繰り返す環境では問題の顕在化が確認されました。
こうした事例は頻繁に起きるわけではありません。再利用される PID のタイミングが偶然一致したときだけ発生するため、再現するには大量の短命プロセスや CI の並列テストなどの限定的な環境が必要です。
セキュリティ
セキュリティに関する問題も存在します。例えば、CVE-2019-6133 は PolicyKit(polkit)0.115 に存在したローカル権限昇格の脆弱性です。
この脆弱性は、polkit が「プロセス開始時刻」を使って一時的な認可キャッシュを識別する仕組みに起因します。fork() はアトミックではないため、以下の手順で攻撃が可能です。
- 親プロセスが認可を得た直後に終了
- 同じ PID が即座に再利用される
- 攻撃側プロセスがその PID で起動
この PID 再利用レースにより、攻撃側が親プロセスの認可を「横取り」できてしまいました。
管理コスト
PID ベースのポーリングでは常に子プロセスを監視しなければならない手間に加えて、CPU も無駄にしてしまいます。モニタリングのインターバルにより、プロセスが死んだこと検知するのに遅延があります。
プロセスツリーの制限
プロセスツリーを追うビルトインの機能は提供されていません。孤児プロセスになった場合は init
(PID 1) に再配置されてしまいます。また子プロセスでなければ直接監視する方法はありません。
pidfd の利点
プロセス識別子の一意性が保証され、誤って他のプロセスにシグナルを送信するリスクを完全に排除することができます。
また File Descriptor として扱えるため、select/poll/epoll で polling が可能です。
pidfd の制限
Linux Kernel 5.3 以降でのみ利用可能です。macOS や Windows では利用できません。
Ruby では言語としてサポートされていないので、FFI を利用して syscall をマップする必要があります。
PID と同じく PID namespace の制限を受けます。
PID で可能だったプロセスグループ単位での操作はできません。
Ruby における pidfd
Ruby においては io-event
がこれまでのところ pidfd を使っている唯一のライブラリです。Puma
や Unicorn
、Sidekiq
、Pitchfork
などはこれまで通りの fork
で管理しています。
今回の発表で紹介された Karafka Swarm における FFI を利用した pidfd 実装はわずか 150 行程度の Ruby コードで実現されています。このモジュールでは、#supported?
によりサポートされている platform かどうかで判別されて pidfd を利用するか出し分けを行っています。
Karafka Swarm ではプロセスの生死監視はもちろん、pidfd によるプロセスへの確実なシグナル送信が行えます。またゾンビプロセスのクリーンアップも行い、スレッドセーフで動作します。
これに加えて興味深いのが、子プロセスが親の pidfd を持つことにより双方向でプロセスの状態を監視することができるようになっています。子プロセスが親のプロセスの状態を見ることにより、自身が孤児プロセス (Orphan Process) になっているか判別できます。Karafka のワーカープロセスでは自身が孤児プロセスになった場合は、自動で自身を終了させるようになっています。
このモジュールの詳細の実装は Maciej Mensfeld 氏のプレゼンテーション資料にて解説されています。ご興味がある方はご参照ください。
まとめ
これまでの PID によるプロセス管理と違い、pidfd には魅力的な機能を持っていることがわかりました。特に大量の短命なプロセスを扱っている環境の場合、誤った対象にシグナルを送る事故を完全に防ぐことができます。この他にも
- アプリケーションを様々なプロセスが動いている環境で動かす必要がある
- 直接の子プロセス以外を管理したい
- 誤ったプロセスに対して PID が再利用されたときにセキュリティ上の問題が起きる場合
などといったケースでは検討するに値するでしょう。
また常駐サービスのスーパーバイザにおいても、ワーカープロセスを安全に管理できるので pidfd の有効性はあると考えられます。
とはいえ多くのケースではオーバーエンジニアリングになることは否めません。実際にプレゼンテーション中でも
In most cases, pidfd is overkill — even in Karafka
と言っており、日常的なスクリプトや数プロセス程度を管理するのであれば、レースに遭遇する確率は極めて小さく、従来の PID での管理で十分だと思われます。
参考
- RubyKaigi 2025 における Maciej Mensfeld 氏のプレゼンテーション資料 https://mensfeld.github.io/bringing_linux_pidfd_to_ruby/
- Karafka Swarm の pidfd 実装 https://github.com/karafka/karafka/blob/master/lib/karafka/swarm/pidfd.rb
- GDB のビルドボットテストにおける誤 kill 問題 https://palves.net/a-tale-of-inexplicable-gdb-racy-fails/
- Ruby Issue#19322: Support spawning "private" child processes https://redmine.ruby-lang.org/issues/19322
We are hiring!
ラクスルでは成長するビジネスを支える SRE を募集しています。 https://hrmos.co/pages/raksul/jobs/rksl866q_2