- WindowsカーネルドライバーもRustで記述できることを示すため、任意のスレッドの優先度を変更するBooster WDMドライバーを実装
- ビルド環境では、WDKまたはEWDK、LLVM/Clang、
windows-drivers-rsベースのWDK crates、build.rsとCargo.tomlの設定が連携する必要がある
- カーネル空間では標準ライブラリを使えないため、
#![no_std]、WDK allocator、panic handler、unsafeなFFI呼び出しを組み合わせる必要がある
- ドライバーは
\\Device\\Boosterと\\??\\Boosterを作成し、IRP_MJ_WRITEで受け取ったThreadDataを使ってスレッド優先度を1〜31の範囲で変更する
- RustでWindowsカーネルドライバーを書くことは可能だが、WDK cratesは0.3段階のため、より安全なラッパーと少ない
unsafeコードが必要
Rustで実装したBooster WDMドライバー
- この例は、Windowsカーネルプログラミングで使われる「Booster」ドライバーをRustへ移植したもの
- 目的は、任意のスレッドの優先度を希望の値に変更すること
- ドライバーモデルはWDM
- Rustエコシステムは、コンパイル時のメモリ安全性、並行性の安全性、
cargoビルドシステム、cratesエコシステムを備えている
- Cの型をRustで直接扱うとコードが冗長になりやすい
- 適切なラッパーやマクロを使えば、この負担を軽減できる
ビルド準備とCargo設定
- ドライバービルドの準備にはWindows Drivers-rsを参照し、WDKまたはEWDKをインストールする必要がある
- LLVMのインストールも必要で、Clangコンパイラへアクセスするために使う
- 新しいRustライブラリプロジェクトを作成する。カーネルドライバーは技術的にはカーネル空間にロードされるDLLである
cargo new --lib booster
build.rsは、cargoがCRTへ静的リンクするよう設定し、WDKバイナリビルドを構成する
fn main() -> Result<(), wdk_build::ConfigError> {
std::env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static");
wdk_build::configure_wdk_binary_build()
}
Cargo.tomlには、WDMドライバーモデル、cdylib crateタイプ、WDK関連の依存関係を追加する
- 主なcratesは
wdk、wdk-macros、wdk-alloc、wdk-panic、wdk-sys
- dev/releaseプロファイルには
panic = "abort"とlto = trueを設定する
wdkとwdk-sysにはオプションでnightly featureがある
標準ライブラリなしでカーネルコードを書く
- カーネルにはRust標準ライブラリがないため、
#![no_std]を使う
wdk_sysは低レベルのカーネル関数との相互運用を担い、wdkはより高水準のラッパーを提供する
VecやStringは標準ライブラリの一部に見えるが、実際にはallocモジュールの型を使える
- これを使うにはグローバルアロケータが必要
- WDK cratesが提供する
WdkAllocatorを#[global_allocator]に指定する
WdkAllocatorはExAllocatePool2とExFreePoolを使って割り当てを管理する
- 標準ライブラリがないため、allocatorサポートとpanic handlerのために
wdk_panicとalloc外部crateを追加する
DriverEntryとデバイス初期化
- Windowsカーネルドライバーのエントリポイントは
DriverEntry
- Rustの関数名は慣例に合わせて
driver_entryとし、#[export_name = "DriverEntry"]でリンカが探す名前を指定する
println!マクロはDbgPrint呼び出しとして再実装されており、C/C++でDbgPrintを使うのと同様にカーネルデバッグ出力へ使える
UNICODE_STRINGはprintln!で直接サポートされないため、unicode_to_string関数でRustのStringへ変換する
- デバイスオブジェクトは
IoCreateDeviceで\\Device\\Boosterに作成する
- 失敗時はエラーステータスを出力し、その
NTSTATUSを返す
- 成否判定には、WDKの
NT_SUCCESSマクロに似たnt_successを使う
- 標準の
CreateFile呼び出しでデバイスを開けるよう、IoCreateSymbolicLinkで\\??\\Boosterシンボリックリンクを作成する
- シンボリックリンク作成に失敗した場合はデバイスオブジェクトを削除して失敗ステータスを返す
- デバイスオブジェクトはBuffered I/Oを使うよう設定する
DriverUnloadはboost_unloadに設定する
IRP_MJ_CREATEとIRP_MJ_CLOSEはboost_create_closeで処理する
IRP_MJ_WRITEはboost_writeで処理する
- コールバックの有無はRustの
Option<>型で表現される
リクエスト処理とスレッド優先度の変更
- アンロードルーチンは
IoDeleteSymbolicLinkとIoDeleteDeviceを呼び出して、シンボリックリンクとデバイスオブジェクトを片付ける
IRP_MJ_CREATEとIRP_MJ_CLOSEの処理は単純
- IRPの
IoStatus.StatusをSTATUS_SUCCESSに設定する
IoStatus.Informationを0に設定する
IofCompleteRequestでリクエストを完了する
IoStatusはIO_STATUS_BLOCKであり、Statusへアクセスする際に自動生成されたunionメンバーを経由する必要があるため、コードが見栄えよくない
- 実際の優先度変更は
IRP_MJ_WRITEハンドラで行う
- クライアントがドライバーへ渡す構造体は、C/C++と同じメモリレイアウトにするため
#[repr(C)]を使う
#[repr(C)]
struct ThreadData {
pub thread_id: u32,
pub priority: i32,
}
boost_writeはBuffered I/Oで渡されたSystemBufferをThreadDataポインタとして解釈する
- エラーチェックには次の条件が含まれる
- データポインタがnullなら
STATUS_INVALID_PARAMETER
- 優先度が1未満または31超なら
STATUS_INVALID_PARAMETER
PsLookupThreadByThreadIdでスレッドオブジェクトを検索する
- 失敗した場合、そのスレッドIDが存在しない可能性があり、処理ループを抜ける
- スレッドが見つかったら
KeSetPriorityThreadで優先度を設定し、ObfDereferenceObjectで参照を解放する
- リクエスト完了時にはIRPのステータスと情報フィールドを設定してから
IofCompleteRequestを呼び出す
署名、インストール、テスト
- INFまたはINXファイルがあれば、cratesはドライバー署名をサポートしているようだが、この例ではINFを使わないため手動署名が必要
- プロジェクトルートで次のコマンドによりビルド成果物へ署名できる
signtool sign /n wdk /fd sha256 target\debug\booster.dll
/n wdkは、Visual Studioがドライバービルド時に通常自動生成するWDKテスト証明書を使う
- ビルド成果物の拡張子はDLL
- 現在の
cargo buildプロセスでは自動で拡張子を変更する方法はない
- INF/INXを使えば拡張子はSYSに変わる
- 拡張子は自分で変更してもよいし、DLLのままでもよい
- テスト署名が有効なマシンでは、管理者権限のコマンドプロンプトから
sc.exeを使ってソフトウェアドライバーのようにインストールできる
sc.exe sc create booster type= kernel binPath= c:\path_to_driver_file
sc.exe start booster
- テストクライアントには既存のC++アプリケーションを使う
CreateFile(L"\\\\.\\Booster", GENERIC_WRITE, ...)でデバイスを開く
ThreadDataにスレッドIDと優先度を入れ、WriteFileで送る
- 例ではID 9408のスレッド優先度を26へ変更するテストを行う
残る課題と参考資料
1件のコメント
Hacker News の意見
アプリケーションごとにパスの再マッピング規則を設定できるファイルシステム・フィルタードライバーを作ろうと考えたことがある
例えば
%userprofile%\.vscode -> %appdata%\vscode、%CSIDL_MYDOCUMENTS%\Call of Duty -> %userprofile%\Saved Games\Call of Dutyのような形Documents フォルダーとホームディレクトリが、すでに場所の決まっている雑多なファイルで埋め尽くされるのがあまりに腹立たしくて、Rust でフィルタードライバーのプロジェクトの骨組みを作り、ミニフィルターのドキュメントも読んでみたが、作業量を見て諦めた
Windows システムは結局ゴミだらけになるしかないのだと受け入れることになった
.DS_Store、.fseventsd、._xxxxのようなファイルは、何度消しても Apple があちこちに撒き散らし続けるそれでも macOS にはアプリケーションのインストール先とユーザー文書の場所が概ねそれぞれ1つずつあり、ほとんどのアプリはある程度それを守っている
その代わり
~/Libraryのような指定されたゴミ捨て場があり、そこには自分に必要なのか分からない雑多なものが大量に入っているShortcut to Documents (OneDrive - Personal)だった今ではそのゴミまでデバイス間で同期されていて、実にありがたい
Detours ライブラリは、プロセス実行時にユーザー DLL を差し込み、ファイルシステム API への Win32 呼び出しをフックできるようにしてくれる
組み込みの「互換性 shim」もほとんど文書化されていないが似たように動作し、互換性フラグを有効にしてファイルやレジストリのパスをリダイレクトできる
特定の EXE に対してだけヒューリスティックベースで発動するよう設計されている点も便利
App-V や後の Docker コンテナのような技術を支えるために、Windows にはファイルシステム、レジストリ、その他の NT カーネル名前空間を仮想化する API サーフェスがある
ほかにも User Mode Filesystem があり、自分が知らない、あるいは忘れているアプローチもまだありそう
My Documentsという有害廃棄物処分場は完全に無視し、実際に気にかけるファイルはもっと短いパスの別の場所に置いているデータの場所をハードコードしているプログラムの1つか2つは、ディレクトリジャンクションでリダイレクトしていたかもしれない
ntuser.ini、ntuser.dat.LOG1、ntuser.dat.LOG2、NTUSER.DAT、NTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TM.blf、NTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TMContainer00000000000000000001.regtrans-ms、NTUSER.DAT{b2352f18-cdbf-1122-8680-002248483d79}.TMContainer00000000000000000002.regtrans-msのようなもの半分関連する話として、Windows カーネルでの Rust 利用状況について最新情報があるのか気になる
ほぼ2年前に「システムコールを含む36,000行のコード」と言っていたが [1]、そのプロジェクトがどう進んだのか知りたい
[1] https://www.thurrott.com/windows/282471/microsoft-is-rewriti...
最近ではセキュリティエンクレーブのファームウェアがある
https://techcommunity.microsoft.com/blog/windows-itpro-blog/...
win32k.sysの一部が Rust に移植されているwin32*.sysで検索してみると、どのように分割されたのかが分かる興味深い。自分が Rust で多く触ってきた組み込みドライバーとはかなり違って見える
そちらはたいてい、レジスタの読み書き、ビットシフト、DMA、データシート参照が中心
ここのコードは実質的に、構文だけが違う C のように見える。すべての関数が
unsafeと示され、すべてのリソースを手動で管理している率直に言って申し訳ないが、これではRust を使う意味が分からない
string_to_ustringから返されたUNICODE_STRINGを使おうとする試みは、確定的な解放後使用だWindows カーネルコードを書きたいなら、ここから始めるべきではない
実際に面白いことをするドライバーなら、境界部分にだけ
unsafeインターフェースを置き、内部はより典型的な Rust コードで書ける本当に標準的な Rust の命名規則を全部捨てて、古い Windows 流の「名前で型を表す」慣習を受け入れるつもりなのか?
25年ほど前、Windows 用の特定のドライバーを書かなければならなかった
その頃にはすでに Linux に完全移行していたので、作成とビルドに Windows を使いたくなく、MSYS でビルドできるようにするためかなり頑張った
最終的に成功し、ドライバーも問題なく動作した
実際にロードできるようにするには、生成物の PE ファイル(
.sys)にパッチャーを使う必要があった気がする面白い時代だった
記事も良いが、ブログのデザインのほうが印象的
すっきりしていて直感的で、目に優しく、すぐに読み込まれる
見たところ WordPress.com のようだ
キャッシュと CDN にかなり依存しているらしい
10年前に自分の WordPress ブログを維持し続けなかったことが惜しくなった