XFSを解析した話 (1) - SuperBlock 編

はじめに

XFS ファイルシステムを解析できるGolangのライブラリを作りました。
Trivyというコンテナ脆弱性検知ツールで仮想マシンイメージの脆弱性検知機能を開発するにあたって、KernelやC言語の資源に依存せずにXFSを解析する必要がありました。
仕事終わりの時間を使ってガッと作ったので対応していないデータ構造などもありますが、一旦動くものができたので、知識を貯めるという意味でも記事に残そうと思います。

かなり長い記事になりそうなので、何回かに分けて記事を作成しようと思います。

目次

想定読者

XFS Filesystemの生データを解析したいと思っている方に向けた記事です。

今回の記事では、XFSのバイナリデータを作成して、スーパーブロックのデータを見たいと思います

作ったライブラリ

Goの io/fs の FS interfaceに合わせて実装していますので、簡単に利用できるかと思います。

作ったもの

GitHub - masahiro331/go-xfs-filesystem

io/fsのドキュメント

fs package - io/fs - Go Packages

概要

ではXFSの解析に必要な知識をまとめていきます。
今回の記事では以下のような点は記載しておりません。

  • Journal Log
  • Extended Attributes
  • ファイルの書き込み, 削除など

Extended Attributesはファイルに紐づくメタ情報のようなもので、SELinuxなどの情報が入っています。(知らんけど)

初めに

まずは公式ドキュメントと信頼できる解析ツールを用意しましょう。

公式のドキュメントは以下のようにビルドするか、古いですがインターネットに公開されているやつを見ましょう。

公式ドキュメント

$ git clone git://git.kernel.org/pub/scm/fs/xfs/xfs-documentation.git
$ cd xfs-documentation
$ make 

僕は centos:7 ですが、別になんでもいいです。

$ yum install xfs_dumps

(前提知識の補足)ファイルシステムの構造について

私はXFSとEXT4のデータ構造しか知らないのですが、ファイルシステムには以下のような共通のデータ構造があります。

  • SuperBlock
  • Inode
  • Extents List (Tree)

Superblock とは

ファイルシステムがHDDのデータを管理するためのサイズをブロックと呼び、ブロックのサイズはSuperBlockに書かれています。
そのためファイルシステムの先頭にSuperBlockが配置されていることが多いです。
(※ EXT4では先頭1024バイトにBootSectorが配置されていることがあるため、1024バイト目から SuperBlockが始まります。 )

SuperBlockにはファイルシステムを読み込むために必要な情報が書かれており、例えばInodeあたりのサイズやファイルシステム全体のブロック数などが管理されています。

参考: スーパーブロック (ファイルシステム) - Wikipedia
Wikipediaが詳しいです。

Inode とは

InodeはLinux上のファイル に一意に付与される番号です。つまりファイルシステムに管理できるファイルは、Inodeの総数に依存します。

ファイルの種類

  • Regular file
  • Directory
  • Character special device
  • Block special device
  • FIFO
  • Socket
  • Symlink

Linux上ではファイルのInodeと実際のデータは別で管理されているため、Inodeにはデータが保存されている場所が保存されています。
その他にも、ファイルの所有者やタイムスタンプの情報などが格納されています。

参考: inode - Wikipedia
Wikipediaを信じましょう。

Extents Listとは

Extents List(Tree) はよくわかってないです。 僕の認識ではInodeとデータを紐づける中間データをExtents Listと呼んでおり、実データの領域をExtentsと呼んでいる気がします。

参考: Extent (file systems) - Wikipedia
これもWikipediaを信じましょう。

XFSについて

共通の用語がわかったところでXFSについて記載していきます。

XFSではファイルシステムを Allocation Group(以降はAGと記載) と呼ばれる単位で分割して管理しています。
各AGは独立して、空き容量やinode、その他のメタデータを管理しているため、個別のファイルシステムとほぼ見なすことができます。
これにより、同時アクセスの数が増えてもパフォーマンスを低下させることなく、XFSでの操作を並行することが処理できます。

