本文へスキップ
株式会社オープンナレッジ
株式会社オープンナレッジ Open Knowledge Inc.
不変ストレージ工学・ランサムウェア即時復旧 OpenZFS / 128B blkptr_t / Fletcher-4 / ARC 対象:経営者様・医療/製造/物流管理者様・インフラ技術者様

OpenZFS の 128バイト blkptr_t マークル木・ARCキャッシュ・Copy-on-Write 不変スナップショットによる 0.1秒ランサムウェア復旧基盤

監修:加藤 明生(株式会社オープンナレッジ 創業者・代表取締役CTO / データベーススペシャリスト / 登録情報処理安全確保支援士(第007617号))

1. 基本と全体像

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

身近なたとえで見る仕組み:「黒板を消して書き直す記録」と「全ページに指紋を押して綴じる公証役場の台帳」に関するイメージ写真
身近なたとえで見る仕組み:「黒板を消して書き直す記録」と「全ページに指紋を押して綴じる公証役場の台帳」
現場で陥りやすい落とし穴と回避策:ZFSと不変バックアップ運用の3つの急所に関するイメージ写真
現場で陥りやすい落とし穴と回避策:ZFSと不変バックアップ運用の3つの急所
OpenZFS の 128バイト blkptr_t マークル木・ARCキャッシュ・Copy-on-Write 不変スナップショットによる 0.1秒ランサムウェア復旧基盤 の各章におけるテーマと要点一覧表
章構成 主なテーマ 要点と確認ポイント
1. 基本と全体像 身近なたとえで見る仕組み:「黒板を消して書き直す記録」と「全ページに指紋を押して綴じる公証役場の台帳」 一般的なファイル保存は「同じ場所への上書き」であるため、ランサムウェアに暗号化されると元のデータは失われますが、OpenZFSは「既存の場所には一切触れず、常に空き領域へ書き足す(Copy-on-Write)」方式をとります。
2. 現場での進め方と対策 現場で陥りやすい落とし穴と回避策:ZFSと不変バックアップ運用の3つの急所 すべてのデータは128バイトの管理票(blkptr_t)と256ビットのデジタル指紋で家系図(マークル木)のように結ばれており、1ビットの劣化や改ざんも自動で検知・修復します。
3. 関連する制度とルール 「毎日夜中に外付けハードディスクやNASへコピーしている」のと、ZFSのスナップショットは何が違うのですか? 過去の目次(Uberblock)を指し示す不変スナップショットと zfs hold ロックにより、万が一の感染時もデータのコピー待ち時間ゼロ(約0.1秒のポインタ切替)で感染前の状態へ巻き戻せます。
4. 詳しい技術仕様としくみ 万が一サーバーの管理者権限(root権限)が乗っ取られたら、スナップショットも消されてしまいませんか? 一般的なファイルコピーは、1テラバイトのデータがあれば毎回何時間もかけて別の場所へ複製し、しかもコピー先のドライブがパソコンや社内ネットワークとつながったままになっています。そのためランサムウェアに侵入されるとコピー先まで一緒に暗号化されてしまいます。一方、OpenZFSのスナップショットは「既存のデータを一切上書きせず、変更差分だけを別領域へ書き足す(Copy-on-Write)」仕組みのため、容量が何テラバイトあっても0.1秒で記録が完了し、過去の時点のデータは読み取り専用として保護されます。
第1章〜第4章では、実務チェックリスト・公的制度基準・詳しい技術仕様を確認できます。
この記事のポイント
  • 01 一般的なファイル保存は「同じ場所への上書き」であるため、ランサムウェアに暗号化されると元のデータは失われますが、OpenZFSは「既存の場所には一切触れず、常に空き領域へ書き足す(Copy-on-Write)」方式をとります。
  • 02 すべてのデータは128バイトの管理票(blkptr_t)と256ビットのデジタル指紋で家系図(マークル木)のように結ばれており、1ビットの劣化や改ざんも自動で検知・修復します。
  • 03 過去の目次(Uberblock)を指し示す不変スナップショットと zfs hold ロックにより、万が一の感染時もデータのコピー待ち時間ゼロ(約0.1秒のポインタ切替)で感染前の状態へ巻き戻せます。
