本文へスキップ
株式会社オープンナレッジ
株式会社オープンナレッジ Open Knowledge Inc.
TECHNICAL MONOGRAPH — LANGUAGE-BASED SECURITY & SANDBOXING W3C WebAssembly 2.0 / WASI 0.2 / WIT Canonical ABI Rust Edition 2024 / SFI & Capability Theory

WebAssembly Component Model (WASI 0.2) 線形メモリ境界分離と Capability-Based Security の数理検証

著者:加藤 明生(株式会社オープンナレッジ 代表取締役CTO)

1. 基本と全体像

この記事の要点と4つの章のご案内

要旨と脅威モデル:なぜ従来の共有メモリ型プロセスはサプライチェーン侵害に脆弱なのかに関するイメージ写真
要旨と脅威モデル:なぜ従来の共有メモリ型プロセスはサプライチェーン侵害に脆弱なのか
線形メモリ境界検査・仮想メモリガードページ・Canonical ABI の数理定式化に関するイメージ写真
線形メモリ境界検査・仮想メモリガードページ・Canonical ABI の数理定式化
WebAssembly Component Model (WASI 0.2) 線形メモリ境界分離と Capability-Based Security の数理検証 の各章におけるテーマと要点一覧表
章構成 主なテーマ 要点と確認ポイント
1. 基本と全体像 要旨と脅威モデル:なぜ従来の共有メモリ型プロセスはサプライチェーン侵害に脆弱なのか 一般的なLinuxコンテナはホストOSのシステムコール境界(数百個のsyscall)を共有し、起動に数十ミリ秒〜数秒と数十MBのメモリを要します。一方、WebAssembly Component Model(WASI 0.2)は、(1) 64KiBページ単位の線形メモリ境界検査と仮想メモリガードページ(PROT_NONE)によるハードウェア隔離、(2) デフォルトでファイル・ソケット・環境変数へのアクセス権を一切持たないCapability-Based Security(権限ベース安全設計)、および (3) 数十マイクロ秒でのコールドスタートを実現します。
2. 現場での進め方と対策 線形メモリ境界検査・仮想メモリガードページ・Canonical ABI の数理定式化 はい。従来のC/C++やNode.js、Python等のランタイムでは、同一プロセス内のすべての依存パッケージが同じメモリ空間とOS環境変数(データベース接続文字列やAPI秘密鍵)を共有する「Ambient Authority(暗黙の全権限)」構造になっていました。Component Modelでは各モジュールが独立した線形メモリ空間(Shared-Nothing)を持ち、Canonical ABIによる型安全な値コピーのみが許可されるため、別コンポーネントのメモリ盗み見や外部通信は構造的に不可能です。
3. 関連する制度とルール WASI 0.2 リソースハンドル表と権限減衰(Capability Attenuation)の形式検証 Rust 2024製のホストランタイムにおいて、WASM命令ブロックごとに決定論的な燃料カウンタ(Fuel Instrumentation)を挿入しています。1回の呼び出しあたりに消費可能な命令数上限(Fuel Budget)と線形メモリ拡張上限(N_max Pages)を厳密にバインドし、上限超過時はホストプロセスに影響を与えずマイクロ秒単位で該当インスタンスのみを安全に中断(Trap)させます。
4. 詳しい技術仕様としくみ WASI 0.2 世代付き Capability ハンドル表・線形メモリ境界検査・Fuel 決定論的実行制御の Rust 2024 実装 著者:加藤 明生(株式会社オープンナレッジ 代表取締役CTO)
第1章〜第4章では、実務チェックリスト・公的制度基準・詳しい技術仕様を確認できます。
SECTION 01 — ABSTRACT & AMBIENT AUTHORITY THREAT MODEL

1. 要旨と脅威モデル:なぜ従来の共有メモリ型プロセスはサプライチェーン侵害に脆弱なのか

