ソフトウェアの脆弱性のうち、約70%がメモリ関連の問題に起因しています(出典: Microsoft Security Response Center)。バッファオーバーフローやuse-after-free(解放済みメモリへのアクセス)といったバグは、攻撃者にシステムの制御を奪われるリスクを生みます。Rustはこの課題に対し、ガベージコレクション(GC)を使わず、コンパイル時の静的検査でメモリ安全性を保証するという独自のアプローチを採用しています。
メモリ安全性とは何か
メモリ安全性とは、プログラムがRAMにアクセスする際にバッファオーバーフローやダングリングポインタなどのバグから保護されている状態を指します(出典: Wikipedia メモリ安全性)。
メモリ安全性の問題は、空間的エラー(spatial error) と時間的エラー(temporal error) の2つに大別されます。空間的エラーは確保した領域の範囲外にアクセスする問題、時間的エラーは有効でなくなったメモリにアクセスする問題です。
| 脆弱性の種類 | 分類 | 発生条件 | 実際の被害例 |
|---|---|---|---|
| バッファオーバーフロー | 空間的 | 確保した領域を超えてデータを書き込む | Heartbleed(2014年、80万件超のWebサイトに影響) |
| use-after-free | 時間的 | 解放済みメモリを参照する | BLASTPASS(2023年、ユーザー操作なしでiPhoneを侵害) |
| ダングリングポインタ | 時間的 | 無効なメモリ領域を指すポインタを使用する | クラッシュ、データ破壊 |
| データ競合 | 時間的 | 複数スレッドが同時にメモリへ読み書きする | 不定な動作、セキュリティホール |
CやC++ではプログラマがメモリの確保・解放を手動で管理します。この手動管理がヒューマンエラーを招き、上記の脆弱性を生む原因となっています。Google Chromiumプロジェクトでも重大なセキュリティバグの約70%がメモリ安全性の問題です(出典: Chromium Memory Safety)。
Rustの所有権システム:GCなしでメモリを安全に管理する仕組み
Rustのメモリ安全性を支える中核が「所有権(ownership)」システムです。GCのような実行時コストを伴わず、コンパイル時にメモリ管理の正しさを検証します。
所有権の3つの基本ルール
- Rustの各値には「所有者」となる変数が1つだけ存在する
- 所有者がスコープを外れると、その値は自動的に解放される(
dropが呼ばれる) - 所有権は別の変数に「ムーブ」できるが、ムーブ後に元の変数は使用できない
Rustでは変数はデフォルトで不変(immutable)です。可変にするにはlet mutで明示的に宣言する必要があります。C++のデフォルトが可変(mutableが標準、constで不変を明示)であるのとは逆の設計で、意図しないデータ変更を言語レベルで抑止しています。
C言語では解放忘れがメモリリークに、二重解放がクラッシュにつながります。Rustでは所有権ルールにより、こうした問題がコンパイル時に検出されます。
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1の所有権がs2にムーブ
// println!("{}", s1); // コンパイルエラー: s1はもう使えない
println!("{}", s2); // OK: s2が所有者
} // s2がスコープを外れ、メモリが自動解放される
同等のC++コードでは、s1とs2が同じメモリ領域を指す状態(浅いコピー)が起こりえます。どちらかが先に解放されると、もう一方はダングリングポインタとなり、use-after-freeバグの原因になります。Rustのムーブセマンティクスは、この種の問題を型システムのレベルで排除しています。
ムーブ・クローン・コピーの使い分け
| 操作 | 動作 | 用途 |
|---|---|---|
| ムーブ | 所有権を移動、元の変数は無効化 | Stringやヒープデータ |
クローン(.clone()) | ヒープデータを含めた深いコピー | 明示的に複製が必要な場合 |
コピー(Copyトレイト) | スタック上のビットコピー | i32、f64、bool等の固定サイズ型 |
Copyトレイトを実装する型はムーブではなくコピーされるため、元の変数も引き続き使用できます。整数型やブール型がこれに該当します。
借用とライフタイム:所有権を移さずにデータを参照する
所有権をムーブすると元の変数が使えなくなるため、関数にデータを渡すたびにムーブしていては不便です。Rustでは「借用(borrowing)」によって、所有権を移さずにデータを参照できます。
不変参照と可変参照
fn calculate_length(s: &String) -> usize {
s.len()
// sは借用しているだけなので、ここで解放されない
}
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // sの不変参照を渡す
println!("'{}'の長さ: {}", s, len); // sはまだ使える
}
Rustの借用には以下のルールがあります。
- 不変参照(
&T):同時に複数作成できるが、データの変更は不可 - 可変参照(
&mut T):同時に1つだけ作成でき、データの変更が可能 - 不変参照と可変参照は同時に存在できない
この排他的ルールは「Mutability XOR Aliasing(可変性と別名の排他)」原則と呼ばれ、借用チェッカーの核心を成しています。Read-Writeロックと同様の考え方で、「読み取りは複数同時に可能だが、書き込みは排他的に行う」という制約をコンパイル時に強制します。C/C++では複数のポインタが同じメモリ領域を読み書きする状況をコンパイラが検出できず、実行時に不定な動作を引き起こすケースがあります。
ライフタイム注釈
ライフタイムは「参照が有効な期間」を表します。多くの場合、コンパイラが自動的に推論しますが、複数の参照が関わる場面では明示的な注釈が必要です。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
ライフタイム注釈 'a は、戻り値の参照が引数 x と y のうち短い方のライフタイムと同じ期間だけ有効であることを示します。この仕組みにより、ダングリングポインタの発生がコンパイル時に検出されます。
borrow checkerによるコンパイル時検証の仕組み
Rustコンパイラに組み込まれたborrow checkerは、所有権・借用・ライフタイムのルール違反を静的に検出します。プログラムの実行前にメモリ安全性を保証する点が、他の言語と大きく異なります。
borrow checkerが防ぐ問題の例を示します。
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // 不変参照を取得
v.push(4); // コンパイルエラー:不変参照が存在する間に可変操作はできない
println!("{}", first);
}
C++のstd::vectorでは、push_backによって内部バッファが再割り当てされると、既存のイテレータや参照が無効化されます。この問題はコンパイル時に検出されず、実行時にクラッシュや不正メモリアクセスとして現れます。Rustのborrow checkerはこのパターンを静的に捕捉します。
メモリ管理方式の比較:GC・手動管理・所有権
プログラミング言語のメモリ管理方式は大きく3つに分類されます。Rustの所有権モデルがどこに位置づけられるか整理します。
| 特性 | GC搭載言語(Go, Java, C#) | 手動管理(C, C++) | 所有権モデル(Rust) |
|---|---|---|---|
| メモリ解放のタイミング | GCが自動判定 | プログラマが明示 | スコープ離脱時に自動 |
| 実行時オーバーヘッド | GCの停止(Stop-the-World)あり | なし | なし |
| メモリ安全性の保証時点 | 実行時 | 保証なし | コンパイル時 |
| ダングリングポインタ | 発生しない | 発生する | コンパイルエラー |
| データ競合の防止 | 言語仕様による(部分的) | 防止機構なし | borrow checkerで検出 |
| 組み込み・リアルタイム適性 | GC停止が制約 | 高い | 高い |
GC搭載言語はメモリ安全性を実行時に担保しますが、GCの停止時間が予測困難なため、リアルタイムシステムや組み込み分野では制約となります。Rustは実行時オーバーヘッドなしでメモリ安全性を保証するため、パフォーマンスとセキュリティの両立が求められるシステムプログラミング領域で強みを発揮します。
Rustの「安全」の正確な範囲:unsafeとメモリリーク
Rustのメモリ安全性には明確な境界があります。過大評価も過小評価もせず、正確に理解することが重要です。
safe Rustが保証するもの
safe Rust(unsafeブロックを使わないコード)では、以下が保証されます。
- 未定義動作(Undefined Behavior)が発生しない
- ダングリングポインタが存在しない
- バッファオーバーフローが発生しない(境界チェック付き)
- データ競合が発生しない
「安全」の定義は「未定義動作が発生しないこと」です。未定義動作とは、C/C++の仕様で「何が起きてもよい」と規定されている操作であり、コンパイラの最適化によって予測不能な結果を招く原因となります。
unsafeの役割
unsafeキーワードは「コンパイラの安全性チェックを一部解除する」ための仕組みです。以下の5つの操作にのみ使用します。
- 生ポインタの参照外し
unsafeな関数やメソッドの呼び出し- 可変な静的変数へのアクセスと変更
unsafeなトレイトの実装unionのフィールドへのアクセス
unsafeはメモリ安全性の放棄ではなく、「この範囲の安全性はプログラマが責任を持つ」という宣言です。OS APIの呼び出し、FFI(外部関数インタフェース)、ハードウェア直接操作など、コンパイラが安全性を検証できない操作に限定して使います。
fn main() {
let mut num = 5;
let r = &mut num as *mut i32; // 生ポインタの作成はsafe
unsafe {
*r = 10; // 生ポインタの参照外しはunsafeブロック内で行う
}
println!("{}", num); // 10
}
Rustの標準ライブラリ自体も内部的にunsafeを使用していますが、安全なAPIとして外部に公開しています。unsafeの使用を局所化し、安全な抽象で包むことが推奨される設計パターンです。
メモリリークは「安全」に含まれない
Rustにおけるメモリリーク(確保したメモリを解放しないまま参照を失うこと)は「安全」の範囲外です。std::mem::forgetやRc(参照カウント型)の循環参照によって、safe Rustでもメモリリークは発生します。
use std::rc::Rc;
use std::cell::RefCell;
// 循環参照によるメモリリークの例
#[derive(Debug)]
struct Node {
next: Option<Rc<RefCell<Node>>>,
}
fn main() {
let a = Rc::new(RefCell::new(Node { next: None }));
let b = Rc::new(RefCell::new(Node { next: Some(Rc::clone(&a)) }));
a.borrow_mut().next = Some(Rc::clone(&b)); // 循環参照が発生
// a, bのスコープが終了しても参照カウントが0にならず、メモリが解放されない
}
メモリリークは未定義動作ではないため、Rustの安全性モデルでは「許容」されています。循環参照を防ぐにはWeak(弱参照)を使う設計が有効です。
米国政府が推進するメモリ安全言語への移行
メモリ安全性は技術的な議論にとどまらず、国家のサイバーセキュリティ政策として推進される段階に入っています。
ホワイトハウスONCDの報告書(2024年2月)
2024年2月26日、米国ホワイトハウスの国家サイバー局長室(ONCD)は「Back to the Building Blocks: A Path Toward Secure and Measurable Software」と題する技術報告書を公開しました。この報告書はRust、Go、Python、Swift、Java、C#などのメモリ安全言語への移行を推奨し、C/C++に依存した開発慣行からの脱却を呼びかけています(出典: White House ONCD)。
NSA/CISAの共同ガイダンス(2025年)
米国家安全保障局(NSA)とサイバーセキュリティ・インフラストラクチャセキュリティ庁(CISA)は、共同でサイバーセキュリティ情報シートを公開し、AdaとRustをメモリ安全言語として明示的に推奨しています。組み込みシステムやミッションクリティカルなソフトウェアにおいて、メモリ安全言語の採用がセキュリティ向上に直結するとされています。
Google Androidでの実績データ
Googleは2019年からAndroid開発にRustを段階的に導入しました。その結果、メモリ安全性に起因する脆弱性の割合が2019年の76%から2024年には24%に減少しています(出典: Google Security Blog)。2025年にはこの割合が20%を下回り、Rust導入部分ではC/C++コードと比較してメモリ安全性の脆弱性密度が1000分の1に低下しています(出典: Google Security Blog)。
開発効率の面でも、Rustコードのコードレビュー時間はC++と比較して約25%短縮され、中〜大規模な変更のロールバック率はC++の約4分の1です。
Linux kernelのRust正式採用
Linux kernelへのRust導入は2022年のバージョン6.1で実験的に開始されました。2025年12月の Kernel Maintainer Summitにて、RustはC・アセンブリに次ぐ正式なコア言語として承認されています(出典: LWN.net)。Android 16搭載デバイスでは、Rustで実装されたメモリアロケータが既に本番稼働しています。
実務でメモリ安全性を高めるツールと手法
Rustのコンパイル時チェックに加え、さらにメモリ安全性を強化するためのツールと設計パターンがあります。
Miri:未定義動作の実行時検出
Miriは、Rustの中間表現(MIR)を解釈実行し、unsafeコード内の未定義動作を検出するインタプリタです。
# Miriのインストール
rustup +nightly component add miri
# テストをMiriで実行
cargo +nightly miri test
borrow checkerが検出できないunsafeブロック内の問題(不正なポインタ操作、アライメント違反、不正なenum値の構築など)をMiriが補完します。CI/CDパイプラインに組み込むことで、unsafeコードを含むクレートの品質を継続的に検証できます。
Clippyによる安全なコーディング支援
ClippyはRust公式のlintツールで、メモリ安全性に関連する警告も提供します。
cargo clippy -- -W clippy::all
不要なclone()の呼び出し、unsafeブロックの不適切な使用、参照のライフタイムに関する改善提案などを検出します。
newtypeパターンによる型レベルの安全性
型システムを活用してビジネスロジックの誤りを防ぐ設計パターンも、広い意味でのメモリ安全性に寄与します。
struct UserId(u64);
struct OrderId(u64);
fn find_user(id: UserId) -> Option<String> {
// UserId と OrderId を取り違えるとコンパイルエラー
Some(format!("User {}", id.0))
}
fn main() {
let user_id = UserId(42);
// let order_id = OrderId(100);
// find_user(order_id); // コンパイルエラー:型が異なる
let user = find_user(user_id);
println!("{:?}", user);
}
まとめ
Rustのメモリ安全性は、所有権・借用・ライフタイムの3つの仕組みにより、GCなしでコンパイル時にメモリ関連バグを排除する点に本質があります。米国政府がONCDやNSA/CISAを通じてメモリ安全言語の採用を推進し、Google Androidではメモリ安全性の脆弱性が76%から20%未満に減少するなど、実運用での成果も確認されています。
一方で、unsafeコードの存在やメモリリークの非保証など、Rustの「安全」が万能ではない点も正確に理解する必要があります。borrow checkerによるコンパイル時検証に加え、Miriやclippyなどのツールを活用することで、実務におけるメモリ安全性をさらに高められます。
