Xbox 360 における XNA ゲームのセキュリティモデルは,.NET のサンドボックスではなくハードウェアの非特権モードによるもの

を読んで,XNA CLR ゲームが非特権モードによって保護されていることを思い出した.実は XNA CLR 自体はサンドボックスの役目を果たしていない.
その点で言えば,先日の『Secure coding は Microsoft からイノベーションを奪っているか? - NyaRuRuの日記』での XNA への言及の仕方はまずかった.Singularity のあとに XNA のサンドボックスに言及すれば,当然 CLR がサンドボックスの役目を果たしているような気がしてくるが,実は違う.私もあの記事を書いているときは深く考えていなかったが,よく思い出してみると XNA ゲームの実行モードが「ユーザモード」であることがセキュリティ上の要になっている.
以下に GDC 2008 のスライドを引用する.

Xbox 360 上で実行される XNA ゲームでも,ライブラリ内部の描画処理やハードウェア操作はスーパーバイザモードで実行される.そのスーパーバイザモードで実行可能なメモリページはコード署名が必要だ.しかし XNA ゲーム本体は JIT コンパイルが必要で,JIT コンパイルは実行時に行われるため,その結果をコード署名するのは不可能だ.XNA ではどうしているかというと,動的にコンパイルされたコードをユーザモードで実行することでこの問題を解決している.
結果として,Xbox 360 上で実行される XNA ゲームでは,スーパーバイザモードとユーザモードのモード切替が必要になってしまった.モード切替を伴うシステムコール一回につき 4 マイクロ秒が消費される.このオーバーヘッドは悩ましい.そもそも Xbox 360 設計時には,XNA CLR のようなソフトウェアを載せることを想定していなかったらしい.
もちろん影響を軽減する方法はある.一度のシステムコールで複数の描画コマンドを投入できれば,システムコールの呼び出し回数も減り,オーバーヘッドの影響も削減されるはずだ.XNA ではこれが自動で行われる.ユーザモードのゲームプログラムが描画 API を呼び出すと,それは可能な範囲内でキューイングされる.とはいえパフォーマンス最適化に関する話は今回の本題ではないので,続きは PPT の方をご覧頂きたい.
セキュリティに話を戻そう.JIT コンパイル結果をユーザモードで実行することで XNA ゲームは「セキュア」とみなされている.もちろんスーパーバイザモードへの入り口にあたるシステムコールでは,厳密なパラメータチェックが行われている.ユーザモードでどんな酷いクラッシュをしようが,スーパーバイザモードは無事ということになる.
では XNA CLR 自体のセキュリティモデルはどうなっているのだろうか.先ほども述べたように,JIT 済みコードがクラッシュしてもセキュリティ上の問題はない.これに対応するかのように,XNA CLR は「検証可能コード」を要請しない.
「検証可能コード」というのは .NET 用語で,CLI 仕様のコード検証ルールをパスし,メモリアクセスに関する安全性が保証できるコードのことである.

.NET Framework 開発者ガイド - タイプ セーフとセキュリティ

タイプ セーフなコードは、アクセス権限を与えられているメモリ位置にだけアクセスします。この場合のタイプ セーフとは、メモリのタイプ セーフの意味です。広い意味でのタイプ セーフと混同しないでください。たとえば、タイプ セーフなコードは、他のオブジェクトのプライベート フィールドから値を読み取ることができません。適切に定義された許容される方法でだけ、タイプにアクセスします。

そして XNA CLR が「検証可能コード」を要請しないというのは,「型安全かどうか (CLI 仕様の範囲内で) 保証できないゲームでも,XNA は受け入れて動かしちゃいますよ」ということだ.
ここで unsafe コードについても触れておこう.

unsafe コードという言葉は,複数の意味で使われている.

  1. 有効な CIL の使い方ではあるが,型安全性が (CLI 仕様の範囲内で) 検証不能とされる使い方をしているコード
  2. C# の unsafe キーワードで修飾されたコンテキスト