現代のソフトウェアサプライチェーン攻撃(NPM / PyPI / Crates.io 等における依存パッケージ侵害)が甚大な被害をもたらす根本原因は、POSIX互換OSプロセスの「Ambient Authority(暗黙の包括的権限)」と「単一フラット仮想アドレス空間」にあります。たとえば、日付文字列の整形やPDF帳票のレイアウト計算を行うだけの小さなサードパーティ製ライブラリであっても、ひとたびWebアプリケーションと同じOSプロセス内でリンクされると、そのプロセスが保持するすべての環境変数(DATABASE_URL や署名用秘密鍵)、ヒープメモリ上の他テナントの平文データ、および外向きTCPソケット発行権限(connect(2))へ無制限にアクセスできてしまいます。

この構造的欠陥をコンパイル時および命令セット(ISA)レベルで解消するのが、W3C WebAssembly 2.0 および Bytecode Alliance が策定した WebAssembly Component Model(WASI 0.2 / Preview 2) です。当社のエッジ基盤(ap01.ok.st)およびブラウザ内ローカル解析エンジン(PII自動マスキング・8ch表面筋電位STFT演算・契約書構造検証)では、すべてのプラグインおよび非信頼データパーサを独立した WebAssembly コンポーネントとして分離実行しています。

本論文では、(1) 64KiBページ境界と仮想メモリガードページによるソフトウェア・フォルト・アイソレーション(SFI)の数理、(2) ポインタ共有を一切許さない Canonical ABI の Lifting / Lowering 変換規則、および (3) 偽造不可能なハンドルインデックスに基づく Capability-Based Security の形式証明と Rust 2024 実装を提示します。

ISOLATION DIMENSION 従来型同一プロセス依存(Node.js / PHP / Python) Linux Container(Docker / namespaces) WASM Component Model(WASI 0.2 Sandbox)
Memory Space 全ライブラリが同一ヒープを共有(任意メモリ読取可) コンテナ単位で分離(同一コンテナ内は共有) コンポーネント単位の Shared-Nothing 線形メモリ
Authority Model Ambient Authority(全環境変数・全ソケットへ到達可) Linux Capabilities / seccomp-bpf(約300 syscall) Fine-Grained Handle Capability(初期権限 = 空集合 ∅)
Control-Flow Integrity JIT Spraying / ROP / JOP のリスクあり ゲストバイナリ内のROPはネイティブ同様に成立 構造化制御フロー + ホスト管理 Call Stack 分離
Cold-Start & Footprint 数十ms 〜 数百ms / 30MB〜150MB 150ms 〜 数秒 / 数十MB〜数百MB 50µs 以下(事前AOTコンパイル時)/ 数十KiB〜数MB
SECTION 02 — LINEAR MEMORY BOUNDS & CANONICAL ABI SEMANTICS

2. 線形メモリ境界検査・仮想メモリガードページ・Canonical ABI の数理定式化

WebAssembly の各コアモジュールインスタンス mi は、ホストのポインタ空間から完全に切り離されたバイト配列としての線形メモリ(Linear Memory) Mi ∈ {0, …, 255}Li(ただし Li = Npages × 65,536 バイト)のみを参照できます。実行スタック(オペランドスタックおよび関数リターンアドレス)は線形メモリ Mi の外側のホスト管理領域に配置されるため、線形メモリ内のバッファオーバーフローによって関数リターンアドレスを書き換える古典的なスタック・スマッシング(ROP攻撃)は原理的に成立しません。

Theorem 1 (Spatial Memory Safety & 4GiB + Guard Page SFI Invariant):
∀ load/store(u32_addr, u32_offset, width) in Module mi :
EA = uint64(u32_addr) + uint64(u32_offset)   ∈ [0, 233 − 2]
ValidAccess(EA, width) ⇔ (EA + width ≤ Npages × 65,536) ∧ (Npages ≤ Nmax_budget)
HostVA(EA) = BaseVA(Mi) + EA,   where [BaseVA(Mi) + Li, BaseVA(Mi) + 8GiB) is mapped PROT_NONE

