dm-integrity によるブロックデバイスのデータ破損の検出

Linux で利用可能な device mapper のひとつに dm-integrity があります。これは汎用的なブロックストレージにデータ破損を検出する機能を付与することができる仕組みです。この記事は dm-integrity について私が気になったいくつかのトピックをまとめたものであり、一部はドキュメントに記載されていない、ソースコードから読み取った内容を含んでいます。

device mapper とは?

dm-integrity は device mapper の一種です。device mapper は仮想ブロックデバイスを作成する機能で、Linux カーネルに組み込まれており LVM のバックエンドとして利用されています。基となるブロックストレージ (underlying device) に付加的な機能を追加した仮想的なブロックデバイスを作成することができます。たとえば仮想ブロックデバイスへの書き込みを複数の underlying device に分散させることで RAID を実現したり (dm-raid)、仮想ブロックデバイスへの書き込みを暗号化してから underlying device に書き込むことでユーザー空間から透過的にディスク上のデータを暗号化したり (dm-crypt) することができます。

dm-integrity が解決したい課題

プログラムからディスクに書き込んだはずのデータは、不幸なことに壊れている場合があります。プログラムや Linux カーネルに不具合が無かったとしても、たとえば以下のような原因でデータ破損が起きる可能性があります。

  • ストレージのファームウェアに不具合があり不正なデータが書き込まれた
  • RAID コントローラーに不具合があり不正なデータが書き込まれた
  • ストレージデバイスへの物理的なダメージにより書き込まれたはずのビット列が変化した

このようなデータ破損は bit rot や silent data corruption と呼ばれ、大規模なインフラを構築しているような企業では無視できない確率で発生する可能性があります。

dm-integrity では仮想ブロックデバイスに書き込んだデータをそのまま保存すると同時に、書き込んだデータのチェックサムを別の場所に記録します。読み出し時には読み出したデータのチェックサムと記録したチェックサムとの比較を行うことでデータ破損を検出します。

dm-integrity と dm-crypt を組み合わせたデータ保護

dm-crypt はディスクに書き込まれるデータを透過的に暗号化する機能を提供する device mapper です。dm-integrity は dm-crypt と組み合わせることもできます。dm-integrity のみを利用した場合はチェックサムとの比較によりデータ破損を検出できますが、データセンターへの不正な侵入や保守作業員の買収などの手段により攻撃者がストレージデバイスのデータを書き換え可能なとき、データとチェックサムの両方を改ざんされてしまった場合には改ざんを検出することができません。dm-integrity と dm-crypt を組み合わせると、データから計算したハッシュをチェックサムとするのではなく、データと秘密鍵から計算した HMAC をチェックサムとすることで、データやチェックサムの改ざんを難しくすることができます。

データが壊れてしまったら検出できても意味がないのでは?

読み出そうとしたデータが壊れていることが分かっても壊れる前のデータが読み出せるようになるわけではないため、それほど有用ではないのでは?と思うかもしれません。たしかに壊れてしまったディスクからデータを読み出すことはできませんが、検出できること自体が有用な場合があります。

まず壊れたデータを返すよりは、データが壊れていることが分かった時点で処理を停止して応答を返さないほうがよい場合があります。このような場合、データが壊れていることを検出するだけでも十分な意味があります。

次に分散システムを構築しているような場合です。少数のノード故障に耐えることを意図して作られた分散システムであれば、壊れたデータを含むノードは落としてしまうことで、データが壊れていない他のノードでサービスを継続することができます。

最後に RAID-1 などで複数のディスクにデータを保存しているような場合です。device mapper で作成されるのは仮想的なブロックデバイスのため、複数の物理ディスクそれぞれについて仮想ブロックデバイスを作ってから RAID で束ねるようなこともできます。このような構成では RAID を構成するそれぞれの物理ディスクについてデータ破損を検出するためのチェックサムが記録されることになります。データ破損が検出されたデバイスは壊れてしまったものとして RAID から切り離して残りのディスクでサービスを継続することができます。