AGについて

AGの最大サイズは 1TBであり、AGあたりのサイズはSuperBlockのAgBlocksとBlockSizeの積によって求められます。
ファイルシステム内のAGの個数はSuperBlockのAgCountで管理されています。

AGは物理的に連続に配置されているため、N番目のAGの開始位置は AgBlocks * BlockSize* N で表現されます 。

各AGの先頭 block には以下の4つのデータが管理されており、各データが512バイトごとに配置されています。

  • SuperBlock
  • AGF
  • AGFL
  • AGI

SuperBlockについて

各AGは、スーパーブロックから始まり、最初の AG に属する SuperBlock は Primary SuperBlock と呼ばれ、基本的にはこの SuperBlock を参照してファイルシステムを読み込んでいきます。 以降の AG に存在する SuperBlock は Secondary SuperBlock と呼ばれ Primary SuperBlock が破損した際のバックアップとして用いられます。

SuperBlock の実装は以下のコードに記載されています。

https://github.com/torvalds/linux/blob/4a3bb4200a5958d76cc26ebe4db4257efa56812b/fs/xfs/libxfs/xfs_format.h#L96

AGF について

AGFは AG Free Space と呼ばれ、 AG内の空き領域を追跡するための領域です。

2つの B+Tree を使用してAG内の空き領域を管理し、1つは B+Tree でブロック番号を指し、もう1つの B+Tree で空き領域ブロックのサイズを記録します。 注意点としてブロック番号などの値は、AGのブロックオフセットからの相対値です。

今回の実装では完全に無視しています。

AGFL について

AGFLは AGフリーリスト と呼ばれ、AG Free Space の B+Tree が大きくなった場合に利用される、予約済みスペースです。 AGFL で管理されるブロックスペースは inodeやデータなどでは利用できないようになってます。

今回の実装では完全に無視しています。

AGI について

AGI は AG内の Inode を管理します。 AG内で使用している inode数や B+Tree のレベル制限などが記載されています。

今回の実装では完全に無視しています。

実際のデータに触れてみる

簡単なXFSのデータを以下の手順で作成して触ってみましょう。

デバッグ用のイメージを作成

# はじめにからのブロックデバイスを作成します。
$ dd of=Linux.img count=0 seek=1 bs=41943040

# 空いているループデバイスを検索
$ losetup  -f

# 出力されたループデバイスと先ほど作成したブロックデバイスを接続(/dev/loop5が出力された場合)
$ losetup /dev/loop5 Linux.img

# パーティションを作成していきます。
# 先頭 1MiB から 2MiB までをbootパーティションとして作成
# 先頭 2MiB から 最後までを XFSのパーティションとして作成
$ parted /dev/loop5
(parted)$ mklabel gpt
(parted)$ mkpart primary 1MiB 2MiB
(parted)$ set 1 boot on
(parted)$ mkpart primary xfs 2MiB 100%
(parted)$ quit

# XFS ファイルシステムで先ほど作った XFSパーティションをフォーマット
# loop5 の partition 2という意味で loop5p2 という表現になっていると推測(知らん)
$ mkfs.xfs /dev/loop5p2

# xfsのパーティションを /mnt/xfs にマウント
$ mount /dev/loop5p2 /mnt/xfs

# ファイルとかが作成できることを確認
$ mkdir /mnt/xfs/etc/
$ cp system-release /mnt/xfs/etc/system-release

# アンマウント
$ umount /mnt/xfs

# ループデバイスとの接続を切断
$ losetup -d /dev/loop5

これで作成した Linux.img はブートパーティション付きのXFSファイルシステムが入ったイメージになりました。パーティションなど作成せずにイメージを作成できるかもしれませんが、自分は知らないので知っている方は教えてください。

次は作成したデータを見てみましょう。

スーパーブロックを眺める

# 先頭 2MiB から 512 bytes 分だけ出力
$ xxd -s 0x200000 -l 0x200 Linux.img