64ビットホストCPU(x86_64 / AArch64)上では、各線形メモリの基底仮想アドレス BaseVA(Mi) から 4GiB の実効領域と 4GiB のハードウェア・ガードページ領域(mmap(..., PROT_NONE)、物理RAM消費はゼロ)を予約することで、32ビット基底アドレスと32ビット静的オフセットの和 EA が萬一境界を超過しても、明示的な比較分岐命令なしにCPUのMMUページフォルト(SIGSEGV / SIGBUS → WASM Trap変換)で即座に捕捉されます。

Component Model Canonical ABI:Lifting と Lowering による Shared-Nothing 合成

WebAssembly Component Model(WIT: Wasm Interface Types)では、コンポーネント CA とコンポーネント CB は互いの線形メモリ MA, MB へのポインタを一切共有できません。CA が CB のエクスポート関数を呼び出す際、ランタイムは CA の線形メモリ表現から高水準の正規形値(Abstract Value v ∈ ValWIT)へとLift(抽象化・UTF-8妥当性検証・境界検査)し、続いて CB の再確保関数 cabi_realloc を呼び出して CB 側の独立メモリ MB へとLower(具象化コピー)します。

Definition 2 (Canonical ABI Non-Interference & Deep-Copy Morphism):
CallCA → CB(ptrA, lenA) = LowerMB( LiftMA(MA[ptrA .. ptrA + lenA]) )
Disjointness :   AddrSpace(MA) ∩ AddrSpace(MB) = ∅   ⇒   Zero TOCTOU / Zero Dangling Pointer Aliasing
FIGURE 1: WASI 0.2 COMPONENT MODEL SHARED-NOTHING LINEAR MEMORY ISOLATION & CANONICAL ABI LIFT/LOWER
01 GUEST COMPONENT A (UNTRUSTED) Linear Memory M_A (Isolated) 0 ≤ EA + len ≤ N_A × 64KiB + 4GiB PROT_NONE Guard Zone Local Handle Table T_A idx=1 → ReadOnlyStream (Scoped) Network / FS Root = UNMAPPED (∅) Fuel Execution Bounding FuelRemaining -= Cost(BasicBlock) 燃料枯渇時は0.01msで安全にTrap終了 02 CANONICAL ABI & HOST KERNEL 1. Canonical Lift (from M_A) Validate UTF-8 / Enum Discriminant 生ポインタを抽象WIT型へ安全に変換 2. Capability Handle Attenuation Verify(HandleIdx, RightsMask) 世代番号(Generation)付き偽造防止 3. Canonical Lower (to M_B) Invoke cabi_realloc in M_B 03 COMPONENT B (VERIFIED) Linear Memory M_B M_A ∩ M_B = ∅ (Disjoint) M_Aからの直接メモリアクセス不可 Security Guarantees ・サプライチェーンRCE封じ込め ・環境変数・秘密鍵の盗取不能 ・未認可システムコール = 0件 ・ブラウザ/エッジ両環境で同一バイナリ Zero Ambient Authority
SECTION 03 — WASI 0.2 HANDLE TABLE & CAPABILITY ATTENUATION

3. WASI 0.2 リソースハンドル表と権限減衰(Capability Attenuation)の形式検証

WASI 0.2(Preview 2)において、ファイルディスクリプタ、HTTP送信クライアント、乱数生成器などの外部リソースはすべて WIT の resource 型(偽造不可能な所有権付きハンドル)として表現されます。ゲストコンポーネントが受け取るハンドル値 h ∈ ℕ32 は、ホスト空間のポインタではなく、そのコンポーネントインスタンス専用のローカルハンドルテーブル Ti への世代付きインデックス(Generational Arena Index)です。