dm-integrity 以外に同じ問題を解決する技術にはどんなものがあるのか?

dm-integrity は device mapper の仕組みを使うことでデータのチェックサムを付与する機能をブロックデバイス自体に付与していますが、これ以外のレイヤーでデータのチェックサムを付与している技術もあります。

データベースなどのアプリケーションやライブラリでは silent data corruption を想定した機能が備わっておりデフォルトで有効になっている場合が多いです。たとえば MySQL では innodb_checksum_algorithm という項目により設定可能ですが、16 KiB のページごとに CRC32 を付与し整合性をチェックする設定がデフォルトで有効になっています。

ファイルシステムレベルでチェックサムを持つ場合もあります。有名なのは Btrfs と ZFS です。

dm-integrity を採用することが適切な場面

silent data corruption の耐性があるシステムを構築する必要があるようなとき、どのような場面では dm-integrity を採用することが適切になるでしょうか?データにチェックサムを付与する機能がないファイルシステム(典型的には ext4 や XFS)を使わざるを得ない制約があり、かつアプリケーションレベルでチェックサムを付与することができないような状況が考えられます。

具体的には RHEL がサポートしている範囲のテクノロジーで silent data corruption への耐性があるシステムを構築したいという場合には候補に挙がるかもしれません。SUSE のデフォルトファイルシステムは Btrfs であり、近年の UbuntuZFS のサポートを強化していますが、Red Hat はこれらのファイルシステムには積極的ではありません。RHEL でよく使われている ext4, XFS に関しては、ext4 では metadata_csum オプション、XFS では crc= オプションによりファイルシステムメタデータにはチェックサムを付与できる機能がありますが、データにチェックサムを付与できる機能はありません。一方、RHEL では dm-integrity は TechPreview ではなく supported ということになっているようです。Red Hat には device mapper の開発者が多数在籍しておりストレージ機能の拡充を device mapper で解決する傾向がありますが、silent data corruption の対策も device mapper のレイヤーで問題を解決したいという方針なのかもしれません。

もし ext4 や XFS にもデータにチェックサムを付与する機能が搭載されるようなことがあれば dm-integrity は不要になるのでしょうか? 実は、ext4 や XFS にデータのチェックサムを付与する機能が搭載されただけではそれほど状況は変わりません。データにチェックサムを付与する機能がこれらのファイルシステムに搭載されたとして、データ破損が見つかった場合にできるのはファイルが破損していることを通知してデータの読み取りを拒否することだけです*1。一方、dm-integrity を使う方法では前述したように RAID と組み合わせることでデータ破損が見つかったディスクを RAID から切り離して残りのディスクでサービスを継続することができます。よって、もし ext4 や XFS にもデータにチェックサムを付与する機能が搭載されるとしても、dm-integrity は不要になるとは限りません。

3つの動作モードとその特徴

dm-integrity ではデータの書き込み時にデータとチェックサムの2つを書き込む必要があります。データベースのストレージレイヤーの心得がある人ならお察しの通り、これは書き込みを2回するだけの単純な操作ではありません。単純に2箇所を書き換えるだけではデータの書き込みとチェックサムの書き込みの間で予期しない電源断やカーネルパニックによって処理が中断されたとき、ディスク上に永続化されているデータとチェックサムが矛盾する可能性があります。

この対策として dm-integrity は3つの動作モードを提供しています。

  • J (journaled write): ジャーナルを利用するモードです。データ部、チェックサム部を変更する前にまず変更内容をジャーナルに書き出してディスクに永続化し、そのあとデータ部、チェックサム部に変更を適用します。最も整合性が強いですが、必要な書き込み量が増えるため性能的には不利です。

  • B (bitmap mode): データとチェックサムが一致していない可能性があるセクタをビットマップで管理するモードです。詳細は後述します。

  • D (direct write): 何も対策しない漢気モードです。電源断が生じたマシンを再起動したときには、データ破損が生じていなかったとしてもデータとチェックサムが一致しない可能性があります。またデータとチェックサムの矛盾が見つかったとしても、電源断によって起きた不一致なのかデータが破損したのか判別することができません。一方、オーバーヘッドはほかのモードと比べて小さいため、運用によっては利用を検討できる場合があります。たとえば予期しない電源断が生じたマシンは単純に再起動後に復帰させず運用から外して修理に出し、修理から帰ってきたらサーバーを初期化して再セットアップしてから運用に復帰させるような運用ではこの問題を許容できるでしょう。

