Read OSS

カーネルのRust: 安全でない基盤の上に築く安全な抽象化

上級

前提知識

  • 第1回: アーキテクチャとディレクトリマップ
  • 第2回: ブートシーケンスと初期化
  • Rustの基礎(所有権、トレイト、no_std、unsafe)
  • 第4回で解説したCの関数ポインタ構造体の理解

カーネルのRust: 安全でない基盤の上に築く安全な抽象化

このシリーズを通じて、ある共通のパターンが繰り返し登場してきました。sched_classfile_operationsinode_operationsio_issue_def といった、関数ポインタで埋まったC構造体です。これらは実行時ポリモーフィズムの vtable として機能しています。動作はしますが、根本的なリスクも抱えています。関数ポインタが NULL のままでもファイルシステムを止める手段はなく、ロックの解放を忘れたドライバを検出する術もなく、use-after-free をコンパイル時に捉えることもできません。Rust-for-Linux イニシアチブは、こうした問題を解消することを目指しています。ただしカーネルを書き直すのではなく、既存のCパターンを安全なRust抽象化でラップし、バグのクラスごと根絶するというアプローチです。

第1回で触れたように、Rustサポートはトップレベルの Kbuild に書かれた obj-$(CONFIG_RUST) += rust/ によって条件付きでコンパイルされます。この記事では、その rust/ ディレクトリに何が含まれているか、CバインディングがどのようにRust APIへと流れ込むか、そして実際のGPUドライバがこれらの抽象化をどう活用しているかを見ていきます。

kernel クレートの構造

kernel クレートは、カーネル内のRustコード全体を支える中心的なライブラリです。lib.rs を読むと、CカーネルのKconfig駆動コンパイルモデルを反映したアーキテクチャが浮かび上がります。

rust/kernel/lib.rs#L1-L160

#![no_std]
...
#[cfg(not(CONFIG_RUST))]
compile_error!("Missing kernel configuration for conditional compilation");

extern crate self as kernel;

pub use ffi;

pub mod alloc;
#[cfg(CONFIG_AUXILIARY_BUS)]
pub mod auxiliary;
#[cfg(CONFIG_BLOCK)]
pub mod block;
pub mod clk;
#[cfg(CONFIG_CONFIGFS_FS)]
pub mod configfs;
pub mod device;
pub mod driver;
#[cfg(CONFIG_DRM = "y")]
pub mod drm;
pub mod error;
pub mod fs;
#[cfg(CONFIG_I2C = "y")]
pub mod i2c;
pub mod init;
pub mod irq;
#[cfg(CONFIG_NET)]
pub mod net;
#[cfg(CONFIG_PCI)]
pub mod pci;
pub mod platform;
pub mod prelude;
pub mod sync;
pub mod task;
...
graph TD
    subgraph "kernel crate (rust/kernel/)"
        lib["lib.rs<br/>#![no_std]"]
        lib --> prelude["prelude.rs"]
        lib --> alloc["alloc/"]
        lib --> device["device.rs"]
        lib --> driver["driver.rs"]
        lib --> drm["drm/<br/>#[cfg(CONFIG_DRM)]"]
        lib --> auxiliary["auxiliary.rs<br/>#[cfg(CONFIG_AUXILIARY_BUS)]"]
        lib --> pci["pci.rs<br/>#[cfg(CONFIG_PCI)]"]
        lib --> init["init.rs"]
        lib --> sync["sync/"]
        lib --> fs["fs.rs"]
        lib --> net["net/<br/>#[cfg(CONFIG_NET)]"]
    end

#[cfg(CONFIG_*)] 属性は、Cの #ifdef CONFIG_* に相当するRust版です。KconfigシンボルはRustコンパイラへ --cfg フラグとして渡されるため、#[cfg(CONFIG_DRM = "y")] は DRM がビルトイン(ローダブルモジュールとしてではない)の場合にのみ DRM モジュールをビルドに含めます。この密な統合により、Rustコードも3,000万行のCコードベース全体を管理する条件付きコンパイルシステムに参加できます。

14行目の #![no_std] が示す通り、標準ライブラリは使用しません。カーネルは独自のアロケータ、独自の文字列型、独自の同期プリミティブを持っています。カーネル内のRustはフリースタンディング環境で動作します。

Prelude と共通抽象化

すべてのRustカーネルモジュールは use kernel::prelude::* から始まります。

