起源

ポインタは単なる「整数」や「アドレス」ではない。例えば、「幸運」があり Use After Free にて解放されたメモリがあなたの読み書きよりも前に再割り当てされたとしても、明らかに Undefineded Behavior なのは議論の余地がない (実際にはこれは最悪の場合のシナリオで、これがなければ UAF の懸念はだいぶ減るだろう!)。別の例として、wrapping_offset はオリジナルポインタが指し示すメモリの割り当てを「思い出す」と文書化されていて、これはたとえその割り当てに占有されるメモリ範囲のはるか外側にオフセットされてもなおそうである事を考えてみよう。このような主張が合理性を持つには、ポインタは単なるアドレス以上の存在である必要がある: つまりそれらには起源がなければならない。

Rust におけるポインタの値は意味論的に以下の情報を含む:

起源の正確な構造はまだ仕様化されていないが、ポインタの起源により定義される許可は空間の成分、時間の成分、そして可変性の成分を持つ:

割り当てが作成されると、それは固有のオリジナルポインタとなる。alloc API ではこれは文字通りその呼出が戻すポインタであり、ローカル変数や静的変数ではこれは変数/静的変数の名前である (これは用語「ポインタ」の簡潔な説明のためゆるい重複定義になる)。

割り当てに使われたオリジナルポインタには起源があり、それは割り当てたメモリ範囲へのこのポインタの空間の許可、そして割り当てのライフタイムへの時間の許可を制約する。起源はオリジナルポインタから offset、借用、ポインタキャストのような操作を通して推移的に派生する全てのポインタに暗黙的に継承される。幾つかの操作はアクセスできるメモリの量や有効期間を制限する事で、派生した起源の許可を縮小する (すなわち、サブフィールドやサブスライスの借用は起源の空間の成分を縮小するかもしれず、全ての借用は起源の時間の成分を縮小するかもしれない)。ただし、派生した起源の許可を拡大する操作はない: たとえより大きな割り当てがそこにあるとあなたが「把握」していても、より大きな起源でのポインタの派生はできない。同じように、二つの連続する起源を一つに戻す「再結合」もできない (例: fn merge(&[T], &[T]) -> &[T] のような操作)。

場所への参照は、常に最低でもその場所が占めるメモリを覆う起源を持つ。スライスへの参照は、常に最低でもそのスライスが表現する範囲を覆う起源を持つ。参照の起源が「縮小」されそれが指し示すメモリにぴったりと収まるかどうか、またそれが正確にいつ行われるかはまだ決まっていない。

共有参照はいつもメモリからの読込を許可する起源のみを持ち、決して書込を許さないが、UnsafeCell の内側は例外である。

起源はプログラムが未定義動作を持つかに影響しうる:

しかし、以下は依然として健全である:

Rust における起源の完全な定義はまだ決まっていない事に注意されたいが、これはまだ未決定なエイリアシングのルールとの相互作用を伴うためである。

ポインタ vs 整数

この議論により、usize が正確にはポインタを表現できない事、そしてポインタから usize への変換は一般にはアドレスを取り出すだけの操作である事が明白となった。このアドレスをポインタへ戻すには疑問: 結果のポインタはどの起源を持つべきなのか?に答える何かが必要になる。

Rust はこの状況を扱う二つの方法を提供する: 厳密な起源公開された起源

ポインタは usize を表現できる (without_provenance を介して)、そのため値が「ある時はポインタである時は生の usize」の状況で使うべき適切な型は、ポインタ型である事に注意されたい。

厳密な起源

「厳密な起源」はより明示的に起源とともに作業するための API の集まりである。それらはポインタから整数またはその逆のキャストの代替として意図されている。

ポインタへの整数のキャストを完全にやめる事で、その操作の潜在的な曖昧さを首尾よく回避できる。これはコンパイラ最適化に有益であり、ポインタの誤用の検出診断を目的とした Miri のようなツールや CHERI のようなアーキテクチャの使用要件ともうまく適合する。

ポインタへの整数のキャストを全くなしにプログラミングを行えるようにする鍵となる洞察は with_addr メソッドにある:

    /// Creates a new pointer with the given address.
    ///
    /// This performs the same operation as an `addr as ptr` cast, but copies
    /// the *provenance* of `self` to the new pointer.
    /// This allows us to dynamically preserve and propagate this important
    /// information in a way that is otherwise impossible with a unary cast.
    ///
    /// This is equivalent to using `wrapping_offset` to offset `self` to the
    /// given address, and therefore has all the same capabilities and restrictions.
    pub fn with_addr(self, addr: usize) -> Self;