データフォーマット

データフォーマットは dmsetup の meta_device パラメータ、あるいは integritysetup の --data-device オプションを渡すかどうかで大きく変わります*2

meta_device を指定しない場合は dm-integrity のスーパーブロック、ジャーナルまたはビットマップに続いてデータを含むブロックとチェックサムを含むブロックが交互に続く(インターリーブされる)フォーマットになります。

meta_device を指定した場合にはデータを書き込むデバイスと dm-integrity のスーパーブロックやチェックサムを書き込むデバイスが分離されます。この機能は Linux 4.19 で導入されました。このフォーマットではすでにデータが書き込まれているデバイスに対して dm-integrity の device mapper を重ねてチェックサムを新たに付与することができます*3。また、何らかの事情で dm-integrity が使えなくなった場合にデータデバイスに直接アクセスすることもできます。

どちらの方式のほうが優れているか考えたとき、私の理解ではインターリーブされるフォーマット(meta_device を使わない方式)の利点は思いつきませんでした。meta_device を使わない場合は必要な underlying device が一つだけである一方、meta_device を使う場合は underlying device が二つ必要という点が気になるかもしれません。しかし device mapper を利用する場合には大抵は LVM による論理ボリューム (dm-linear) を利用すると思われるので、それほど問題にならないと考えています。

bitmap mode での処理の流れ

bitmap mode ではデータとチェックサムが確実に一致しているはずであるかどうかを表すビット列をディスク上に永続化しています。ビットマップの1ビットがデータの何セクタ分に相当するかは調整可能で、dmsetup の sectors_per_bit パラメータ、integritysetup コマンドでは --bitmap-sectors-per-bit というオプション*4で制御できます。ただしここで与えた値が必ず使われるわけではなく、ビットマップ領域がディスクに収まらないときにはビットマップ領域のサイズを小さくするために sectors_per_bit を勝手に大きくする調整が入るようです

このビットマップのそれぞれのビットが SET と CLEAR という状態をとります。SET は対応するセクタのディスク上のデータとチェックサムが一致しないかもしれない状態です。これに対して CLEAR はデータとチェックサムが一致しているはずの状態です。電源断が発生したとき、ビットマップが SET になっているセクタでは D (direct write) と同様にデータ破損が発生していなくてもデータとチェックサムが一致しない可能性があり、データ破損が起きたのかどうか定かでありません。一方、ビットマップが CLEAR になっているセクタでデータとチェックサムが一致しなければ確実にデータ破損が起きています。

ビットマップの SET と CLEAR がこのような意味を持つため、bitmap mode ではビットマップの SET をディスク上に FLUSH してからでないとデータやチェックサムを書き換えてはならないといった制約を守りながら書き込みをする必要があります。bitmap mode は journaled mode と比べてオーバーヘッドが小さいとはいえ、一度の書き込みの中で何度か FLUSH を待たなければいけないポイントがあり、ある程度のオーバーヘッドは覚悟する必要があります。逆にビットマップを CLEAR にする処理は多少の遅延が許容されるため dmsetup の bitmap_flush_interval パラメータや integritysetup の --bitmap-flush-time オプションで調整することができます。

使用できるハッシュ関数

チェックサムの計算に利用できるアルゴリズムは dmsetup の internal_hash パラメータや integritysetup の --integrity オプションで指定できます。ここで指定可能なハッシュ関数の一覧や名前のフォーマット("SHA-256" なのか "sha256" なのか)はドキュメントに記載はありませんが、結局のところ渡されたパラメータは crypto_alloc_shash に渡されています。crypto_alloc_shash で取得できるハッシュ関数は crypto_register_shash や crypto_register_shashes で登録されたもののようなので、crypto/ ディレクトリ以下で使いたいハッシュ関数のファイルを探して登録されているかどうかを調べればよさそうでした。md5, sha1, sha256, sha512, sha3-256, xxhash64 などは登録されていました。