rust/kernel/prelude.rs

pub use crate::alloc::{flags::*, Box, KBox, KVBox, KVVec, KVec, VBox, VVec, Vec};

pub use macros::{export, fmt, kunit_tests, module, vtable};

pub use pin_init::{init, pin_data, pin_init, pinned_drop, InPlaceWrite, Init, PinInit, Zeroable};

pub use super::error::{code::*, Error, Result};

pub use super::{str::CStrExt as _, ThisModule};

pub use super::{pr_alert, pr_crit, pr_debug, pr_emerg, pr_err, pr_info, pr_notice, pr_warn};

主要な抽象化は以下の通りです。

  • KVecKBox — 標準ライブラリのアロケータではなく GFP_KERNEL フラグを使うカーネル対応アロケータ
  • Error / Result — カーネルのエラーコード(-ENOMEM-EINVAL)をRustの型システムでラップしたもの
  • ThisModule — 現在のカーネルモジュールへの参照(所有権追跡のために使用)
  • pr_info! など — フォーマットチェック付きの printk() ラッパー
  • pin_datapin_init — ピン留め(移動不可)型を安全に初期化するマクロ
  • vtable — Rustトレイトと Cの関数ポインタ構造体を橋渡しするマクロ

ヒント: 新しいRustカーネルモジュールを書くなら、まず use kernel::prelude::*module! マクロから始めましょう。この2つだけで、基本的なモジュールライフサイクル管理に必要なものはすべて揃います。

C → Rust バインディングパイプライン

RustカーネルコードはCの関数を直接呼び出しません。代わりに、階層化されたパイプラインを経由します。

flowchart TD
    A["C Headers<br/>(include/linux/*.h)"] -->|bindgen| B["bindings_generated.rs<br/>(raw unsafe FFI)"]
    B --> C["rust/bindings/lib.rs<br/>(raw module, re-exported)"]
    C --> D["rust/kernel/*.rs<br/>(safe Rust abstractions)"]
    D --> E["Kernel Modules<br/>(drivers/gpu/drm/nova/, etc.)"]

    style A fill:#f9f,stroke:#333
    style B fill:#fbb,stroke:#333
    style C fill:#fbb,stroke:#333
    style D fill:#bfb,stroke:#333
    style E fill:#bfb,stroke:#333

レイヤー1: Raw バインディングbindgen がCヘッダを処理し、生の extern "C" 関数宣言と型定義を含む bindings_generated.rs を生成します。これらは unsafe であり、使い勝手も良くありません。

rust/bindings/lib.rs は、生成されたコードを適切な #![allow] 属性でラップします。

//! This crate may not be directly used. If you need a kernel C API that is
//! not ported or wrapped in the `kernel` crate, then do so first instead of
//! using this crate.

#![no_std]
#![allow(clippy::all, missing_docs, non_camel_case_types, ...)]

mod bindings_raw {
    ...
    include!(concat!(env!("OBJTREE"), "/rust/bindings/bindings_generated.rs"));
}

このコメントはポリシーの表明です。ドライバは bindings を直接使用してはなりません。生のバインディングはあくまで中間表現です。

レイヤー2: 安全な抽象化rust/kernel/ 内のモジュールが、生のバインディングを安全なRust型でラップします。生の struct device * は RAII クリーンアップ付きの Device になり、生のスピンロックは型安全なロックを持つ SpinLock<T> になります。Rustの所有権システムがもっとも価値を発揮するのはここです。ライフタイムとロックのルールがコンパイル時に強制されます。

レイヤー3: ドライバ — 実際のカーネルドライバは安全な抽象化のみを使います。生のCポインタではなく、Rustのトレイトと型だけを扱います。

rust/Makefile がこのパイプライン全体を統制しています。

obj-$(CONFIG_RUST) += core.o compiler_builtins.o ffi.o
always-$(CONFIG_RUST) += bindings/bindings_generated.rs bindings/bindings_helpers_generated.rs
obj-$(CONFIG_RUST) += bindings.o pin_init.o kernel.o

#[vtable] とピン初期化パターン

#[vtable] マクロ

第4回では、Cが struct file_operations と関数ポインタを使ってポリモーフィズムを実現する様子を見ました。Rustにおける答えが #[vtable] 属性マクロです。トレイト実装にこれを付けると、対応するCの操作構造体が自動生成されます。実装されていないオプション操作には自動的に NULL が埋められ、has_* フラグも適切に設定されます。