Theorem 2 (Capability Confinement & Monotonic Attenuation Invariant):
∀ Component mi, ∀ HostResource r ∉ Domain(Ti) :   Reachable(mi, r) = False
DeriveHandle(hparent, Rightsreq) :   Rights(hchild) = Rights(hparent) ∩ Rightsreq   ⊆ Rights(hparent)
DropHandle(h)   ⇒   Generation(Ti[idx(h)]) ← Generation(Ti[idx(h)]) + 1   (Eliminates Use-After-Close)

これにより、以下の3つのセキュリティ不変条件が数学的に成立します。第1に「権限の無からの生成不能性(No Fabrication)」:ゲストが任意の32ビット整数を捏造してホスト関数に渡しても、Ti 内の世代番号(Generation Counter)および型タグと一致しない限り即時 Trap となります。第2に「単調権限減衰(Monotonic Attenuation)」:親ハンドルから子ハンドルを派生させる際、権限ビットマスクは部分集合(Rightschild ⊆ Rightsparent)にしかなり得ず、権限昇格は構造的に起こりません。第3に「借用(borrow<T>)と所有(own<T>)のライフタイム追跡」:アクティブな借用ハンドルが存在する間に親所有ハンドルを破棄(Drop)しようとした場合、ホストテーブルが即座にプロトコル違反を検知して実行を遮断します。

WASI 0.2 INTERFACE DEFAULT SANDBOX POLICY 権限減衰と境界制御仕様(Host Enforcement)
wasi:filesystem/types DENY ALL (No Preopens) ホストのファイルシステムツリーは一切マウントせず、必要な入力はインメモリ wasi:io/streams としてのみ注入
wasi:sockets/tcp DENY ALL (Zero Raw Socket) 生のTCP/UDPソケット生成を確実に遮断し、サプライチェーン侵入時のC2サーバ通信(Reverse Shell)を無力化
wasi:http/outgoing-handler ALLOWLISTED ORIGIN ONLY 外部連携が必要なコンポーネントにのみ、ホスト署名済みオリジンリストに一致するHTTPSリクエストのみを中継
wasi:clocks/monotonic-clock COARSE RESOLUTION (1ms) ナノ秒単位の高精度タイマーを1ミリ秒粒度に量子化し、Spectre等のキャッシュタイミング計測攻撃を阻止
SECTION 04 — PRODUCTION RUST 2024 IMPLEMENTATION

WASI 0.2 世代付き Capability ハンドル表・線形メモリ境界検査・Fuel 決定論的実行制御の Rust 2024 実装

Rust Edition 2024 / #![no_std] Compatible Core
Memory Isolation 64KiB Pages + Guard Zone
Handle Security Generational Capability Table
Termination Proof Deterministic Fuel Metering
Ambient Syscalls 0 (Strict Deny-by-Default)
src/wasm/wasi02_capability_sandbox.rs
Rust Edition 2024 — WASI 0.2 Handle Table, Canonical ABI Lift/Lower & Fuel Guard
#![forbid(unsafe_code)]

pub const WASM_PAGE_BYTES: u64 = 65_536;
pub const MAX_COMPONENT_HANDLES: usize = 256;

/// Fine-grained WASI 0.2 capability rights bitmask.
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CapabilityRights(pub u32);

impl CapabilityRights {
    pub const NONE: Self = Self(0);
    pub const STREAM_READ: Self = Self(1 << 0);
    pub const STREAM_WRITE: Self = Self(1 << 1);
    pub const HTTP_OUTGOING_GET: Self = Self(1 << 2);
    pub const MONOTONIC_CLOCK: Self = Self(1 << 3);

    #[inline]
    pub const fn contains(self, required: Self) -> bool {
        (self.0 & required.0) == required.0
    }