00200000: 5846 5342 0000 1000 0000 0000 0000 25fb  XFSB..........%.
00200010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00200020: 1b1b cec8 c7eb 4ff3 bfd5 1fad cd95 ee46  ......O........F
00200030: 0000 0000 0000 2006 0000 0000 0000 0080  ...... .........
00200040: 0000 0000 0000 0081 0000 0000 0000 0082  ................
00200050: 0000 0001 0000 12fe 0000 0002 0000 0000  ................
00200060: 0000 0558 b4a5 0200 0200 0008 0000 0000  ...X............
00200070: 0000 0000 0000 0000 0c09 0903 0d00 0019  ................
00200080: 0000 0000 0000 0080 0000 0000 0000 0077  ...............w
00200090: 0000 0000 0000 1bb6 0000 0000 0000 0000  ................
002000a0: ffff ffff ffff ffff ffff ffff ffff ffff  ................
002000b0: 0000 0000 0000 0008 0000 0000 0000 0000  ................
002000c0: 0000 0000 0000 0001 0000 018a 0000 018a  ................
002000d0: 0000 0000 0000 0005 0000 0003 0000 0000  ................
002000e0: 47b4 f325 0000 0004 ffff ffff ffff ffff  G..%............
002000f0: 0000 0001 0000 0038 0000 0000 0000 0000  .......8........
          ... 全部 0x00 なので割愛
002001f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

先頭4bytes がスーパーブロックのマジックバイトである、XFSB(0x58465342) となっており、正しくとれていそうです。仕様書を眺めながら見ていただいても良いのですが、大変なので xfs_db を使ってもう少しわかりやすくしましょう。

# Linux.imgには boot partitionが存在するため、XFSのデータ領域だけ取ってやる
$ xxd -s 0x200000  Linux.img  | xxd -r  >> xfs.img

# xfs_db でデバッグする
$ xfs_db xfs.img

# インタラクティブなやつが立ち上がるので、以下の手順で確認(helpが使えるので気になる方は調べてください)
xfs_db> sb
xfs_db> p 

magicnum = 0x58465342
blocksize = 4096
dblocks = 9723
rblocks = 0
rextents = 0
uuid = 1b1bcec8-c7eb-4ff3-bfd5-1fadcd95ee46
logstart = 8198
rootino = 128
rbmino = 129
rsumino = 130
rextsize = 1
agblocks = 4862
agcount = 2
rbmblocks = 0
logblocks = 1368
versionnum = 0xb4a5
sectsize = 512
inodesize = 512
inopblock = 8
色々長いので割愛
meta_uuid = 00000000-0000-0000-0000-000000000000

いろいろ情報がでてきました。

先ほど説明したこれらが本当かどうか確かめてみましょう。

AGあたりのサイズはSuperBlockのAgBlocksとBlockSizeの積によって求められます。
ファイルシステム内のAGの個数はSuperBlockのAgCountで管理されています。
AGは物理的に連続に配置されているため、N番目のAGの開始位置は AgBlocks * BlockSize* N で表現されます 。

今回作成したXFSは2つのAGを持っているようです。各AGはスーパーブロックを保持しているので、2つめのAGの先頭 8 bytes は XFSB のマジックバイトを持っているはずです。

agcount = 2
blocksize = 4096
agblocks = 4862

xfs_dbの出力結果をもとに xxd で2つめのAGの先頭バイトを確認しましょう。

# AG Offset = blocksize * agblocks * ag number(0から数えます). 
# AG Offset = 4096 * 4862 * 1 
# AG Offset = 19914752
$ xxd -s 19914752 -l 8 xfs.img
012fe000: 5846 5342 0000 1000                      XFSB....

先頭 8 bytes が XFSBになっているため、正しくAGが配置されていることがわかります。

おわり

本記事はここまでにします。

次の記事では inode について深掘りしていこうと思います。 このスーパーブロックはinodeを取得するときなどにも利用するので、今回触れなかったパラメータについては次以降の記事で触れていこうと思います。