次のように書くと:

#[vtable]
impl drm::Driver for NovaDriver {
    type Data = NovaData;
    type File = File;
    type Object = gem::Object<NovaObject>;
    const INFO: drm::DriverInfo = INFO;
    ...
}

#[vtable] マクロは、Rustのトレイトメソッドにコールバックする関数ポインタを持つ C互換の struct drm_driver を生成し、C-Rust間のFFI境界も自動的に処理します。実装していない必須メソッドはコンパイルエラーになります。実装していないオプションメソッドは NULL 関数ポインタになり、対応する HAS_* 定数が false に設定されます。

ピン初期化

カーネルのデータ構造には、自己参照(リストヘッドが構造体自身を指すなど)だったり、固定されたメモリアドレスに置かれなければならない(ハードウェアに登録される構造体など)ものが頻繁にあります。Cではその場で初期化して一切移動しません。しかしRustのムーブセマンティクスはこれを困難にします。

#[pin_data]try_pin_init! マクロがこの問題を解決します。

#[pin_data]
pub(crate) struct NovaData {
    pub(crate) adev: ARef<auxiliary::Device>,
}

#[pin_data] 属性は、構造的にピン留めされている(移動してはならない)フィールドを指定します。try_pin_init! はインプレース初期化を提供し、値を最終的なメモリ位置に直接構築するため、一度も移動しません。これは、構築中にCサブシステムへ自分自身を登録するカーネルオブジェクトにとって不可欠な仕組みです。

classDiagram
    class RustTrait["trait drm::Driver"] {
        +type Data
        +type File
        +const INFO: DriverInfo
    }
    class VtableMacro["#[vtable] generates"] {
        +C struct drm_driver
        +fn pointers → Rust methods
        +NULL for optional ops
    }
    class PinInit["#[pin_data] + try_pin_init!"] {
        +In-place construction
        +No move after init
        +Self-referential safe
    }
    RustTrait --> VtableMacro : "attribute macro"
    PinInit --> RustTrait : "initializes pinned data"

Nova DRMドライバの解説

Nova は NVIDIA GPU 向けの実在のRustカーネルドライバです。おもちゃのサンプルではなく、Rustドライバモデル全体を体現するツリー内ドライバとして存在しています。drivers/gpu/drm/nova/ に置かれています。

モジュールのルート:

drivers/gpu/drm/nova/nova.rs

mod driver;
mod file;
mod gem;

use crate::driver::NovaDriver;

kernel::module_auxiliary_driver! {
    type: NovaDriver,
    name: "Nova",
    authors: ["Danilo Krummrich"],
    description: "Nova GPU driver",
    license: "GPL v2",
}

module_auxiliary_driver! マクロは、Cの module_init() / module_exit() に相当するRust版です。第2回で説明した initcall、モジュールのメタデータ、登録・登録解除コードを生成します。type: NovaDriver によって、ドライバを実装している型がマクロに伝わります。

ドライバの実装:

drivers/gpu/drm/nova/driver.rs#L1-L79

pub(crate) struct NovaDriver {
    drm: ARef<drm::Device<Self>>,
}

impl auxiliary::Driver for NovaDriver {
    type IdInfo = ();
    const ID_TABLE: auxiliary::IdTable<Self::IdInfo> = &AUX_TABLE;

    fn probe(adev: &auxiliary::Device<Core>, _info: &Self::IdInfo) -> impl PinInit<Self, Error> {
        let data = try_pin_init!(NovaData { adev: adev.into() });
        let drm = drm::Device::<Self>::new(adev.as_ref(), data)?;
        drm::Registration::new_foreign_owned(&drm, adev.as_ref(), 0)?;
        Ok(Self { drm })
    }
}

#[vtable]
impl drm::Driver for NovaDriver {
    type Data = NovaData;
    type File = File;
    type Object = gem::Object<NovaObject>;
    const INFO: drm::DriverInfo = INFO;