    /// Monotonic capability attenuation: child rights can never exceed parent rights.
    #[inline]
    pub const fn attenuate(self, mask: Self) -> Self {
        Self(self.0 & mask.0)
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ResourceKind {
    MemoryInputStream,
    MemoryOutputStream,
    ScopedHttpEndpoint { allowed_origin_hash: u64 },
}

/// Unforgeable generational handle passed across the Canonical ABI boundary.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GuestHandle {
    pub slot_index: u16,
    pub generation: u16,
}

#[derive(Clone, Copy, Debug)]
struct HandleSlot {
    generation: u16,
    occupied: bool,
    active_borrows: u16,
    kind: ResourceKind,
    rights: CapabilityRights,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SandboxTrap {
    OutOfBoundsLinearMemory { effective_addr: u64, limit_bytes: u64 },
    MemoryPageQuotaExceeded { requested_pages: u32, max_pages: u32 },
    FuelExhausted { budget: u64 },
    InvalidOrForgedHandle,
    UseAfterDropGenerationMismatch,
    CapabilityRightsDenied { required: CapabilityRights, actual: CapabilityRights },
    CannotDropBorrowedResource { active_borrows: u16 },
    HandleTableCapacityExhausted,
    InvalidUtf8CanonicalLift,
}

/// Isolated WASI 0.2 Component Instance State (Shared-Nothing Memory + Handle Table + Fuel).
pub struct WasiComponentSandbox {
    linear_memory: Vec<u8>,
    current_pages: u32,
    max_pages: u32,
    fuel_remaining: u64,
    initial_fuel_budget: u64,
    handle_table: [HandleSlot; MAX_COMPONENT_HANDLES],
}

impl WasiComponentSandbox {
    pub fn new(initial_pages: u32, max_pages: u32, fuel_budget: u64) -> Result<Self, SandboxTrap> {
        if initial_pages > max_pages {
            return Err(SandboxTrap::MemoryPageQuotaExceeded {
                requested_pages: initial_pages,
                max_pages,
            });
        }
        let byte_len = (initial_pages as usize) * (WASM_PAGE_BYTES as usize);
        let empty_slot = HandleSlot {
            generation: 1,
            occupied: false,
            active_borrows: 0,
            kind: ResourceKind::MemoryInputStream,
            rights: CapabilityRights::NONE,
        };
        Ok(Self {
            linear_memory: vec![0u8; byte_len],
            current_pages: initial_pages,
            max_pages,
            fuel_remaining: fuel_budget,
            initial_fuel_budget: fuel_budget,
            handle_table: [empty_slot; MAX_COMPONENT_HANDLES],
        })
    }

    /// Consumes deterministic instruction fuel; traps immediately on exhaustion.
    #[inline]
    pub fn consume_fuel(&mut self, instructions: u64) -> Result<(), SandboxTrap> {
        if self.fuel_remaining < instructions {
            self.fuel_remaining = 0;
            return Err(SandboxTrap::FuelExhausted {
                budget: self.initial_fuel_budget,
            });
        }
        self.fuel_remaining -= instructions;
        Ok(())
    }

    /// Verifies W3C WebAssembly 2.0 spatial bounds: 0 <= addr + offset + len <= N_pages * 65536.
    #[inline]
    pub fn verify_memory_slice(
        &self,
        base_addr: u32,
        static_offset: u32,
        byte_len: u32,
    ) -> Result<&[u8], SandboxTrap> {
        let effective_addr = (base_addr as u64) + (static_offset as u64);
        let end_addr = effective_addr + (byte_len as u64);
        let limit_bytes = (self.current_pages as u64) * WASM_PAGE_BYTES;

        if end_addr > limit_bytes || end_addr > (self.linear_memory.len() as u64) {
            return Err(SandboxTrap::OutOfBoundsLinearMemory {
                effective_addr: end_addr,
                limit_bytes,
            });
        }
        Ok(&self.linear_memory[(effective_addr as usize)..(end_addr as usize)])
    }

