ネコのために鐘は鳴る

寺院に住み着くパソコ〇好き

(C#) ReadOnlyCollection<T> を Unsafe.As で違法に書き換える

System.Runtime.CompilerServices.Unsafe クラス

System.Runtime.CompilerServices.Unsafeクラスは C# において safe なコンテキストで unsafe なことができる、.NET の標準ライブラリにある公式黒魔術書です。Unsafeという名前から明らかですが、safe で使える (ポインタをかかない) にもかかわらず unsafe と同等かそれ以上に危険なことができます。

むしろUnsafeクラスが提供しているメソッドの内容は C# の表現力ではコードとして書き表せないものが大半です。

なのでUnsafeクラスのソースコードは直接 IL で書かれています。.NET Core 3.1 のUnsafeクラスのソースコードはこれです。

github.com

各メソッドでどんなことができるのかは MSDN を見るか、このページが分かりやすいです。

blog.meilcli.net

Unsafe.As メソッド

いろいろあるUnsafeクラスの中でもかなり危険だと (個人的に) 思うのが、T Unsafe.As<T>(object) where T : classメソッドで、継承関係に関わらず強制的に型変換できるというものです。

// ClassA と ClassB は互いに継承関係にない class 

ClassA a = new ClassA();          // ClassA のインスタンス
ClassB b = Unsafe.As<ClassB>(a);  // 強制型変換 (本来はキャスト不可)

これが何の例外も出ずに実行できてしまいます。一体何が起こっているのかさっぱり分からないのでUnsafeクラスのソースコードを覗いてみましょう。ソースコードは前述の通り IL です。

.method public hidebysig static !!T As<class T>(object o) cil managed aggressiveinlining
{
    .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor() = ( 01 00 00 00 )
    .maxstack 1
    ldarg.0
    ret
} // end of method Unsafe::As

スマートすぎる、天才か?

  • 1行目 : .custom instance ...はおまじないです。(このメソッドの属性の宣言)
  • 2行目 : 使用するスタックの数の宣言 (スタック数 1)
  • 3行目 : 引数をスタックの0番目にロード
  • 4行目 : スタックの先頭を return

仰々しく書いたがやっていることは完全に素通しです。IL にとっては引数のobject oが何の型だろうが知ったことではなく、所詮はただの参照、それをT型として返すだけ。

こんな単純なことなのに C# の表現力ではこれは書けません。如何に C#コンパイラによって安全に守られているか分かります。

 

しかし、これは IL が素通しで型を変えただけであるため、それが C# の型としてメモリ上で有効な状態になっている保証は一切ありません。レイアウトが異なる型を変換すると、意図したプロパティとは違うプロパティが呼ばれてしまう、アクセスできないメモリを参照しようとしてクラッシュして落ちる、等おかしな挙動が起きます。

つまり、型情報や仮装関数テーブルを経由するメソッド呼び出しは、本来ありえない型のテーブルを引くため未定義動作になります。(たいていはAccessViolationException等を吐いて落ちますが、たまたま有効な別メソッドを引くこともできるので動きは未定義)

継承関係にないがメモリレイアウトが同じ型のキャストを無理やり実行したい、というのが使い道です。 勘違いしそうですが、このメソッドは実行時のインスタンスの動的型を変える魔法ではなく、静的型をすり替えてコンパイラの型チェックをごまかすだけです。  

ちなみにUnsafe.Asメソッドにはもう一つオーバーロードがあって、Unsafe.As<TFrom, TTo>(ref TFrom)です。先ほどのはclass用なので、構造体を見かけ上、別の構造体として扱いたいときはこちらです。

ロジックは先ほどと同じで、あくまで参照を素通しして見かけの型を変えるものであるため、入力・出力ともにrefです。`構造体を値のコピーではなくダイレクトに別のものとして叩き込みたい、のようなアグレッシブな場面で使えます。

ReadOnlyCollection<T> の中身を書き替え

Unsafe.As<T>(object)の悪用(?)法として、System.Collections.ObjectModel.ReadOnlyCollection<T>の中身を書き替えてみたい、というのが今回の本題。

.NET Core のReadOnlyCollection<T>のソースは[これです]。(https://source.dot.net/#System.Private.CoreLib/ReadOnlyCollection.cs)

メソッドとプロパティがいっぱいありますが、メモリ上でのレイアウトに関係ないものを取っ払うと、以下のようになってます。

public class ReadOnlyCollection<T>
{
    private IList<T> list;
}

本当にただIList<T>を read only にラップしただけなのがよくわかります。要はこのprivateな変数を引っこ抜いてこれば書き換えができます。普通ならリフレクションでprivateフィールドを取ってくるんですが、今回の趣旨はUnsafeクラスを使ってやります。

Unsafe.As<T>(object)を使って、ReadOnlyCollection<T>を同じメモリレイアウトで中身がpublicな別の型にすり替えます。

// ReadOnlyCollection<T> と同じメモリレイアウトを持つクラス
public class IllegalExposure<T>
{
    public IList<T> Content;
}

これを使って型をすり替えます

// もとになる ReadOnlyCollection
ReadOnlyCollection<int> collection = ###;

// 強制キャストで型をすり替える
var illegal = Unsafe.As<IllegalExposure<int>>(collection);
// 中身が public になったので抜き出す
var content = illegal.Content;

content[0] = 1234;  // 中身を書き替える

抜き出した中身を書きかえると、元のReadOnlyCollection<T>の中身も当然書き換わります。

これ自体はリフレクションでも全く同じことができるのですが、速度が桁違いです。リフレクション経由でのprivateフィールドの取得などは余裕で2, 3桁実行速度が遅くなるのに対し、この強制キャストの中身は先ほど見た通り参照素通しなので爆速です。比になりません。

 

Unsafeクラス、フォースの暗黒面は素晴らしい