場面別の実務確認ポイント(ご自身の目的に合わせてお読みいただけます) 全訪問者対応・3段階の構成
01 経営者・病院長・BCP(事業継続)責任者の方

なぜ従来のバックアップでは復旧に数週間〜数千万円かかってしまうのか、そして「コピーし直すのではなく時間を巻き戻す」ZFSがどのように事業停止を防ぐのかがわかります。

02 情シス・サーバー運用・総務担当の方

スナップショットの空き容量枯渇リスク、権限分離(zfs allow / zfs hold)、月次の定期検査(zpool scrub)など、現場で失敗しない運用設計の勘所を確認できます。

03 ストレージ技術者・OSカーネル研究者の方

後半の「第3章 Monograph」にて、128バイト blkptr_t のビットフィールド配置、Fletcher-4巡回和の数理、128スロットUberblockリングバッファ、Rust製検証器を詳解しています。

第1章:Overview & Visual Architecture

身近なたとえで見る仕組み:「黒板を消して書き直す記録」と「全ページに指紋を押して綴じる公証役場の台帳」

一般的なハードディスクやクラウドの保存方式(NTFSやext4など)は、学校の「黒板」に似ています。昨日の売上データを今日の売上データに更新するとき、黒板消しで昨日の数字を消して、同じ場所に新しい数字を上書きします。もし夜中に悪意のある侵入者(ランサムウェア)が入ってきて、黒板の文字をすべて消して暗号に書き換えてしまったら、昨日の文字はもう二度と元に戻りません。

さらに問題なのは「バックアップ」です。黒板の内容を別のノート(外付けHDDやNAS)に書き写す作業は、データが大きくなると何時間もかかります。しかも復元するときには、何百ギガバイトものデータをもう一度最初から書き写し直す必要があるため、業務再開までに何日もかかってしまいます。

これに対して、私たちがデータベース基盤(db01.ok.st)に採用している「OpenZFS(オープン・ゼットエフエス)」は、「一度書いたページには一切触れず、変更があれば必ず新しい白紙のページに書き足し、表紙の『目次しおり』だけを付け替える」という動き方をします。15分前の目次しおり(スナップショット)は読み取り専用として保管されているため、万が一ウイルスが新しいページに落書きをしても、「目次しおりを15分前のページに戻すだけ(約0.1秒)」で、何百ギガバイトのデータが瞬時に元の姿へ蘇るのです。

ポイント 01 計算量の違い(O(1) ポインタ切替)
約 0.1 秒で世代記録

データを物理コピーするのではなく、木の根元(Uberblock)の参照先を固定するだけなので、データ量に比例せず一瞬でスナップショットが完了します。

ポイント 02 自己検証ハッシュ(Fletcher-4 / SHA-256)
256 ビットの指紋

すべてのデータブロックに対して256ビットの検査値を親ブロック側に記録し、ディスクの微細な磁気劣化(ビット腐敗)を100%自動検出します。

ポイント 03 高速メモリキャッシュ(ZFS ARC)
MRU + MFU 二重学習

「最近読まれたデータ」と「何回も繰り返し読まれる帳簿データ」を自動で学習・分離し、大量の一括検索が来てもキャッシュが消えない高速応答を維持します。