検証可能性に関係するのは (1) の方である.プログラムが C# で書かれていて,C#ソースコードに unsafe キーワードで修飾されたコンテキストが存在するかどうかは,(1) の意味には関係しない.検証可能な機能のみでプログラミングされていれば,たとえソースコードに unsafe と書かれていてもいなくても,それは「検証可能コード」である.
実はここに落とし穴がある.Microsoft の C# コンパイラには,unsafe キーワードを使わなくても「検証不能」なコードを出力する方法が存在する.以下に例を示す*1

using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Explicit)]
public struct UnsafeStruct
{
    [FieldOffset(0)]
    public long[] longs;
    [FieldOffset(0)]
    public byte[] bytes;
}
static class Program
{
    static void Main(string[] args)
    {
        UnsafeStruct us = new UnsafeStruct();
        us.longs = new long[1];
        us.bytes = new byte[4];

        for (int i = 0; i < 4; ++i)
        {
            Console.WriteLine(us.longs[i]);
        }
    }
}

Visual C# は,上記コードに /unsafe オプションを要求しない.しかしこのコードは「検証不能」である.問題は StructLayout 属性の使い方にある.『プログラミングMicrosoft .NET Framework 第2版』第5章「単純型、参照型、値型」には,値型を共用体的に用いる場合には次の点に注意せよとある*2

  • 値型と参照型をオーバーラップさせるのは不正である.
  • 複数の参照型を同じ開始オフセットに配置することは許されるが,検証不能コードとなる.
  • 値型をオーバーラップさせることは許される.ただし、オーバーラップする部分を通じて一部の型の非 public 領域を書き換えられるような場合,検証不能となる.そのような可能性がなければ検証可能である.

上記コードは配列という参照型が同じ開始オフセットに配置されているため,検証不能コードということになる.
にも関わらず,あなたはこのコードを実行時エラーなしに実行できてしまうかもしれない.結果として配列サイズを超えて配列要素にアクセスできてしまったかもしれないが,それは恐らく SkipVerification 権限が有効だったためである.ローカルコンピュータ上で .NET アプリケーションの開発・実行を行っている限り,実行時のコード検証は案外と行われていないものだ.

ジャスト イン タイム (JIT: Just-In-Time) コンパイル時に、オプションの検査プロセスは、ネイティブなマシン コードに JIT コンパイルされるメソッドのメタデータと Microsoft Intermediate Language (MSIL) を調べて、タイプ セーフかどうかを確認します。コードに検査を省略するためのアクセス許可がある場合、このプロセスは省略されます。検査の詳細については、「MSIL からネイティブ コードへのコンパイル」を参照してください。

もちろん,XNA CLR は上記コードを受理する.

Appendix: CLR のコード検証ルールを知る

.NET の実行ファイルが検証可能コードかどうか調べる方法についても触れておこう.最も簡単な方法は,.NET Framework SDK に付属する PEVerify ツール を使用することだ.
ではこの PEVerify がどのように実装されているかだが,Shared Source CLI にそのソースコードらしきものが付属している.(ただしソースを見る前にライセンスにはご注意を)

  • sscli20\clr\src\tools\peverify

ソースを読んでみると,肝心の検証処理は ICLRValidator に委譲されている.おそらく .NET Framework 版の PEVerify も似たような実装になっているのだろう.
そして Rotor には Rotor 版の ICLRValidator 実装も同梱されている.もしコード検証のアルゴリズムの実装例が必要になった場合には,ここにあることを思い出すと良いだろう.

*1:この例は『[https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=98226:title=フィードバック: .NET CLR allows invalid array access with apparently safely compiled C# code]』にヒントを得ている.ただし,件名に反して,これは CLR セキュリティの欠陥ではない.C# コンパイラが /unsafe 抜きで「検証不能コード」を出力することによる混乱が原因である.投稿者もコメント中で「CLR のバグ」という主張の撤回を申し出ている.

*2:『[http://www.atmarkit.co.jp/fdotnet/directxworld/directxworld06/directxworld06_04.html:title=第6回 .NETアプリを軽快にするためのガベージ・コレクション講座 - 連載 .NETWindows Vistaへ広がるDirectXの世界]』