FlawOpen

flawopen.com/メモリ安全性/Rust unsafe

Rust の unsafe ブロックはメモリ安全なのか?

CWE-787: 範囲外書き込みリファレンスページ
要点

unsafe は Rust の検査を無効にしません。コンパイラが安全性を証明できない特定の操作を許可し、その条件を満たす責任をプログラマーに課します。そのため、健全でない unsafe 抽象化は、呼び出し側が unsafe を書かなくても、通常の安全なコードから未定義動作(UB)を引き起こす可能性があります。

重要な用語

unsafe 操作:
生ポインターの参照外しなど、コンパイラが安全条件を証明できない操作。
健全な抽象化:
安全な呼び出し側が未定義動作を引き起こせない安全 API。
size_hint():
最適化のためのイテレーターサイズの推定値。正確な件数でもメモリ安全性の証明でもありません。
未定義動作 (UB):
Rust が動作を定義しないプログラムの振る舞い。unsafe コードは安全な呼び出し側がこれを起こせないようにする必要があります。

健全でない安全な抽象化が破綻する仕組み

ステップ 1

安全 API がイテレーターを受け取る

呼び出し側は unsafe の事前条件がない関数に通常のイテレーターを渡します。

ステップ 2

ヒントの値だけ容量を確保する

関数は報告された下限分だけを確保しますが、実際に生成される要素数はそれを上回る場合があります。

ステップ 3

検査なしの書き込みが余剰容量を超える

実装は割り当てを拡張せず、要素ごとの容量確認もせずに、生ポインター経由ですべてを書き込みます。

ステップ 4

安全な呼び出しが 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 を公開した抽象化に欠陥があります。

実践的なルール

よくある質問

unsafe を完全に禁止できますか?

#![forbid(unsafe_code)] は指定した crate 内で unsafe 構文を禁止でき、アプリケーションで便利です。依存関係までは対象にせず、実装、FFI 境界、ライブラリーによっては unsafe 操作が必要です。

unsafe コードが trait を信頼できる方法はありますか?

あります。unsafe trait は実装者が守るべき義務を文書化し、unsafe コードはその義務を前提にできます。TrustedLen は正確な size hint を約束するイテレーター向けの不安定な標準ライブラリー trait です。通常の Iterator::size_hint にその保証はありません。

これは Rust の利点を損ないますか?

いいえ。Rust の安全性モデルはメモリ安全性の義務が生じる範囲を狭め、unsafe 境界でそれを明確にします。unsafe コード、依存関係、FFI 契約、unsafe 実装に基づく安全 API の監査は引き続き必要です。

参考資料

関連項目 strncpy() は strcpy() より安全ですか? CWE と CVE:何が違うのか Mass Assignment:リスクと安全なフィールド許可リスト Serverless の状態漏えい:ユーザーデータをリクエストごとに分離 PHP == と Magic Hash の比較