    kernel::declare_drm_ioctls! {
        (NOVA_GETPARAM, drm_nova_getparam, ioctl::RENDER_ALLOW, File::get_param),
        (NOVA_GEM_CREATE, drm_nova_gem_create, ioctl::AUTH | ioctl::RENDER_ALLOW, File::gem_create),
        (NOVA_GEM_INFO, drm_nova_gem_info, ioctl::AUTH | ioctl::RENDER_ALLOW, File::gem_info),
    }
}
sequenceDiagram
    participant K as Kernel (C)
    participant R as Rust Abstractions
    participant N as Nova Driver

    K->>R: auxiliary bus probe callback
    R->>N: NovaDriver::probe(adev, info)
    N->>N: try_pin_init!(NovaData { ... })
    N->>R: drm::Device::new(adev, data)
    R->>K: C drm_dev_alloc() via FFI
    K-->>R: struct drm_device*
    R-->>N: ARef<drm::Device>
    N->>R: drm::Registration::new_foreign_owned()
    R->>K: C drm_dev_register() via FFI
    K-->>R: Success
    R-->>N: Ok(Self { drm })

各パターンを順に追ってみましょう。

  1. impl auxiliary::Driver — Cの struct auxiliary_driver をラップしたRustトレイト。カーネルがこのドライバをデバイスにマッチングすると probe 関数が呼ばれます。
  2. try_pin_init! — 上で述べた通り、NovaData をインプレースで初期化します。
  3. drm::Device::<Self>::new() — DRMデバイスを作成します。Cの drm_dev_alloc() を、自動クリーンアップ付きの型安全なRustラッパーでくるんでいます。
  4. drm::Registration::new_foreign_owned() — DRMサブシステムにデバイスを登録します(Cの drm_dev_register() に対応)。
  5. #[vtable] impl drm::Driver — DRMドライバの操作を宣言します。declare_drm_ioctls! マクロが ioctl ディスパッチテーブルを生成します。

このコードの美しさは、存在しないものにあります。手動のエラークリーンアップパス、失敗時の登録解除忘れ、NULL 関数ポインタのリスク——これらがすべて消えています。Rustの型システムが Drop を通じてリソースのクリーンアップを担い、? 演算子が自動アンワインドを伴いながらエラーを伝播します。

Kbuild 統合と今後の展望

統合ポイントはトップレベル Kbuild のたった1行です。

Kbuild#L103

obj-$(CONFIG_RUST)  += rust/

CONFIG_RUST=y の場合、ビルドシステムは次の順序で動作します。

  1. corecompiler_builtinsffi クレートをコンパイルする
  2. bindgen を実行して bindings_generated.rs を生成する
  3. kernel クレートをコンパイルする
  4. 設定で有効化されたRustドライバをコンパイルする

現在のカーネル(lib.rs の19〜59行目)は、arbitrary_self_typesderive_coerce_pointee などいくつかの unstable なRust機能を必要とします。一部はすでに新しいバージョンのRustで安定化されており、リリースのたびにリストは縮小しています。長期的な目標は stable なRustのみで動作させることです。標準の rustc をインストールすれば誰でもカーネルのRust開発に参加できる環境を目指しています。

Rust-for-Linux イニシアチブはカーネルを書き直そうとしているのではありません。新しいコード、とりわけドライバ——コードベースの60%を占め、バグの主な発生源でもある部分——に向けた並行の道を作ろうとしています。デバイスモデル、DRM、ファイルシステム、ネットワークへと抽象化のカバレッジが広がるにつれ、より多くの新しいドライバをRustで書けるようになります。メモリ安全性の保証を享受しながら、このシリーズ全体で見てきた既存のCサブシステムとシームレスに連携できます。

ヒント: Rustサポート付きでカーネルをビルドするには、rustcbindgenrustfmt のインストールが必要です。make LLVM=1 rustavailable を実行してツールチェーンが対応しているか確認した上で、.configCONFIG_RUST=y を有効にしてください。

シリーズのまとめ

6本の記事を通じて、Linuxカーネルを3,000万行のディレクトリ構造から出発し、ブート初期化、プロセススケジューリング、syscallエントリ、非同期I/O、そしてRustの安全性レイヤーまで順に追いかけてきました。パターンは繰り返します。分散登録のためのリンカセクションのトリック、ポリモーフィズムのためのC関数ポインタ構造体、パフォーマンスのためのキャッシュライン意識のデータレイアウト、オーバーヘッドを排除するための共有メモリ。これらは孤立したテクニックではなく、世界で最も広く使われているOSのアーキテクチャの語彙です。これらを理解することで、カーネルソースのどの部分を読むときにも通じる確かな基礎が身につきます。