    /// Canonical ABI Lift for UTF-8 strings across Shared-Nothing component boundary.
    pub fn canonical_lift_utf8(
        &mut self,
        ptr: u32,
        len: u32,
    ) -> Result<String, SandboxTrap> {
        self.consume_fuel(16 + (len as u64))?;
        let slice = self.verify_memory_slice(ptr, 0, len)?;
        core::str::from_utf8(slice)
            .map(str::to_owned)
            .map_err(|_| SandboxTrap::InvalidUtf8CanonicalLift)
    }

    /// Grants a scoped resource capability into the component's local handle table.
    pub fn insert_capability(
        &mut self,
        kind: ResourceKind,
        rights: CapabilityRights,
    ) -> Result<GuestHandle, SandboxTrap> {
        for (idx, slot) in self.handle_table.iter_mut().enumerate() {
            if !slot.occupied {
                slot.occupied = true;
                slot.active_borrows = 0;
                slot.kind = kind;
                slot.rights = rights;
                return Ok(GuestHandle {
                    slot_index: idx as u16,
                    generation: slot.generation,
                });
            }
        }
        Err(SandboxTrap::HandleTableCapacityExhausted)
    }

    /// Attenuates an existing handle into a restricted child handle (Rights_child <= Rights_parent).
    pub fn attenuate_handle(
        &mut self,
        parent: GuestHandle,
        mask: CapabilityRights,
    ) -> Result<GuestHandle, SandboxTrap> {
        let (kind, parent_rights) = self.verify_capability(parent, CapabilityRights::NONE)?;
        let child_rights = parent_rights.attenuate(mask);
        self.insert_capability(kind, child_rights)
    }

    /// Validates handle index, generation counter, and required capability rights bitmask.
    pub fn verify_capability(
        &self,
        handle: GuestHandle,
        required: CapabilityRights,
    ) -> Result<(ResourceKind, CapabilityRights), SandboxTrap> {
        let idx = handle.slot_index as usize;
        if idx >= MAX_COMPONENT_HANDLES {
            return Err(SandboxTrap::InvalidOrForgedHandle);
        }
        let slot = &self.handle_table[idx];
        if !slot.occupied {
            return Err(SandboxTrap::UseAfterDropGenerationMismatch);
        }
        if slot.generation != handle.generation {
            return Err(SandboxTrap::UseAfterDropGenerationMismatch);
        }
        if !slot.rights.contains(required) {
            return Err(SandboxTrap::CapabilityRightsDenied {
                required,
                actual: slot.rights,
            });
        }
        Ok((slot.kind, slot.rights))
    }

    /// Drops an owned capability handle and increments slot generation to prevent Use-After-Close.
    pub fn drop_capability(&mut self, handle: GuestHandle) -> Result<(), SandboxTrap> {
        let idx = handle.slot_index as usize;
        if idx >= MAX_COMPONENT_HANDLES {
            return Err(SandboxTrap::InvalidOrForgedHandle);
        }
        let slot = &mut self.handle_table[idx];
        if !slot.occupied || slot.generation != handle.generation {
            return Err(SandboxTrap::UseAfterDropGenerationMismatch);
        }
        if slot.active_borrows > 0 {
            return Err(SandboxTrap::CannotDropBorrowedResource {
                active_borrows: slot.active_borrows,
            });
        }
        slot.occupied = false;
        slot.rights = CapabilityRights::NONE;
        slot.generation = slot.generation.wrapping_add(1).max(1);
        Ok(())
    }
}
ご質問・公的制度の活用に関するお問い合わせ窓口

掲載資料に関するご質問・公的制度活用のご案内

本ページの図解や解説内容に関するご質問、経済産業省「SCS評価制度」やIPA「SECURITY ACTION・サイバーセキュリティお助け隊サービス」等の公的制度の確認方法について、落ち着いた参考窓口としてご案内しております。売り込みや営業連絡は一切行いませんので、社内勉強会やご家庭での確認資料としてご自由にご活用ください。

図解・ビジュアルの拡大表示

図表内のテキストもそのまま選択・検索可能です(ベクターSVG描画) 枠外クリックまたは ESC キーで閉じます