チェックサムの計算方法

チェックサムintegrity_sector_checksum という関数で計算されています。要約すると以下のようになります。

crypto_shash_init(req);
crypto_shash_update(req, (const __u8 *)&sector_le, sizeof sector_le);
crypto_shash_update(req, data, ic->sectors_per_block << SECTOR_SHIFT);
crypto_shash_final(req, result);

sector_le は書き込み先のセクタの位置(リトルエンディアン表現)、data は書き込もうとしているデータを指すポインタです。ic->sectors_per_block << SECTOR_SHIFTチェックサム1つに対応するデータの大きさ(バイト換算)で dmsetup するときに block_size というパラメータで指定することができます。したがって、チェックサムの計算には書き込もうとしているデータだけでなくセクタ位置も含まれることが分かります。チェックサムの計算にデータのみを利用した場合はデータの内容の破損は検知することができる一方で、データの内容は合っているが書き込み位置がおかしくなるような場合のデータ破損を検知することができません。セクタ位置をチェックサムに含めることにより、このようなデータ破損を検知することができるようになります。

buffer_sectors とは

dmsetup の buffer_sectors パラメータ、あるいは integritysetup の --buffer-sectors オプションとして謎のパラメータを渡すことができるようになっています。この値をもとに計算される log2_buffer_sectors という変数は dm-integrity のソースコード中でチェックサムの保存先の位置を計算する処理など、さまざまな重要な箇所で登場します。このパラメータは dm_bufio という buffered I/O の仕組みのバッファサイズを指定するもので、dm_bufio_client_create() の block_size というパラメータとして与えられます。このバッファサイズを基準として読み書きを行うために、ディスクレイアウトを決める際には block_size の倍数になるようにする必要があったり、読み書きの際に指定するポジションも block_size を基準に指定しなければならないといった理由があり、ソースコード中のいろいろな箇所でバッファサイズが登場しています。

終わりに

dm-integrity を利用しようとしていた時期があり、一時期ソースコードに踏み込んだ調査をしていたこともあったのですが、途中で没になってしまったので中途半端なところで終わってしまいました。

コードリーディングは処理の流れを追うだけならついていけなくもないのですが、queue_work() を駆使して処理を別スレッドにたらい回しにしている箇所が多数あるため、処理の順番を想像するのが難しかったです。自分でゼロから正しく書けと言われたり、バグを見つけろと言われたら無理です…

*1:Btrfs や ZFSチェックサム機能にも同様の問題があるのではと思ったかもしれませんが、Btrfs や ZFS にはファイルシステム自体に複数のディスクを扱うストレージプールとしての機能があるため、正常なデータが書き込まれているディスクからデータを復元できる可能性があります。

*2:meta device と data device は対になる概念なので、ツールによって指定対象が逆なのがややこしい…

*3:すでにデータが保存されているデバイスに対してチェックサムを追加したいときにはよく検証してから使ってください。integritysetup format --no-wipe してから integritysetup open --integrity-recalculate するのが正しい使い方です。--no-wipe なしで実行するとデータが消えます…

*4:integritysetup コマンドで --bitmap-sectors-per-bit を使うときには罠があります。integritysetup format してから integritysetup open するのが通常の使い方ですが、--bitmap-sectors-per-bit オプションは integritysetup open で設定してください。integritysetup format ではありません。--bitmap-sectors-per-bit で指定した値を journal_watermark に設定したうえで bitmap mode ではなく journaled write で初期化しようとしてしまいます。そして --bitmap-sectors-per-bit に100より大きい値を設定していたときには Invalid argument というエラーが出て失敗します。どうやら integritysetup が journaled write 以外のモードを想定せずに設計されていたところ、あとから bitmap mode に対応させようとした結果としてこのようなことになってしまったようです