yohhoyの日記

技術的メモをしていきたい日記

危険な型変換:&T→&mut T

プログラミング言語Rustにおける、&Tから&mut Tへの型変換*1と同オブジェクトに対する変更操作は、未定義動作(undefined behavior)とされている。これは単にRustコンパイラのBorrow Checkerを騙すだけでなく、該当プログラムの動作が不定となり深刻な障害要因になり得る。*2

下記プログラムをrustc 1.15.1でコンパイル&実行すると、Debugビルドではおそらく期待通り動作するが、Releaseビルドでは★のassert!マクロより実行時エラーが報告される*3。なお明示的な#[allow(mutable_transmutes)]属性を削除すれば、コンパイル時lint検査エラー*4として危険なコードを検出できる。

// &i32 → &mut i32型変換と代入
#[allow(mutable_transmutes)]
#[inline(never)]
fn danger(p: &i32) {
  unsafe {
    *std::mem::transmute::<&_, &mut _>(p) = 0  // NG: 未定義動作
  }
}

#[allow(unused_mut)]
fn main() {
  let mut x = 1;
  danger(&x);
  assert!(x == 0);  // ★
}

イミュータブル参照型(&T)からミュータブル生ポインタ型(*mut T)への型キャスト*5はlint検査を通ってしまうが、同様に未定義動作を引き起こす。

// &i32 → *mut i32型変換と代入
#[inline(never)]
fn danger(p: &i32) {
  unsafe {
    *(p as *const _ as *mut _) = 0  // NG: 未定義動作
  }
}

イミュータビリティとUnsafeCell

Rust言語におけるイミュータビリティ(Immutability; 不変性)のセマンティクスは、変数束縛に対して “値の変更を行えない” という制約に加え、変数束縛の “値が変更されることは無い” という保証の2つの側面を持つ。前掲コードでは関数dangerにイミュータブル参照(&i32)しか渡しておらず、セマンティクス上はassert!マクロ到達までに変数xの値が変わることが無いと判断できる。Rustコンパイラにはこの判断に基づいた最適化を行う自由度があるため、特にReleaseビルドで問題が顕在化しやすい。

"Interior Mutability" を安全に実現するために、通常のイミュータビリティ・セマンティクスを無視するような特別扱いが必要となる。この目的のために std::cell::UnsafeCell が提供されており、Rustコンパイラでは同構造体を特別扱いする。標準ライブラリstd::cell::RefCellstd::sync::Mutexでは、その内部実装としてUnsafeCellを利用している。同APIリファレンスより引用(下線部は強調)。

The core primitive for interior mutability in Rust.

UnsafeCell<T> is a type that wraps some T and indicates unsafe interior operations on the wrapped type. Types with an UnsafeCell<T> field are considered to have an 'unsafe interior'. The UnsafeCell<T> type is the only legal way to obtain aliasable data that is considered mutable. In general, transmuting an &T type into an &mut T is considered undefined behavior.

The compiler makes optimizations based on the knowledge that &T is not mutably aliased or mutated, and that &mut T is unique. When building abstractions like Cell, RefCell, Mutex, etc, you need to turn these optimizations off. UnsafeCell is the only legal way to do this. When UnsafeCell<T> is immutably aliased, it is still safe to obtain a mutable reference to its interior and/or to mutate it. However, it is up to the abstraction designer to ensure that no two mutable references obtained this way are active at the same time, and that there are no active mutable references or mutations when an immutable reference is obtained from the cell. This is often done via runtime checks.

Note that while mutating or mutably aliasing the contents of an & UnsafeCell<T> is okay (provided you enforce the invariants some other way); it is still undefined behavior to have multiple &mut UnsafeCell<T> aliases.

Types like Cell<T> and RefCell<T> use this type to wrap their internal data.

The Rustonomicon, The Rust Referenceより該当箇所を一部引用。("UB"=Undefined Behavior)

mem::transmute<T, U> takes a value of type T and reinterprets it to have type U. The only restriction is that the T and U are verified to have the same size. The ways to cause Undefined Behavior with this are mind boggling.

  • (snip)
  • Transmuting an & to &mut is UB
    • Transmuting an & to &mut is always UB
    • No you can't do it
    • No you're not special
  • (snip)
https://doc.rust-lang.org/nomicon/transmutes.html

The following is a list of behavior which is forbidden in all Rust code, including within unsafe blocks and unsafe functions. Type checking provides the guarantee that these issues are never caused by safe code.

  • (snip)
  • &mut T and &T follow LLVM's scoped noalias model, except if the &T contains an UnsafeCell<T>. Unsafe code must not violate these aliasing guarantees.
  • Mutating non-mutable data (that is, data reached through a shared reference or data owned by a let binding), unless that data is contained within an UnsafeCell<T>.
  • (snip)
https://doc.rust-lang.org/reference.html#behavior-considered-undefined

LLVM Language Reference Manual, Parameter Attributes より noalias に関する説明を引用。

This indicates that objects accessed via pointer values based on the argument or return value are not also accessed, during the execution of the function, via pointer values not based on the argument or return value. The attribute on a return value also has additional semantics described below. The caller shares the responsibility with the callee for ensuring that these requirements are met. For further details, please see the discussion of the NoAlias response in alias analysis.

Note that this definition of noalias is intentionally similar to the definition of restrict in C99 for function arguments.

For function return values, C99's restrict is not meaningful, while LLVM's noalias is. Furthermore, the semantics of the noalias attribute on return values are stronger than the semantics of the attribute when used on function arguments. On function return values, the noalias attribute indicates that the function acts like a system memory allocation function, returning a pointer to allocated storage disjoint from the storage for any other object accessible to the caller.

http://llvm.org/docs/LangRef.html#noalias

関連URL:

*1:Rust型システムにおいて同型変換は安全でない(unsafe)ため、明示的に unsafe キーワードを指定する必要がある。

*2:Rustにおける未定義動作は、プログラミング言語C/C++における同用語と同じ意味を持つ。つまり未定義動作のプログラムに対してはいかなる保障も存在せず、何が起きても不思議ではない。

*3:#[inline(never)] 属性を削除すればReleaseビルドでも期待通り動作するようになるが、本質的に未定義動作のため何ら解決していないことに注意。より複雑な関数呼び出し構造において不具合が再発するだけである。

*4:"error: mutating transmuted &mut T from &T may cause undefined behavior, consider instead using an UnsafeCell, #[deny(mutable_transmutes)] on by default"

*5:型キャスト操作ではイュータブル生ポインタ(*const T)を経由する必要がある。イミュータブル参照型からの直接キャスト (p as *mut _) を試みると、"error: casting `&i32` as `*mut i32` is invalid"と怒られる。