kubo39's blog

ただの雑記です。

プロセス起動でposix_spawnとかvforkとかを使うはなし

posix_spawn/vfork/clone(CLONE_VFORK) はメモリを共有するので速い. 速いが、親子でメモリを共有するので危険である. それでも速いのでプロセス起動で気をつけて使っている言語があり、どういう実装をしているか調べた.

前提

Rust

posix_spawnを使う条件

  • ENOENTを直接返すこと
    • 単にposix_spawn側の実装バグの問題
  • Linux/MacOS/FreeBSDであること
    • ENOENTを直接返すことができるOSのため
  • Linuxの場合, Glibcのバージョンが2.24以上であること
    • ENOENTを返すposix_spawnはこのバージョンから
  • getcwdでディレクトリを返すこと
  • getgidが成功する
  • getuidが成功する
  • PATHが変更されていないこと
    • race conditionがある

どう使っているか

  • posix_spawnに持たせる属性を決めている
  • posix_spawnattr_setsigmask シグナルをマスクしている
    • forkするスレッドはfork-exec間でシグナルを受け付けたくない
  • posix_spawnattr_setsigdefault SIGPIPEをSIG_DFLにしている
    • シグナルハンドラでグローバル変数を参照するかもしれないので避けたい
    • 別スレッドがシグナルをうけたときにメモリを書き換えるかも、という危険があるので
    • (追記): そういえばRustはランタイム初期化時にSIGPIPEをSIG_IGNにセットしているので、その設定によって起動するプロセスが影響を受けることを避けたいためにSIG_DFLにしなおしているというほうがメインな気がする
  • (追記): glibc2.29+だとchdirも指定できるようになっている

まとめ

Dellの中の人でFreeBSDのコアコミッタの人なので、けっこう信頼できるんじゃないだろうか. ただいくつか気になる点はあり、

  • すべてのシグナルに対してSIG_DFLを定義しなおさなくてもいいのか
    • してたわ
  • uid/gidが特権ユーザでないことはチェックしなくてよいのか
    • これはposix_spawn側で対策されていそうな雰囲気がある
  • PATHの変更をみていること
    • PATHは状態変わることで振るまいは変わるが具体的にどういった問題があるんだろう
    • race conditionがある

(追記) あとでいろいろ調べ直したけどめちゃくちゃよく考えられてる実装だった。

Go

clone(CLONE_VM | CLONE_VFORK)を使う条件

どう使っているか

  • clone(2)に SIGCHLD|CLONE_VFORK|CLONE_VM を指定して使っている
  • cloneでシステムコール呼び出しをしている場合, 親子はスタックを共有しないらしい
    • 引数にmmap()で確保したメモリを渡すことで制御できるようだ

fork-exec間に子プロセスは何をしているか

  • keep capabilitiesをprctlでセットしている(実行ユーザとかが変更されたかチェック)
  • ほかにもいろいろやっているがforkと共通なので割愛
    • 主にcapabilitiesとかなので権限まわりをセキュアにしたいのだろう

まとめ

自前でがんばっているためコードはかなり複雑になる. 現時点では制限がかなり厳しい(Linuxかつamd64のみ)ので、逆に意図しない問題は起きづらそう. 当然ゴルーチンが全部停止するためスループットが悪化するという懸念はされている.

Ruby

vforkを使う条件

  • vforkを実装していること
  • uid/gidが特権ユーザでないこと
    • setuidとかされたくない

fork-exec間に子プロセスは何をしているか

  • シグナルに対してSIG_DFLを再定義
    • invalidなsignumは無視
    • SIG_IGN が定義されてたら SIG_IGN
      • その場合でもSIGPIPEはSIG_DFLにする
  • sigprocmaskでシグナルをmaskする
    • pthread_sigmaskでないのはasync signal safeなものを使いたいため
  • pthread_setcancelstatePTHREAD_CANCEL_DISABLE して cancel が無効であることを宣言する
    • pthread_cleanup_push で登録した cancellation cleanup handler も呼び出されるのは困る
    • シグナルハンドラと同様の理由

まとめ

だいぶ安全よりに倒しているのではないか.

Nim

posix_spawnを使う条件

  • posix_spawnが存在していてLinuxでないこと
    • Linuxだとclone(2)を使う
    • いろいろよくわからない条件指定がある
    • Rustと違ってglibcのバージョンみないので存在しないコマンドにたいしてENOENTが返ってこない可能性がある

cloneを使う条件

  • Linuxだったら基本使う
    • fork-exec間はasync-signal-safeな関数しか使わないようになっている

どう使っているか

  • 基本Rustと一緒だけどシグナルまわりが怪しい
    • 具体的にいうとSIGPIPEをSIG_DFLにセットしていないとかが違う
    • 追記:これはRustがランタイム起動時にSIGPIPEをSIG_IGNにセットする特殊事情なので特におかしいことではない
  • clone()を使っているときに親がchdirしてからclone()を呼び、また親のディレクトリを戻している?
    • chdirしてからcloneが返ってくるまで親のカレントディレクトリが変わるというわりとめちゃくちゃな実装

まとめ

基本的に優良な実装にみえる. ただしいろいろ不明すぎて安全に使えるのか不安. シグナルまわりとか.

clone()を使っていてGoっぽいけどカレントディレクトリ指定とかやたら怪しい実装になっている