PaPoo
cover
technews
Author
technews
世界の技術ニュースをリアルタイムでキャッチし、日本語でわかりやすく発信。AI・半導体・スタートアップから規制動向まで、グローバルテックシーンの「今」をお届けします。

unsafe を書かなくても UB に届く——Rust 標準ライブラリを壊した「3つの信頼」

この記事は後半です。前半は Qiita にあります: unsafe を書かないのに壊れる Rust 標準ライブラリ

前半では、Rust 標準ライブラリの安全 API が、内部の unsafe を通じて UB に触れうること、そしてその崩れ方に「panic を信じる」「利用者の実装を信じる」「システムを信じる」の3型があることを見ました。ここから先で大事なのは、どれも抽象論ではなく、標準ライブラリの実装で本当に壊れていたという事実です。

画像: github.com より
画像: github.com より

まず panic safety です。Rustonomicon が強調している通り、unsafe コードが一時的に不整合な状態を作るなら、そのあいだに panic が来ても memory safety を壊してはいけません。MapWindows::clone の例はまさにそれで、Buffer を先に作ってから中身を埋める途中に、T::clone が panic すると、未初期化のまま destructor が走ってしまいます。見た目はただの clone() でも、内部では「まだ完成していない構造体」を落としている。修正は、完成するまで ManuallyDrop に入れることでした。要するに、壊れた状態を「壊れたまま落ちても安全」にするか、壊れる前に外部コードを呼ばないかのどちらかです。

この話は Vec のような典型例だけに限りません。clone、比較、Drop、iterator、closure など、呼び出し側が制御できないものは全部「いつ panic してもおかしくない」と見なす必要がある。unsafe の難しさは、低レベル命令そのものより、そうした呼び出しの間に何を許すかの設計にあります。

trait は便利だが、期待どおりには動かない

画像: oxide.computer より
画像: oxide.computer より

次の落とし穴は、利用者が与える実装です。AsRef<[u8]> のような trait は、型としては安全に見えますが、呼び出し側が暗黙に期待する性質までは保証しません。FORGE が示した get<S: AsRef<[u8]>> の小さな例では、最初に長さを確認しても、2回目の as_ref() が別のスライスを返せば境界チェックは崩れます。つまり、安全な関数の中で、実は「同じ入力なら同じ出力が返るはずだ」と信じていたわけです。

この種のバグは、コードレビューで見つけにくい。AsRef は普通なら単純に見えるし、1回呼ぶだけなら問題が出ないことも多い。だから、実装のどこが危ないかというより、「その呼び出しを何回するか」「その間に何を信じるか」が本体になります。Oxide の iddqd が、借りたキーと値の整合性を崩さないために設計全体で工夫しているのも、同じ種類の難しさです。拡張性のある trait は便利ですが、その分だけ安全側の前提が増えます。

OS に任せると、境界は急に荒くなる

3つ目は、システム境界です。std は Unix のシグナルや環境変数、プロセス終了を隠蔽してくれますが、その裏でグローバル状態を書き換えることがあります。GitHub の issue #161018 では、子スレッドから process::exit を呼ぶと、メインスレッドの sigalt stack が unmap されるケースが報告されました。メインスレッドはまだ生きているのに、シグナル用の代替スタックだけが消える。そこにシグナルが来れば、Rust の安全なコードであっても、生の segfault に落ちます。

同じく issue #156951 では、Unix の CommandExt::exec が一時的にグローバルな環境変数ポインタを書き換えるのに、Rust 側では読み取りロックしか持っていないことが問題でした。すると別スレッドから環境変数が見えてしまうし、競合した exec が互いの設定を取り違えることもある。失敗時の復元まで含めると、解放済みの一時領域を指し続ける危険もあります。ここでは「ロックしているから安全」という感覚が、そのまま通りません。

この2件が示すのは、Rust の安全性が言語単体の性質では終わらないことです。OS の仕様、スレッドのタイミング、シグナルの到着順まで含めて考えないと、標準ライブラリの安全 API でも穴が開く。しかもその穴は、利用者が unsafe を一切書かなくても踏めます。

自分なら、どこで警戒線を引くか

この種の問題に対して、私なら標準ライブラリを「安全だから信じる」のではなく、「どの前提までなら信じてよいか」を分けて見ます。呼び出し先が panic しうるなら、状態遷移の途中で外部コードを呼ばない。trait 実装をまたぐなら、呼び出し回数や順序に依存しない設計に寄せる。OS に触るなら、グローバル状態の変更を最小にして、別スレッドやシグナルが割り込んでも壊れないかを先に疑う。

FORGE の報告が示した価値は、Rust でも「安全 API の外側にある信頼」が壊れると UB に届く、と具体的に示したことです。しかも検出に AI 支援のスキャナが効き始めた。標準ライブラリは今後も増築され続けるので、こうした境界の監視は一回きりでは終わりません。安全性は、unsafe を書かないことではなく、信じてよい前提を狭く保ち続けることです。

同じ著者の記事