Fil-C メモリ安全なコンテキストスイッチング

Fil-C メモリ安全なコンテキストスイッチング

Fil-Cは、setjmplongjmp、およびucontext API(getcontextsetcontextmakecontext、およびswapcontext)のメモリ安全な実装を提供します。これらのメカニズムにより、コンテキストスイッチングAPIの誤用がスタックの破損やFil-Cのケーパビリティモデルの違反につながることを防ぎ、特にダングリングスタックの復元を防止します。

メモリ安全な setjmp と longjmp

Fil-Cは、ジャンプバッファを不透明なオブジェクトとして扱い、厳格なスタックフレームの検証を強制することで、setjmplongjmpにおける一般的なメモリ安全性の落とし穴を防ぎます。

標準的な setjmp/longjmp の危険性

標準的なC言語では、setjmpはレジスタの状態(callee-saveレジスタ、スタックポインタ、および命令ポインタ)を保存しますが、スタック自体は保存しません。これにより、主に2つの安全上のリスクが生じます。

  1. ダングリングスタック: プログラムがsetjmpを呼び出し、その後その関数から戻った場合、保存されたコンテキストは無効になります。そのコンテキストへの後続のlongjmpは、もはや存在しないスタックフレームを復元しようと試みるため、不完全なマシン状態(torn machine state)を引き起こします。
  2. コンパイラ最適化エラー: setjmpは2回戻る可能性があるため、コンパイラはそれを呼び出す関数に対して特定の最適化(spill slotの再利用など)を無効にする必要があります。コンパイラが関数がsetjmpを呼び出していることを認識していない場合(例:関数ポインタ経由)、spill slotを再利用してしまい、longjmpの後に変数が誤った値やゴミの値を持つ原因となります。

Fil-C の実装戦略

Fil-Cは、以下のメカニズムを通じてこれらのリスクを軽減します。

  • 不透明なジャンプバッファ: jmp_bufは、Fil-Cランタイムのみが管理する不透明なzjmp_bufオブジェクトへのポインタを含んでいます。これにより、ユーザーがジャンプ状態を偽造したり手動で操作したりすることを防ぎます。
  • コンパイラによる強制: Fil-Cコンパイラ(filcc)は、setjmpが直接呼び出されることを要求します。呼び出しを難読化しようとする試み(例:関数ポインタの使用)は、コンパイラエラーまたは内部コンパイラエラー(ICE)となり、コンパイラのreturns_twiceロジックが常にトリガーされることを保証します。
  • スタックフレームの登録: setjmpが呼び出されると、zjmp_bufを割り当て、現在のスタックフレームに登録します。各フレームは、有効なzjmp_bufターゲットの弱い参照セットを保持します。
  • 先祖検証: longjmpは、現在のスタックフレームがzjmp_bufを作成したフレームの末裔である場合(つまり、ターゲットフレームが現在のコールスタック上の先祖である場合)を除き、パニックを起こします。これは、スタックを辿り、フレーム内の有効セットにzjmp_bufがあるかどうかを確認することで検証されます。
  • GC統合: zjmp_bufは、setjmp実行時のフレームのGCルートのコピーを保存します。zjmp_bufが生存している限り、GCはそれらのルートをマークし続けます。

メモリ安全な ucontext API

ucontext APIのサポートは、Fil-Cリリース0.680で導入されました。これらのAPIは、実行コンテキストの保存と復元を可能にすることで、ファイバーやコルーチンを実装するために使用されます。

ucontext の状態マシン

誤用を防ぐために、Fil-Cはzfiber_contextオブジェクトに対して制限された状態マシンを実装しています。

  • Uninitialized (未初期化): 初期状態。getcontextまたはswapcontextfrom引数としての使用のみが許可されます。
  • After_getcontext (getcontext後): getcontextが返った後の状態。makecontextまたはswapcontextfrom引数としての使用のみが許可されます。 n* Runnable (実行可能): makecontextまたはswapcontextfrom引数となった後の状態。setcontextまたはswapcontextto argument に使用することのみが許可されます。
  • Running (実行中): コンテキストが現在実行されている状態。swapcontextのみが許可され、かつそのコンテキストがスレッド内で現在実行中のものである場合に限ります。

安全性の制約と実装

  • スタック管理: Fil-Cは、ucontext_t内のユーザー提供のss_sp(スタックポインタ)を無視します。代わりに、ランタイムはmakecontext中に内部的に管理されたスタックを割り当て、スタックがFil-Cのオーバーフロー処理と互換性があることを保証します。
  • スレッド親和性: ABIの一貫性を維持するため、zfiber_contextは作成されたスレッドを追跡します。異なるスレッドからのコンテキストへのスイッチングを禁止することで、スタックがスレッド間を移動することを防ぎます。
  • 不透明な状態: jmp_bufと同様に、ucontext_tは不透明なzfiber_contextオブジェクトへのポインタを含み、内部的なマシン状態をユーザーから隠蔽します。

ガベージコレクションとファイバーの切り替え

ファイバーをオンザフライのガベージコレクタと統合するには、現在スレッドが所有していないスタックを注意深く追跡する必要があります。

Grey Fiber 問題

ファイバーがrunnable(実行中でない)状態のとき、そのスタックはGCによってスキャンされる必要があります。しかし、もしミューテータがファイバーに切り替え、GCのマークフェーズ中にファイバーから切り替えた場合、GCがすでにファイバーを「black」(処理済み)としてマークしていると、ファイバーのスタックを読み飛ばしてしまう可能性があります。

Fil-Cは、grey fibersを追跡することでこれを解決します。マークフェーズ中にswapcontextが呼び出されると、切り替え元(from)のコンテキストは、現在のスレッドのgrey_fibersリストに追加されます。GC終了前の最終的なスタック再スキャン時に、スレッドはリスト内のすべてのgrey fibersのスタックをウォークし、すべての生存オブジェクトを可視視化を確保します。

API サポートの要約

API Fil-C 安全性メカニズム 主なユースケース
setjmp / longjmp 先祖検証 & 不透明な zjmp_buf 例外処理、シグナルハンドラ
getcontext / makecontext 状態マシン & 管理されたスタック ファイバー/コルーチンのブートストラップ
swapcontext スレッド親和性 & Grey fiber GC 追跡 ファイバーのコンテキストスイッチング

Summary: Fil-Cは、スタックの破損やダングリングスタックの実行を防止するために、setjmp/longjmpおよびucontext APIのメモリ安全なバージョンを実装しており、これらのメカニズムニズムをそのケーパビリティモデルとガベージコレクタと統合しています。

Sources