図解:OpenZFS のマークル木(親が子の256ビット指紋を持つ構造)と Copy-on-Write スナップショット
01 凍結された過去スナップショット(TXG #1040:不変) Uberblock (TXG #1040) zfs hold ロック済・書き換え不可 データブロック A (8KB) 親 blkptr_t が保持する指紋: cksum: 0x8F4A...29C1 ※新世代からもそのまま共有 旧データブロック B (8KB) 変更前の正常な顧客帳簿 birth_txg = 1040 上書きされず無傷で保存 02 変更発生時の Copy-on-Write(TXG #1041:新領域) 新 Uberblock (TXG #1041) 変更分だけ新ブロックへ連結 未変更ブロックは容量消費0 新データブロック B' (別セクタ) 空き領域へ新しく書き出し birth_txg = 1041 (CoW) 万一感染しても #1040 へ0.1秒復帰
比較項目一般的なファイルシステム(ext4 / NTFS)+ 夜間バックアップ当社標準:OpenZFS マークル木 + 不変スナップショット
データ更新時の動き元のブロックを直接上書きする(In-Place Overwrite)元のブロックには一切触れず、新しい空き領域へ書き出す(Copy-on-Write)
チェックサム(指紋)の保管場所データと同じブロック内(または検査なし)のため、丸ごと化けると検知不能親ブロックの blkptr_t 内に保持するため、ディスクの書き込み抜けも100%検知
1TBデータの巻き戻し時間ネットワークや外付け媒体から全量を書き戻すため数時間〜数日ルートポインタ(Uberblock)を切り替えるだけのため数秒で完了
メモリキャッシュの賢さ単純なLRU(最後に読まれた順)のため、1回の集計処理でキャッシュが消えるARC(MRU/MFU二重管理)により、頻出する帳簿インデックスを常時メモリ保持
第2章:Practical Field Guide & Self-Check

現場で陥りやすい落とし穴と回避策:ZFSと不変バックアップ運用の3つの急所

落とし穴 01:ランサムウェアが全ファイルを書き換えた結果、スナップショット差分が膨れ上がりプール容量が100%に達する

回避策:refreservation とデータセット単位の quota 設定、および 80% 満杯率の厳守

Copy-on-Write 方式の唯一の弱点は、「過去のデータを残したまま新しい書き込みを受け入れるため、万が一ウイルスが全ファイルを暗号化しようとすると、最大で元のデータと同じだけの空き領域を消費する」点です。また、ZFSはディスク全体の使用率が80%〜85%を超えると空き領域を探す方式が切り替わり、書き込み速度が低下します。そのため、当社ではデータセットごとに上限(quota)を設け、プール全体の空き領域を常時30%以上確保するとともに、異常な書き込み速度を検知した瞬間にプロセスを停止させて容量枯渇を防ぎます。

落とし穴 02:データベース(8KB単位)をZFSの初期設定(128KB単位)のまま動かしてしまい、16倍の無駄な読み書きが発生する

回避策:PostgreSQL 19 のページサイズに合わせて recordsize=8K と compression=lz4 を厳密に一致させる

OpenZFS の標準ブロックサイズ(recordsize)は 128KB ですが、PostgreSQL 19 はデータを 8KB のページ単位で書き換えます。もし 128KB のまま運用すると、たった1行の伝票を更新するたびに「128KBを読み出して、8KBを書き換え、再び128KB全体のチェックサムを計算して書き出す(Read-Modify-Write増幅)」という16倍の無駄な負荷がかかります。当社の db01.ok.st では、データベース領域に必ず recordsize=8K を指定し、物理I/Oの増幅を1.0倍(ゼロ増幅)に抑え込んでいます。

落とし穴 03:本番サーバーからバックアップサーバーへ「SSH鍵で自動ログインしてPush送信」する構成にしてしまう

回避策:保管庫側から取りに行く「Pull型レプリケーション」と zfs hold による削除ロックの順守

もし本番サーバーの中に「バックアップ先へログインできる秘密鍵」が置かれていたら、本番サーバーに侵入した攻撃者はその鍵を使ってバックアップ先にも侵入し、データを消去してしまいます。正しくは、矢印を逆向きにし、「外部から一切ログインできない隔離保管庫(Vault)側から、本番サーバーの読み取り専用スナップショットを引き抜く(Pull型)」構成にします。さらに本番側でも zfs hold でカーネル参照ロックをかけることで、自動整理スクリプトや誤操作によるスナップショット消去を防ぎます。

今日から確認できる!不変バックアップと迅速復旧 3項目チェック

達成度: 0 / 3

クリックしてチェックを入れると、御社のデータ保護体制に応じた診断コメントが表示されます。

上の3項目をクリックして、現在のバックアップと復旧体制を点検してみてください。
ZFS POOL & SNAPSHOT VERIFICATION COMMANDS

OpenZFS マークル木整合性・zfs hold 不変ロックの一次検証手順

親ブロック側が子ブロックの256ビットチェックサムを保持するOpenZFSのマークル木構造では、ディスクコントローラの書き込み抜け(Lost Write / Ghost Write)も即座に検出されます。db01.ok.st における実運用検証コマンドは以下の通りです。

ZFS / ZDB COMMAND KERNEL INVARIANT 検証される物理構造・保護動作
zfs holds -r zroot/pgdata ds_userrefs > 0 root権限による zfs destroy 発行時にもカーネルが EBUSY で削除を拒絶すること
zpool status -v zroot READ / WRITE / CKSUM == 0 Fletcher-4 / SHA-256 256ビットマークル木によるサイレント破損のゼロ件維持と自動修復確認
zdb -uuuu zroot Uberblock[txg mod 128] 最新 TXG のルート blkptr_t と 256-bit マークルルートハッシュの整合性確認
関連コラム・豆知識

なぜ「ZFS ARCキャッシュ」は、月末の大量集計をしても普段の画面が遅くならないのか?

多くのシステムでは、よく使うデータをメモリに置いておくために「最近使われたもの(LRU)」を残す仕組みを使っています。しかしこの方法には弱点があり、たとえば経理担当者が「過去5年分の全取引データを一括検索(フルスキャン)」すると、その大量の一過性データによって、普段みんながよく使っている大事な顧客名簿のキャッシュがメモリから追い出されてしまい、画面が急に重くなってしまいます(キャッシュ汚染)。OpenZFSが搭載する「ARC(Adaptive Replacement Cache)」は、メモリを「1回だけ読まれた部屋(MRU)」と「2回以上繰り返し読まれた常連の部屋(MFU)」の2つに分け、アクセスの傾向に合わせて自動で仕切りの幅を調整します。そのため、誰かが大きな集計処理を走らせても常連のデータは一切追い出されず、常に0.001秒単位の軽快な応答が保たれるのです。

技術仕様・実装解説:Technical Architecture & Implementation — OpenZFS 128-Byte blkptr_t, Fletcher-4 Merkle Tree & Atomic Uberblock Commit

OpenZFS 128バイト blkptr_t 物理レイアウト・256ビット Fletcher-4 マークル木数理と Uberblock アトミックTXG検証エンジン

sys/fs/zfs/sys/spa.h / uberblock_impl.h / zio_checksum.c
Block Pointer Size 128 Bytes (3x DVA + 256b Hash)
Uberblock Ring 128 Slots x 1KB (4x VDEV Labels)
Merkle Checksum Fletcher-4 / SHA-256 / Blake3
Snapshot Hold Lock dsl_dataset_user_hold_Irrevocable
1. 設計仕様・数理モデル(128-Byte blkptr_t Bitfield Architecture, Fletcher-4 Recurrence & Atomic TXG State Machine)

OpenZFS の Storage Pool Allocator(sys/fs/zfs/sys/spa.h)において、ツリー内のすべての間接ブロックおよびデータブロックは 128バイトの blkptr_t によってアドレス指定されます。先頭 48バイトは最大3つの Data Virtual Address(dva_t blk_dva[3]、各16バイト:32ビット VDEV ID + 63ビット物理セクタオフセット + 24ビット割り当てサイズ)を格納し、メタデータは自動的に二重・三重書き込み(Ditto Blocks)されます。続く 64ビット整数 blk_prop には、論理サイズ lsize(ビット 0..15)、圧縮後物理サイズ psize(ビット 16..31)、圧縮アルゴリズム識別子(ZIO_COMPRESS_LZ4 = 15、ビット 32..38)、チェックサム関数識別子(ビット 40..47)、およびエンディアン標識(ビット 63)がパックされています。

末端 32バイト(オフセット +0x60..+0x7F)の zio_cksum_t blk_cksum は、子ブロックの物理ペイロード W = (w0, w1, …, wM−1) ∈ (ℤ232)M に対する 256ビット検証値(4つの64ビットワード A, B, C, D)を保持します。高速データパスで標準使用される Fletcher-4 は以下の4次累積漸化式で定義されます:

Ak = (∑i=0..k wi) mod 264,   Bk = (∑i=0..k Ai) mod 264,   Ck = (∑i=0..k Bi) mod 264,   Dk = (∑i=0..k Ci) mod 264
RootHash(TXGt) = H(MOS_blkptr_t),   Snapshot Immutable Invariant: blk_birth ≤ txgsnap ⇒ Read-Only

Transaction Group(TXG)が Open → Quiescing → Syncing フェーズを経てディスクへフラッシュされる際、変更された葉ブロックから Meta Object Set(MOS)に至る全経路が新しい DVA に書き出され、最後に各 VDEV の4つの 256KB ラベル(L0, L1: 先頭、L2, L3: 末尾)内にある 128スロットの Uberblock 配列のうち、スロット s = txg mod 128 がアトミックに更新されます。zfs hold が適用されたスナップショットは ds_userrefs > 0 を保持するため、dsl_destroy_snapshot_check_impl() がカーネルレベルで EBUSY を返し、削除要求を拒絶します。

2. パラメータ・構成仕様(OpenZFS blkptr_t (128B) & uberblock_t (1024B) Exact Byte Specification) sys/fs/zfs/sys/spa.h & uberblock_impl.h
Byte OffsetStruct / Field IdentifierWidth / EncodingStorage & Cryptographic Invariant
blkptr_t +0x00..0x2Fdva_t blk_dva[0..2]48 Bytes (3 x 128-bit)Up to 3 physical copies (Ditto blocks) across VDEVs
blkptr_t +0x30..0x37uint64_t blk_prop8 Bytes BitfieldLSIZE(16b), PSIZE(16b), LZ4(7b), CKSUM(8b), LEVEL(5b), TYPE(8b)
blkptr_t +0x48..0x57blk_phys_birth / blk_birth16 Bytes (2 x uint64_t)TXG epoch when block was allocated; governs CoW freeing
blkptr_t +0x58..0x5Fuint64_t blk_fill8 Bytes CounterCount of non-hole child blocks below this subtree node
blkptr_t +0x60..0x7Fzio_cksum_t blk_cksum32 Bytes (256-bit)Parent-verified Fletcher-4 / SHA-256 hash of child block
uberblock_t +0x00..0x9Fub_magic (0x00bab10c), ub_txg, ub_rootbp160 Bytes + 128B Root BPAtomic tree root pointer updated at end of TXG Syncing
3. 実装コード・設定仕様(src/storage/openzfs_merkle_verifier.rs)
Rust Edition 2024 (#![no_std] Zero-Copy Merkle & Uberblock Engine)
#![no_std]
#![forbid(unsafe_code)]

/// OpenZFS Uberblock magic number (`0x00bab10c` in native endianness)
pub const UBERBLOCK_MAGIC: u64 = 0x00ba_b10c;

/// Exact 128-byte representation of OpenZFS `blkptr_t` (`sys/fs/zfs/sys/spa.h`)
#[repr(C, align(8))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ZfsBlkptr128 {
    pub blk_dva: [[u64; 2]; 3], // +0x00..0x30: 3 Data Virtual Addresses (VDEV + Offset + ASIZE)
    pub blk_prop: u64,          // +0x30..0x38: Bitfield (lsize, psize, compress, cksum, type, level)
    pub blk_pad: [u64; 2],      // +0x38..0x48: Reserved / Encrypted IV words
    pub blk_phys_birth: u64,    // +0x48..0x50: Physical birth TXG
    pub blk_birth: u64,         // +0x50..0x58: Logical birth TXG
    pub blk_fill: u64,          // +0x58..0x60: Subtree fill count
    pub blk_cksum: [u64; 4],    // +0x60..0x80: 256-bit Fletcher-4 / SHA-256 checksum
}

const _: () = assert!(core::mem::size_of::<ZfsBlkptr128>() == 128);

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DecodedBlkProp {
    pub logical_bytes: u32,
    pub physical_bytes: u32,
    pub compression_id: u8,
    pub checksum_id: u8,
    pub indirection_level: u8,
    pub dmu_obj_type: u8,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ZfsAuditError {
    EmbeddedOrHoleBlock,
    PhysicalSizeMismatch { expected: usize, actual: usize },
    MerkleChecksumMismatch { word_index: usize, expected: u64, computed: u64 },
    SnapshotImmutabilityViolation { block_birth_txg: u64, snapshot_ceiling_txg: u64 },
    InvalidUberblockMagic(u64),
}

impl ZfsBlkptr128 {
    /// Decodes the 64-bit `blk_prop` bitfield per OpenZFS SPA sector-512 specification.
    #[inline]
    pub const fn decode_properties(&self) -> DecodedBlkProp {
        let lsize_sectors = (self.blk_prop & 0xFFFF) + 1;
        let psize_sectors = ((self.blk_prop >> 16) & 0xFFFF) + 1;
        let compression_id = ((self.blk_prop >> 32) & 0x7F) as u8;
        let checksum_id = ((self.blk_prop >> 40) & 0xFF) as u8;
        let dmu_obj_type = ((self.blk_prop >> 48) & 0xFF) as u8;
        let indirection_level = ((self.blk_prop >> 56) & 0x1F) as u8;

        DecodedBlkProp {
            logical_bytes: (lsize_sectors as u32) * 512,
            physical_bytes: (psize_sectors as u32) * 512,
            compression_id,
            checksum_id,
            indirection_level,
            dmu_obj_type,
        }
    }

    /// Verifies both Copy-on-Write snapshot immutability (`blk_birth <= snapshot_ceiling_txg`)
    /// and the 256-bit Fletcher-4 Merkle hash over the child payload words.
    pub const fn verify_child_block_fletcher4(
        &self,
        child_words_le: &[u32],
        snapshot_ceiling_txg: u64,
    ) -> Result<DecodedBlkProp, ZfsAuditError> {
        if self.blk_birth == 0 {
            return Err(ZfsAuditError::EmbeddedOrHoleBlock);
        }
        if self.blk_birth > snapshot_ceiling_txg {
            return Err(ZfsAuditError::SnapshotImmutabilityViolation {
                block_birth_txg: self.blk_birth,
                snapshot_ceiling_txg,
            });
        }

        let props = self.decode_properties();
        let actual_bytes = child_words_le.len() * 4;
        if actual_bytes != (props.physical_bytes as usize) {
            return Err(ZfsAuditError::PhysicalSizeMismatch {
                expected: props.physical_bytes as usize,
                actual: actual_bytes,
            });
        }

        let mut a: u64 = 0;
        let mut b: u64 = 0;
        let mut c: u64 = 0;
        let mut d: u64 = 0;
        let mut i = 0;
        while i < child_words_le.len() {
            a = a.wrapping_add(child_words_le[i] as u64);
            b = b.wrapping_add(a);
            c = c.wrapping_add(b);
            d = d.wrapping_add(c);
            i += 1;
        }

        let computed = [a, b, c, d];
        let mut w = 0;
        while w < 4 {
            if computed[w] != self.blk_cksum[w] {
                return Err(ZfsAuditError::MerkleChecksumMismatch {
                    word_index: w,
                    expected: self.blk_cksum[w],
                    computed: computed[w],
                });
            }
            w += 1;
        }

        Ok(props)
    }
}
ご質問・公的制度の活用に関するお問い合わせ窓口

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

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

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

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