そう、まだアドレス表現への落とし込みは可能で、欲するどんな巧妙なビットトリックでも実行でき、それには起源の「再構成」を可能にするために関心のある割り当てへのポインタを手元に置いておけばよい。通常これは非常に簡単である。なぜならポインタを取得し、アドレスでごちゃごやし、すぐにポインタへと戻すだけでよい。私達はこの用法がより人間工学的になるよう、map_addr メソッドを提供している。

コードが厳密な起源の意味論に「従っている」のを明確にするために、私達は addr メソッドを提供していて、これは戻されるアドレスがポインタ-整数-ポインタの往復の一部でない事を約束する。将来的には、コードが厳密な起源を順守しているかを監査できるよう、私達はポインタ ↔ 整数のキャストのためのリントを提供するかもしれない。

厳密な起源の使用

ほとんどのコードは厳格な起源を順守するのに変更する必要がないが、これは真に懸念のある操作が usize からポインタへのキャストのみだからである。usize をポインタへとキャストするコードでは、変更範囲は具体的に何をしているかによる。

一般には、ただ usize のアドレスをポインタへと変換してそのポインタをメモリの読込/書込に使いたいかを確認し、その読込/書込自身を実施するのに十分な起源を持つポインタを手元に置いておけばよい。こうするとアドレスからポインタへの全てのキャストは本質的にただのオフセットの適用やインデックス付けになる。

タグ付きポインタのようにシンプルな場合、タグ付きポインタを usize ではなく実際のポインタで表現する限り、これは大概は平凡に実現できる。

unsafe {
    // A flag we want to pack into our pointer
    static HAS_DATA: usize = 0x1;
    static FLAG_MASK: usize = !HAS_DATA;

    // Our value, which must have enough alignment to have spare least-significant-bits.
    let my_precious_data: u32 = 17;
    assert!(align_of::<u32>() > 1);

    // Create a tagged pointer
    let ptr = &my_precious_data as *const u32;
    let tagged = ptr.map_addr(|addr| addr | HAS_DATA);

    // Check the flag:
    if tagged.addr() & HAS_DATA != 0 {
        // Untag and read the pointer
        let data = *tagged.map_addr(|addr| addr & FLAG_MASK);
        assert_eq!(data, 17);
    } else {
        unreachable!()
    }
}

(そう、並行データ構造の中でポインタのために AtomicUsize を使っているなら、代わりに AtomicPtr を使うべきである。もしこれがあなたがポインタを原子操作する方法を台無しにするのなら、私達にその理由と修正方法を知らせてほしい。)

妥当なポインタをただのアドレスから作成しなければならない状況、ベアメタルコードが固定アドレスにあるメモリマップインタフェースにアクセスするような場合、現在のところ厳密な起源の API では扱えないため公開された起源を使うべきである。

公開された起源

上で議論したように、ポインタへの整数のキャストは厳密な起源の API では不可能である。これは意図的である: 厳密な起源の目標は、私達がそれに自信をもって、曖昧さなく形式化ができ正確な形式的推論の対象にできる、明確な仕様を提供する事である。整数へのポインタのキャストは (現在のところ) そうした明確な仕様を持たない。

しかしながら、ポインタへの整数のキャストが避けられないか、またはそれを避けるためには巨大なリファクタリングが求められるであろう状況は存在する。レガシーなプラットフォームの API もだいたいは usize がポインタを形成する全情報を捕捉できると想定している。ベアメタルのプラットフォームもまた正しい起源を獲得する場所がなく 「どこからともなく」 ポインタを合成する必要があったりする。

Rust でポインタへの整数のキャストを扱うモデルは公開された起源と呼ばれる。とはいえ、公開された起源の意味論は厳格な起源よりもはるかに軟弱な基盤上にあり、この時点において公開された起源に満足のいく曖昧さのない意味論を定義できるかはまだ明らかでない。もしこれが悪く聞こえるなら、ポインタへの整数のキャストを提供する他の人気のある言語もあまり良くやれてない事を再確認しよう。さらに言えば、公開された起源は MiriCHERI のようなツールでは (十分に) 機能しない。

公開された起源は expose_provenancewith_exposed_provenance メソッドによって提供され、それらはポインタと整数の間の as キャストに等しい。

可能な限り、厳密な起源の API へと移植して、公開された起源が必要となるのを避けるよう推奨する。そうしたコードの量の最大化は使用の複雑さを回避し、(アンセーフな) Rust コードの信頼性の向上に大きく貢献する CHERIMiri のようなツールの導入を促進する大きな利点となる。ただ、私達はこれが常に可能だとは限らないのも認識していて、厳密な起源のよく定義された意味論を明示的に「オプトアウト」して、不明瞭なポインタへの整数のキャストの意味論を「オプトイン」する手段として、公開された起源を提供している。