アフィン型システム(Affine Type System)と借用検査による CWE-78 / CWE-89 / CWE-416 のコンパイル時消去証明
著者:加藤 明生(株式会社オープンナレッジ 代表取締役CTO)
この記事の要点と4つの章のご案内
| 章構成 | 主なテーマ | 要点と確認ポイント |
|---|---|---|
| 1. 基本と全体像 | 要旨と脅威モデル:なぜ「可変エイリアシング」と「文字列連結」が脆弱性の大宗を生むのか | Microsoft、Google(Chromium / Android)、および米国CISAの統計調査により、C/C++で記述されたシステムにおける深刻なセキュリティ脆弱性(CVE)の一貫して約70%が、解放後メモリ使用(Use-After-Free: CWE-416)や境界外書き込み(Out-of-Bounds Write: CWE-787)などのメモリ安全性欠如に起因することが判明しているためです。Rustはガベージコレクタ(GC)の停止時間を伴わずに、コンパイル時のアフィン型システムと借用検査(Borrow Checker)によってこれらの脆弱性をビルド段階で拒絶します。 |
| 2. 現場での進め方と対策 | 部分構造論理(アフィン型)と「エイリアシング XOR 可変性」の形式証明 | はい。インジェクション脆弱性の本質は「信頼できないユーザー入力文字列」と「実行可能な命令構文(SQLクエリやシェルコマンド)」を単一の文字列型(String)として連結してしまうこと(Untyped String Concatenation)にあります。Rust 2024においてPhantomDataを用いた型状態機械(Typestate Pattern)と、コンパイル時構文木束縛(SQLxプリペアドステートメントやシェルを介さないexecve引数配列型)を強制することで、生の文字列連結によるクエリ・コマンド実行自体をコンパイルエラーとして排除できます。 |
| 3. 関連する制度とルール | PhantomData 型状態機械による CWE-78 / CWE-89 の消去と4言語定量比較 | GC付き言語はヒープの解放時期を実行時のガベージコレクタに委ねるため、高負荷時に数ミリ秒〜数十ミリ秒のStop-The-World(またはコンカレントGCによるテールレイテンシ悪化)と2〜5倍の余剰メモリ消費が発生します。また、マルチスレッド環境でのデータ競合(Data Race: CWE-362)はGCでは防げません。Rustは変数のスコープ終了時に決定論的(RAII / OBRA)にメモリを解放し、かつSend / Syncトレイト境界によりデータ競合もコンパイル時に構造的に排除します。 |
| 4. 詳しい技術仕様としくみ | なぜ米国CISA・NSA・FBIや欧州セキュリティ機関は、メモリ安全言語(Rust等)への移行を強く勧告しているのですか? | 著者:加藤 明生(株式会社オープンナレッジ 代表取締役CTO) |
1. 要旨と脅威モデル:なぜ「可変エイリアシング」と「文字列連結」が脆弱性の大宗を生むのか
過去20年間に報告された深刻度 High / Critical の共通脆弱性識別子(CVE)を言語ランタイム別に分類すると、2つの明確な構造的病理が浮かび上がります。第1は、CおよびC++で記述されたOSカーネル、ブラウザエンジン、ネットワークデーモンにおける時間的・空間的メモリ安全性違反(Use-After-Free: CWE-416、Double Free: CWE-415、Out-of-Bounds Write: CWE-787、Data Race: CWE-362)であり、全深刻脆弱性の約70%を占めます。これらはすべて、同一のメモリ領域(ロケーション ℓ)を指す複数のポインタ(エイリアス)が存在する状態で、一方のポインタを通じて領域の解放や再確保(Mutation)が行われることに起因します。
第2の病理は、PHP、Python、Ruby、あるいは動的文字列組み立てを多用するWebアプリケーション層における構文インジェクション脆弱性(OS Command Injection: CWE-78、SQL Injection: CWE-89、Cross-Site Scripting: CWE-79)です。これらは、外部から流入した未検証のバイト列(Untrusted Input)と、インタプリタが解釈すべき制御構造(SQL構文木やPOSIXプロセスの引数ベクタ)が、型システム上で区別されず単一の string 型へと暗黙変換・連結されることに起因します。
本論文では、部分構造論理(Substructural Logic)におけるアフィン型システム(Affine Type System)と領域推論(Region Inference / Polonius Borrow Checker)、およびゼロサイズ型 PhantomData<State> を用いた型状態機械(Typestate Pattern)を結合することで、実行時オーバーヘッドを一切課すことなく、CWE-416 / CWE-787 / CWE-362 / CWE-78 / CWE-89 をコンパイル時に数学的に消去する証明と実装を示します。
2. 部分構造論理(アフィン型)と「エイリアシング XOR 可変性」の形式証明
古典命題論理および一般的なプログラミング言語の型システムは、文脈 Γ 内の仮定(変数 x : T)を何度でも複製して使える縮約規則(Contraction Rule)と、使わずに破棄できる弱化規則(Weakening Rule)の両方を無制限に許容します。これに対し、Rust の所有権型システムは、弱化(スコープ終了時の決定論的 Drop)は許容するが暗黙の縮約(複製)を禁止するアフィン論理(Affine Logic:各リソースは高々1回しか消費できない)に立脚しています。
所有権を毎回移動(Move)するだけでは実務上の参照処理が煩雑になるため、Rust はライフタイム領域(Region)'a を伴う不変借用(Shared Reference &'a T)と排他可変借用(Unique Mutable Reference &'a mut T)を導入します。コンパイラの借用検査器(MIR Borrow Checker)は、制御フローグラフ(CFG)上のすべてのプログラム地点 p ∈ CFG およびすべてのメモリロケーション ℓ ∈ Locations に対して、以下の不変条件(Aliasing XOR Mutability)を強制します。
不変条件 (b) により、所有者 Owner(ℓ) の生存期間 Lifetime(Owner(ℓ)) を超えて参照 r1 が生き残ること(Dangling Pointer)は集合包含関係の違反としてコンパイル時に100%拒絶されます。したがって、解放済みメモリへの参照(CWE-416 Use-After-Free)は #![forbid(unsafe_code)] 下で数学的に空集合(発生確率 0)となります。同様に、不変条件 (a) により、同一ロケーションに対する書き込み参照と別スレッドからの読み書き参照が同時にアクティブになることは不可能であり、CWE-362(マルチスレッド・データ競合)もコンパイル時に消去されます。
3. PhantomData 型状態機械による CWE-78 / CWE-89 の消去と4言語定量比較
アフィン型システム(Move Semantics)とゼロサイズ型マーカー core::marker::PhantomData<S> を組み合わせると、オブジェクトの遷移状態 S ∈ {Unvalidated, Sanitized, Parameterized, Executed} を実行時メモリ消費 0 バイトのままコンパイル時の型シグネチャへ埋め込むことができます。
format!("SELECT ... {}", user_input) has lifetime 'local ≠ 'static (Compile-Time Type Error)execve(path, argv[], envp[]) without shell lexer interpretation (; | & $() inert)
第一に、SQLクエリの骨格文字列に静的ライフタイム &'static str を要求することで、ユーザー入力を format!() で動的に結合した String(ヒープ上の動的寿命)を渡そうとした瞬間にライフタイム不一致でコンパイルが失敗します。すべての動的パラメータは PostgreSQL 拡張クエリプロトコル(Frontend/Backend Protocol v3.0 `Bind` メッセージ)の型付きバイナリ引数スロットへ分離送信されるため、SQLインジェクション(CWE-89)は構文解析の原理上100%発生し得ません。第二に、外部プロセス起動ラッパにおいて sh -c の呼び出しを禁止し、バイナリパスと独立した引数ベクタ argv[1..n] のみを取る型状態遷移に限定することで、セミコロンやバッククォートを含む入力が与えられても単なる文字列リテラル引数として処理され、OSコマンドインジェクション(CWE-78)も完全に消滅します。
| EVALUATION METRIC | C / C++ (Manual Memory) | PHP 8.x / Dynamic Scripting | Go (Concurrent GC) | Rust Edition 2024 (Affine + NLL) |
|---|---|---|---|---|
| CWE-416 Use-After-Free | 頻発(全重大CVEの約70%) | C拡張モジュール内で発生 | GCにより保護(実行時コスト伴う) | コンパイル時に100%消去(0件) |
| CWE-362 Thread Data Race | 未定義動作(UB)・メモリ破壊 | プロセス分離(共有状態なし) | Map/Slice並列書込で実行時Panic | Send / Sync 境界でコンパイル時拒絶 |
| CWE-89 / CWE-78 Injection | sprintf / system() で頻発 | 文字列連結・プラグインで多発 | 実行時チェックに依存 | &'static str + Typestate でコンパイル時阻止 |
| P99.9 Tail Latency (GC Pause) | 0 ms(決定論的) | 15 ms 〜 120 ms(リクエスト毎再生成) | 0.5 ms 〜 8.0 ms(GC Write Barrier) | 0 ms(OBRA スコープ即時解放) |
| Idle Memory Footprint | 2 MB 〜 8 MB | 64 MB 〜 256 MB (FPM Pool) | 18 MB 〜 45 MB (GC Heap Reserve) | 2.4 MB 〜 6.5 MB (Static Native Binary) |
アフィン型で検証されたRustコードをさらにWASI 0.2権限テーブルで隔離する設計。
コンパイル時バグ消去が運用フェーズの緊急パッチ工数をどう削減するかの定量分析。
Typestateトランザクションガードと連携するデータベース行レベル分離仕様。
PhantomData 型状態 SQL/Command 検証器と世代付きアリーナアロケータ(CWE-416 消去証明)の Rust 2024 実装
#![forbid(unsafe_code)]
use core::marker::PhantomData;
// ============================================================================
// PART 1: Compile-Time Elimination of CWE-89 (SQL Injection) via Typestate
// and `&'static str` Structural Query Template Enforcement
// ============================================================================
pub struct UnboundQuery;
pub struct TenantScoped;
pub struct ReadyToExecute;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SqlParam {
Uuid([u8; 16]),
Int64(i64),
StrictText(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SecurityViolation {
NullByteInArgument,
ControlCharInIdentifier,
ArgumentCountExceeded,
UseAfterFreeEpochMismatch { expected_epoch: u64, handle_epoch: u64 },
InvalidArenaSlot,
}
/// Zero-cost Typestate SQL Builder.
/// Dynamic `String` templates cannot be passed because `sql_template` requires `&'static str`.
pub struct SafeSqlQuery<State> {
sql_template: &'static str,
tenant_uuid: Option<[u8; 16]>,
bind_params: Vec<SqlParam>,
_state: PhantomData<State>,
}
impl SafeSqlQuery<UnboundQuery> {
/// Accepts ONLY compile-time string literals (`&'static str`), rendering
/// runtime string concatenation (`format!`) a compile-time lifetime error.
#[inline]
pub const fn from_static_template(sql_template: &'static str) -> Self {
Self {
sql_template,
tenant_uuid: None,
bind_params: Vec::new(),
_state: PhantomData,
}
}
/// Affine transition: consumes `UnboundQuery` and produces `TenantScoped`.
#[inline]
pub fn bind_rls_tenant(self, tenant_uuid: [u8; 16]) -> SafeSqlQuery<TenantScoped> {
SafeSqlQuery {
sql_template: self.sql_template,
tenant_uuid: Some(tenant_uuid),
bind_params: self.bind_params,
_state: PhantomData,
}
}
}
impl SafeSqlQuery<TenantScoped> {
pub fn push_param(mut self, param: SqlParam) -> Result<Self, SecurityViolation> {
if let SqlParam::StrictText(ref s) = param {
if s.as_bytes().contains(&0u8) {
return Err(SecurityViolation::NullByteInArgument);
}
}
self.bind_params.push(param);
Ok(self)
}
/// Affine transition: seals parameter list and produces `ReadyToExecute`.
#[inline]
pub fn seal(self) -> SafeSqlQuery<ReadyToExecute> {
SafeSqlQuery {
sql_template: self.sql_template,
tenant_uuid: self.tenant_uuid,
bind_params: self.bind_params,
_state: PhantomData,
}
}
}
impl SafeSqlQuery<ReadyToExecute> {
/// Callable ONLY when both RLS Tenant Context and Parameter Sealing are complete.
#[inline]
pub fn into_wire_parts(self) -> (&'static str, [u8; 16], Vec<SqlParam>) {
let tenant = self.tenant_uuid.unwrap_or([0u8; 16]);
(self.sql_template, tenant, self.bind_params)
}
}
// ============================================================================
// PART 2: Compile-Time Elimination of CWE-78 (OS Command Injection)
// by Forbidding Shell Invocation (`sh -c`) & Enforcing Direct Argv
// ============================================================================
pub struct VerifiedBinaryPath(&'static str);
impl VerifiedBinaryPath {
pub const ZFS_BIN: Self = Self("/sbin/zfs");
pub const PFCTL_BIN: Self = Self("/sbin/pfctl");
}
pub struct ShellFreeCommand {
program: VerifiedBinaryPath,
argv: Vec<String>,
}
impl ShellFreeCommand {
pub const fn new(program: VerifiedBinaryPath) -> Self {
Self {
program,
argv: Vec::new(),
}
}
/// Appends a single OS `execve` argument vector entry after rejecting NUL/control bytes.
/// Because no shell (`sh -c`) is ever spawned, metacharacters (`;`, `|`, `$(...)`) cannot execute.
pub fn arg_strict(mut self, raw_arg: &str) -> Result<Self, SecurityViolation> {
if self.argv.len() >= 16 {
return Err(SecurityViolation::ArgumentCountExceeded);
}
for &b in raw_arg.as_bytes() {
if b == 0 || b < 0x20 || b == 0x7F {
return Err(SecurityViolation::ControlCharInIdentifier);
}
}
self.argv.push(raw_arg.to_owned());
Ok(self)
}
#[inline]
pub fn inspect_execve_spec(&self) -> (&'static str, &[String]) {
(self.program.0, &self.argv)
}
}
// ============================================================================
// PART 3: Safe Generational Arena Allocator (Eliminates CWE-416 for Graph Pools)
// ============================================================================
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ArenaRef<T> {
index: usize,
epoch: u64,
_marker: PhantomData<T>,
}
struct ArenaEntry<T> {
epoch: u64,
value: Option<T>,
}
pub struct SafeGenerationalArena<T> {
entries: Vec<ArenaEntry<T>>,
}
impl<T> SafeGenerationalArena<T> {
pub const fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn allocate(&mut self, val: T) -> ArenaRef<T> {
for (idx, entry) in self.entries.iter_mut().enumerate() {
if entry.value.is_none() {
entry.epoch = entry.epoch.wrapping_add(1);
entry.value = Some(val);
return ArenaRef {
index: idx,
epoch: entry.epoch,
_marker: PhantomData,
};
}
}
let epoch = 1u64;
let index = self.entries.len();
self.entries.push(ArenaEntry {
epoch,
value: Some(val),
});
ArenaRef {
index,
epoch,
_marker: PhantomData,
}
}
pub fn deallocate(&mut self, handle: ArenaRef<T>) -> Result<T, SecurityViolation> {
let entry = self
.entries
.get_mut(handle.index)
.ok_or(SecurityViolation::InvalidArenaSlot)?;
if entry.epoch != handle.epoch || entry.value.is_none() {
return Err(SecurityViolation::UseAfterFreeEpochMismatch {
expected_epoch: entry.epoch,
handle_epoch: handle.epoch,
});
}
entry.epoch = entry.epoch.wrapping_add(1);
Ok(entry.value.take().unwrap())
}
}