未定義とみなされる動作
Rust コードは以下のリスト内のいずれかの動作を発現すると不正となる。これはアンセーフブロックとアンセーフ関数内にあるコードに含まれる。アンセーフは未定義動作を回避するのがプログラマである事を意味するだけだ; Rust プログラムが未定義動作を決して発生させてはならないという事実を何ら変えるものではない。
unsafeコードの記述時に、任意のセーフコードがそのunsafeコードと相互作用してこれらの動作を誘発できないようにするのはプログラマの責任である。任意のセーフなクライアントに対してこの特性を満たすunsafeコードは健全であると呼ばれる; もしもunsafeコードがセーフコードにより誤用されうるなら、それは不健全である。⚠︎ 警告
以下のリストは網羅的ではない; 増減がありうる。何が許され何が許されないかについての Rust の意味論の正式なモデルはないため、未定義とみなされる動作はもっとあるかもしれない。また私達はそのリストのいくつかを将来的に定義済とする権利も留保する。言い換えると、このリストは全ての将来の Rust のバージョンで何かが確実に常に未定義だとは言っていない (ただし将来的にリストのいくつかの項目について、そのような約束をする可能性はある)。
アンセーフコードを書く前に Rustonomicon を読んでほしい。
データ競合。
ダングリングになっているかミスアライメントされたポインタ基準の場所への (ロードやストア) アクセス。
境界内でのポインタ計算の要件に違反する場所についての射影の実行。ここで射影とはフィールド式、タプル添字式、または配列/スライス添字式である。
ポインタのエイリアシングルールの破壊。正確なエイリアシングルールはまだ決まっていないが、ここは一般原則の外形をあげる:
&Tはそれが生存している限りは、変更されないメモリをポイントしなければならない (UnsafeCell<U>の内側のデータは例外)。そして&mut Tはそれが生存している限りは、その参照から派生されていない任意のポインタにより読書されず他の参照がポイントしないメモリをポイントしなければならない。Box<T>はこれらの規則においては&'static mut Tと似たように扱われる。正確な生存期間は指定されないが、幾つかの境界は存在する:
- 参照では、生存期間は借用チェッカーによりあてがわれた構文上のライフタイムにより上限が決まる; そのライフタイム以上には生きられない。
- 参照やボックスは逆参照または再借用されるたび、生存中と見なされる。
- 参照やボックスは関数へと渡されるか関数から戻されるたび、生存中と見なされる。
- 参照 (
Boxは異なる!) が関数に渡される時、それは最低でもその関数の呼出中は生存中となる。繰り返しだが&TがUnsafeCell<U>を含むと例外になる。これらは全てこれらの型の値が複合型の (入れ子の) フィールドに渡された時にも適用されるが、ポインタによる間接処理の元ではそうでない。
不変なバイト列の改変。不変昇格された式を経由して到達できる全てのバイト列は不変である。また、
'staticへライフタイム拡張されたstaticとconst初期化子内の借用を経由して到達できるバイト列も同様である。不変束縛や不変staticによって所有されるバイト列は、それらがUnsafeCell<U>の一部でない限り、不変である。さらに言えば、共有参照によりポイントされるバイト列は、他の参照 (共有と可変の両方) や
Boxを推移的に経由するものも含めて、不変である; ここで推移性には複合型のフィールドに保存された参照も含める。改変とは関連するバイト列のどこかと重複する 0 バイトよりも大きい任意の書込である (たとえその書込がメモリの内容を変更しなくても)。
コンパイラの内在機能による未定義動作の起動。
現在のプラットフォームがサポートしないプラットフォームの機能と共にコンパイルされたコードの実行 (
target_featureを参照)、ただしプラットフォームがこれを明示的に文書化している場合を除く。間違った呼出 ABI での関数の呼出、または巻き戻しが許可されないスタックフレームを超えての巻き戻し (例えば、"C" の関数や関数ポインタとしてインポートもしくは変換された "C-unwind" つき関数の呼出によって)。
不正な値の生成。値の “生成” は値が場所へと代入されるか場所から読取されるか、関数/プリミティブ操作へと渡されるか関数/プリミティブ操作から戻されると、常に発生する。
インラインアセンブリの不正な使用。より詳しくは、インラインアセンブリを使用するコード記述時のルールを参照せよ。
定数文脈において: ポインタ (参照、生ポインタ、または関数ポインタ) を非ポインタ型 (整数など) である何らかの割り当てへと変換または別の方法で再解釈。‘再解釈’ はキャストなしでポインタの値を整数でロードする事を指し、例えば生ポインタのキャストや共用体を使って行える。
Rust ランタイムの仮定への違反。殆どの Rust ランタイムの仮定は現在は明示的に文書化されていない。
- 特に巻き戻しに関する仮定については、パニックについての文書を見よ。
- ランタイムは Rust のスタックフレームはそのスタックフレームにより所有されるローカル変数のデストラクタの実行なしには割り当てを解除されないと仮定している。この仮定は
longjmpのような C の関数により侵害できる。ⓘ 参考
未定義動作はプログラム全体に影響する。例えば、C での未定義動作を発現する C の関数の呼出はあなたのプログラム全体が未定義動作を含む事を意味し、それは Rust コードにも影響しうる。そして逆も同様、Rust での未定義動作は他へのあらゆる FFI 呼出により実行されるコードの悪影響の原因になりうる。
ポイントされるバイト列
ポインタや参照が “ポイントする” バイト列の区間はポインタの値とポイントされる型のサイズにより決定される (
size_of_valの使用)。ミスアライメントされたポインタ基準の場所
場所はもし場所計算中の最後の
*射影がその型にアライメントされていないポインタ上で実行されると、“ミスアライメントされたポインタ基準” であると言われる。(もし場所式の中に*射影がなければ、これはローカルか静的なフィールドへのアクセスで rustc は正しいアライメントを生成する。もし複数の*射影があれば、それらはそれぞれメモリからそれ自身を逆参照するためのポインタのロードを招き、これらのそれぞれのロードがアライメント制約の対象となる。表面的な Rust の構文では自動逆参照があるため特定の*射影は省略できる点に注意せよ; 私達はここでは完全に展開された場所式について考えている。)実例として、もし
ptrが*const Sの型を持ちSのアライメントが 8 なら、ptrはアライメント 8 でなければならず、そうでなければ(*ptr).fは “ミスアライメントされたポインタ基準” になる。これはたとえフィールドfの型がu8(つまりアライメント 1 の型) でも真実である。言い換えると、アライメントの要件はポインタが逆参照された型から導出され、アクセスされるフィールドの型からではない。ミスアライメントされたポインタ基準の場所はそれがロードまたはストアされた時にのみ未定義動作につながる点に注意せよ。
&raw const/&raw mutはそのような場所でも使える。
&/&mutにおける場所はフィールド型のアライメントが要求され (そうでなければプログラムは “不正な値の生成” になるだろう)、これはアライメントされたポインタに基づく場合よりは一般に小さい制限の要求である。フィールドの型がそれを含む型よりも大きなアライメントを持つかもしれない場合 (つまり、
repr(packed))、参照をとる事はコンパイルエラーにつながる。これはつまりアライメントされたポインタを基準にすると常に十分に新しい参照のアライメントを保証できるが、必ずしもそれが必要ではない事を意味している。ダングリングポインタ
参照/ポインタはもしそれがポイントするバイト列の全てが同一の生きた割り当ての一部でないと “ダングリング” になる (つまり具体的にはそれらは全て特定の割り当ての一部でなければならない)。
もしサイズが 0 なら、そのポインタは自明だが “ダングリング” は決してしない (たとえそれがヌルポインタでも)。
動的サイズ型 (スライスや文字列のような) はそれらの範囲全体をポイントするため、長さのメタデータが極端に大きくないのも重要である。
具体的には、Rust の値の動的サイズ (
size_of_valにより決定される) は一回の割り当てがisize::MAXより大きくなるのが不可能なため、決してisize::MAXを超えてはならない。不正な値
Rust のコンパイラはプログラムの実行中に生成される全ての値が “妥当” であり、そして不正な値はそのためすぐに UB になると仮定する。
値が妥当かは型に依存する:
boolの値はfalse(0) かtrue(1) でなければならない。
fnポインタの値は非ヌルでなければならない。
charの値はサロゲートであってはならず (つまり、範囲0xD800..=0xDFFFにあってはならない)、char::MAX以下でなければならない。
!の値は決して存在してはならない。整数 (
i*/u*)、浮動小数点数値 (f*)、または生ポインタは初期化されていなければならない、つまり未初期化のメモリから獲得してはいけない。
str値は[u8]のように扱う、つまり初期化されていなければならない。
enumは妥当な識別子を持たなければならず、その識別子によって示される変異形の全てのフィールドがそれらそれぞれの型で妥当でなければならない。
struct、タプル、そして配列は全てのフィールド/要素がそれらのそれぞれの型で妥当であるよう要求する。
unionでは、正確な妥当性の要求はまだ決定されていない。当然、セーフコードで作成される全ての値は妥当である。もし共用体がゼロサイズのフィールドを持つなら、全ての可能な値は妥当である。さらなる詳細はまだ議論中である。参照や
Box<T>はアライメントされた非ヌルで、ダングリングしてはならず、かつ妥当な値をポイントしなければならない (動的サイズ型では、メタデータにより決まるポイントされる実際の動的型が使われる)。最後の点 (妥当な値のポイントについて) は幾つか議論になる話題が残っている事に注意せよ。ファット参照、
Box<T>、または生ポインタのメタデータはサイズ指定されない末尾の型に適合しなければならない:
dyn Traitのメタデータはコンパイラが生成したTraitのための vtable でなければならない (生ポインタではこの要件は幾つか議論になる話題が残っている)。- スライス (
[T]) のメタデータは妥当なusizeでなければならない。さらに言うと、ファット参照とBox<T>、スライスのメタデータは、もしそれがポイントされる値の合計サイズをisize::MAXより大きくするようなら不正となる。もし型が妥当な値について独自の範囲を持つなら、妥当な値はその範囲内になければならない。標準ライブラリの中では、これは
NonNull<T>andNonZero<T>に影響する。ⓘ 参考
rustcはこれを不安定なrustc_layout_scalar_valid_range_*属性で達成する。参考: 未初期化のメモリは制限された有効な値の集合を持つ任意の型において暗黙的に不正である。言い換えると、未初期化メモリの読取が許されるのは
unionの内側と “パディング” (型のフィールド間にある隙間) の中のみである。