flawopen.com/メモリ安全性/Rust unsafe
Rust の unsafe ブロックはメモリ安全なのか?
unsafe は Rust の検査を無効にしません。コンパイラが安全性を証明できない特定の操作を許可し、その条件を満たす責任をプログラマーに課します。そのため、健全でない unsafe 抽象化は、呼び出し側が unsafe を書かなくても、通常の安全なコードから未定義動作(UB)を引き起こす可能性があります。
重要な用語
- unsafe 操作:
- 生ポインターの参照外しなど、コンパイラが安全条件を証明できない操作。
- 健全な抽象化:
- 安全な呼び出し側が未定義動作を引き起こせない安全 API。
- size_hint():
- 最適化のためのイテレーターサイズの推定値。正確な件数でもメモリ安全性の証明でもありません。
- 未定義動作 (UB):
- Rust が動作を定義しないプログラムの振る舞い。unsafe コードは安全な呼び出し側がこれを起こせないようにする必要があります。
健全でない安全な抽象化が破綻する仕組み
安全 API がイテレーターを受け取る
呼び出し側は unsafe の事前条件がない関数に通常のイテレーターを渡します。
ヒントの値だけ容量を確保する
関数は報告された下限分だけを確保しますが、実際に生成される要素数はそれを上回る場合があります。
検査なしの書き込みが余剰容量を超える
実装は割り当てを拡張せず、要素ごとの容量確認もせずに、生ポインター経由ですべてを書き込みます。
安全な呼び出しが UB に至る
要素数が容量を超えると、安全な呼び出しでも範囲外書き込みが起きます。Vec::extend など安全に拡張する実装を使います。
// UNSOUND: reserve uses only the lower hint, // then unchecked writes consume every item. fn append_unsound(dst: &mut Vec<u8>, iter: impl Iterator<Item = u8>) { let (lower, _) = iter.size_hint(); dst.reserve(lower); let start = dst.len(); let mut written = 0; unsafe { let out = dst.as_mut_ptr().add(start); for item in iter { // UB once writes exceed spare capacity. out.add(written).write(item); written += 1; } dst.set_len(start + written); } }
// SOUND: Vec::extend handles capacity // growth and length updates safely. fn append(dst: &mut Vec<u8>, iter: impl Iterator<Item = u8>) { dst.extend(iter); }
Rust の保証が意味すること
正確な保証は次のとおりです。依存する unsafe コードが健全であれば、安全な Rust は未定義動作を引き起こしません。Rust Reference は境界の観点から soundness を定義し、安全なクライアントが UB を起こせないよう unsafe コードに求めています。コンパイラは unsafe 操作内のすべての証明義務を検証しません。プログラマーとレビュー担当者がその義務を満たす必要があります。
C や C++ と比べても、これは Rust の大きな実務上の利点です。Rust はメモリに関わる操作と安全性の義務を見えやすくし、レビュー担当者が unsafe 境界と、その上に構築された安全な抽象化に注目できるようにします。境界には unsafe ブロック、unsafe 関数の契約、unsafe trait の実装、FFI 宣言、unsafe 属性があります。必ずしも小さな一つのブロックに収まるとは限りません。実例として smallvec の CVE-2021-25900 では、SmallVec::insert_many が size hint の下限だけを使って確保し、割り当て容量を超える要素を書き込みました。CVSS は 9.8 です。
基準は慎重さではなく soundness
安全なコードから未定義動作を起こせる API は健全ではありません。たとえば Iterator::size_hint は最適化に使える境界値であり、正確な要素数ではありません。有効なイテレーターでも下限より多く生成することがあり、不具合のあるイテレーターは推定値に違反することがあります。標準ドキュメントは、このヒントを理由に unsafe コードの境界検査を省略してはならないと明記しています。安全な trait の誤動作は論理エラーや panic を起こす場合もありますが、それだけで UB になるわけではありません。ただし呼び出し側の予期しない動作があっても、unsafe コードはメモリ不変条件を保つ必要があります。
unsafe コードの影響は局所的とは限りません。ある箇所で確立したメモリ不変条件を、その上位の安全 API が前提として使う場合があります。不変条件が誤っていれば、契約どおりに使った安全な呼び出し側ではなく、安全 API を公開した抽象化に欠陥があります。
実践的なルール
- ✓安全な trait メソッドをメモリ安全性の証明とみなさない。
size_hint()は最適化にのみ使い、検査を維持するか必要に応じて容量を拡張します。 - ✓panic 時にも不変条件を維持する。
Iterator::next、デストラクター、クロージャーなど呼び出し側のコードはアンワインドを起こすことがあります。部分初期化状態が有効で、正しく破棄されるようにします。 - ✓
// SAFETY:コメントを書く。成立すべき条件と、その条件が満たされる理由を説明します。説明のない unsafe はレビューできません。 - ✓ブロックを小さく保つ。キーワードの繰り返しを避けるためにループ全体を囲むと、レビュー対象が広がります。
- ✓実行経路を通るテストで Miri を実行する。実行された経路における範囲外アクセス、解放後使用、不正なエイリアスなど一部の UB を検出できますが、健全性の証明にはなりません。
- ✓依存関係を監査する。
cargo auditは既知の RustSec アドバイザリーを確認し、cargo geigerは unsafe の使用状況把握に役立ちます。どちらも出荷するコードの unsafe 契約レビューの代わりにはなりません。
よくある質問
#![forbid(unsafe_code)] は指定した crate 内で unsafe 構文を禁止でき、アプリケーションで便利です。依存関係までは対象にせず、実装、FFI 境界、ライブラリーによっては unsafe 操作が必要です。
あります。unsafe trait は実装者が守るべき義務を文書化し、unsafe コードはその義務を前提にできます。TrustedLen は正確な size hint を約束するイテレーター向けの不安定な標準ライブラリー trait です。通常の Iterator::size_hint にその保証はありません。
いいえ。Rust の安全性モデルはメモリ安全性の義務が生じる範囲を狭め、unsafe 境界でそれを明確にします。unsafe コード、依存関係、FFI 契約、unsafe 実装に基づく安全 API の監査は引き続き必要です。
参考資料
- The Rust Reference — unsafe code and soundness
- Rust Reference — the unsafe keyword and proof obligations
- Rust standard library — Iterator::size_hint contract
- Rustonomicon — working with unsafe
- RustSec Advisory RUSTSEC-2021-0003 — smallvec buffer overflow
- GitHub Security Advisory GHSA-43w2-9j62-hq99 — CVE-2021-25900
- The Rustonomicon — the guide to unsafe Rust
- RustSec Advisory Database
- Miri — an interpreter that detects undefined behaviour
- CWE-787 — Out-of-bounds Write
- Case study: the smallvec overflow (CVE-2021-25900)