XFSの2038年問題にオンラインでも勝利したい

tldr;

これの詳しい話. また, オンラインでも変換できることを示す.

XFSのbigtimeとは?

XFSのタイムスタンプは現状では, signed 32bitがUNIXエポックタイム(1970年1月1日 00:00:00 UTC)からの秒数で, 32bitがナノ秒部分となっています. これだと2038年以後の日時を表現できなくなります. そこで, その2つのフィールドをまとめて64bitにして, signed 32bitで表現できていた最小のタイムスタンプ(1901年12月13日 20:45:52 UTC, signedなのでマイナスも表現できてエポックより前のここが最小)からの経過ナノ秒でタイムスタンプを表現することにします. これがbigtimeという機能で, 2486年7月頃までの表現が可能になります.

ようするに, これが

/* Legacy timestamp encoding format. */
struct xfs_legacy_timestamp {
    __be32      t_sec;      /* timestamp seconds */
    __be32      t_nsec;     /* timestamp nanoseconds */
};

これになる

typedef __be64 xfs_timestamp_t;

bigtimeの変換を見てみよう

おおよそ10年以内, 2013年以後に作られたXFSであれば, オフラインで(mountしていない状態で)bigtimeを有効にできます. 実験してみましょう.

まずは, 適当なところにXFSのファイルシステムイメージを作りmountします. もうデフォルトでbigtimeが有効な場合もあるので, "-m bigtime=0"でbigtimeを明示的に無効化しておきます.

$ rm xfs2.img
$ truncate -s 10G xfs2.img
$ mkfs.xfs -m bigtime=0 xfs2.img
meta-data=xfs2.img               isize=512    agcount=4, agsize=655360 blks
         =                       sectsz=512   attr=2, projid32bit=1
         =                       crc=1        finobt=1, sparse=1, rmapbt=1
         =                       reflink=1    bigtime=0 inobtcount=1 nrext64=1
         =                       exchange=0
data     =                       bsize=4096   blocks=2621440, imaxpct=25
         =                       sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0, ftype=1, parent=0
log      =internal log           bsize=4096   blocks=16384, version=2
         =                       sectsz=512   sunit=0 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0
$ sudo mount xfs2.img /mnt/scratch

"old"というファイルに適当なタイムスタンプをつけて, umountします.

$ sudo touch -t 202410160900 /mnt/scratch/old
$ sudo umount /mnt/scratch

xfs_adminでbigtimeを有効にします. さくっと終わります.

$ xfs_admin -O bigtime=1 xfs2.img
Running xfs_repair to upgrade filesystem.
...
Phase 1 - find and verify superblock...
...
Phase 7 - verify and correct link counts...
done

新しいファイル"new"に同じタイムスタンプをつけて, umountします.

$ sudo mount xfs2.img /mnt/scratch
$ sudo touch -t 202410160900 /mnt/scratch/new
$ sudo umount /mnt/scratch

ここからxfs_dbを使ってinodeをチェックします. まず"info"でスーパーブロックの情報を見ると, "bigtime=1"とフラグがついていることがわかります.

xfs_db> info
meta-data=xfs.img                isize=512    agcount=4, agsize=655360 blks
         =                       sectsz=512   attr=2, projid32bit=1
         =                       crc=1        finobt=1, sparse=1, rmapbt=1
         =                       reflink=1    bigtime=1 inobtcount=1 nrext64=1
         =                       exchange=0
data     =                       bsize=4096   blocks=2621440, imaxpct=25
         =                       sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0, ftype=1, parent=0
log      =internal log           bsize=4096   blocks=16384, version=2
         =                       sectsz=512   sunit=0 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0

次に2つのファイルのinodeの中の情報を見ます. まずは"ls /"で2つのファイルのinode番号を確認. 131と132です.

$ xfs_db xfs2.img
xfs_db> ls /
/:
8          128                directory      0x0000002e   1 . (good)
10         128                directory      0x0000172e   2 .. (good)
12         131                regular        0x001bf664   3 old (good)
14         132                regular        0x001bb2f7   3 new (good)

inode番号131であるファイル"old"を見ます. "v3.bigtime = 0"とinodeの内部にもフラグがあります.

xfs_db> inode 131
xfs_db> p
...
core.atime.sec = Wed Oct 16 09:00:00 2024
core.atime.nsec = 0
core.mtime.sec = Wed Oct 16 09:00:00 2024
core.mtime.nsec = 0
core.ctime.sec = Wed Oct 16 09:21:57 2024
core.ctime.nsec = 153635926
...
v3.bigtime = 0
...

一方, inode番号132であるファイル"new"を見ます. 今度は"v3.bigtime = 1"です.

xfs_db> inode 132
xfs_db> p
...
core.atime.sec = Wed Oct 16 09:00:00 2024
core.atime.nsec = 0
core.mtime.sec = Wed Oct 16 09:00:00 2024
core.mtime.nsec = 0
core.ctime.sec = Wed Oct 16 09:23:32 2024
core.ctime.nsec = 281349756
...
v3.bigtime = 1
...

すなわち, スーパーブロックとそれぞれのinodeの両方にbigtimeのフラグがあります. それぞれのinodeのbigtimeフラグは, そのinode内のタイムスタンプが新旧どちらの表現であるかを示します. スーパーブロックのフラグが立っていれば, 新しいファイルやファイルの書きかえ時に新しいフォーマットのタイムスタンプに書き変わります.

ダンプを見よう

バイナリダンプを見とかないと終われないので見ましょう.

ファイル"old"はinode番号が131で, inodeのサイズが512バイト(infoのisizeに書いてる)なので, これで見れます.

先頭の"IN"でちゃんとinodeをdumpできていることが確認できます. 周りをあけている, 0x10620からの2行がタイムスタンプ(atime, mtime, ctime)です. dateコマンドでチェックしたところ(big-endianに注意), ちゃんと設定したタイムスタンプが入っています.

$ hexdump -C -s $((131 * 512)) -n 512 xfs2.img
00010600  49 4e 81 a4 03 02 00 00  00 00 00 00 00 00 00 00  |IN..............|
00010610  00 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00010620  67 0f 02 00 00 00 00 00  67 0f 02 00 00 00 00 00  |g.......g.......|
00010630  67 0f 07 25 09 28 4c 56  00 00 00 00 00 00 00 00  |g..%.(LV........|

00010640  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010650  00 00 18 01 00 00 00 00  00 00 00 00 56 5c 1c 43  |............V\.C|
00010660  ff ff ff ff f4 58 94 32  00 00 00 00 00 00 00 04  |.....X.2........|
00010670  00 00 00 01 00 00 00 04  00 00 00 00 00 00 00 10  |................|
00010680  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010690  67 0f 07 25 09 28 4c 56  00 00 00 00 00 00 00 83  |g..%.(LV........|
000106a0  2c 82 1d 4d 14 58 4a 73  8c dd 20 29 7f c7 88 dd  |,..M.XJs.. )....|
000106b0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00010770  00 2b 01 00 07 1d 04 73  65 6c 69 6e 75 78 73 74  |.+.....selinuxst|
00010780  61 66 66 5f 75 3a 6f 62  6a 65 63 74 5f 72 3a 75  |aff_u:object_r:u|
00010790  6e 6c 61 62 65 6c 65 64  5f 74 00 00 00 00 00 00  |nlabeled_t......|
000107a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00010800

$ date --date=@$((0x670f0200))
Wed Oct 16 09:00:00 AM JST 2024

一方, ファイル"new"の方はこうです. 同様にタイムスタンプの周囲をあけています.

$ hexdump -C -s $((132 * 512)) -n 512 xfs2.img
00010800  49 4e 81 a4 03 02 00 00  00 00 00 00 00 00 00 00  |IN..............|
00010810  00 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00010820  35 cc 2a cf 0b 94 00 00  35 cc 2a cf 0b 94 00 00  |5.*.....5.*.....|
00010830  35 cc 2c 17 de 1b 36 7c  00 00 00 00 00 00 00 00  |5.,...6|........|

00010840  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010850  00 00 18 01 00 00 00 00  00 00 00 00 36 2f 08 59  |............6/.Y|
00010860  ff ff ff ff 38 e7 18 5c  00 00 00 00 00 00 00 04  |....8..\........|
00010870  00 00 00 01 00 00 00 0f  00 00 00 00 00 00 00 18  |................|
00010880  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010890  35 cc 2c 17 de 1b 36 7c  00 00 00 00 00 00 00 84  |5.,...6|........|
000108a0  2c 82 1d 4d 14 58 4a 73  8c dd 20 29 7f c7 88 dd  |,..M.XJs.. )....|
000108b0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00010970  00 2b 01 00 07 1d 04 73  65 6c 69 6e 75 78 73 74  |.+.....selinuxst|
00010980  61 66 66 5f 75 3a 6f 62  6a 65 63 74 5f 72 3a 75  |aff_u:object_r:u|
00010990  6e 6c 61 62 65 6c 65 64  5f 74 00 00 00 00 00 00  |nlabeled_t......|
000109a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00010a00

タイムスタンプはナノ秒なので…とdateコマンドにスケールだけ変えてつっこむと未来が見えてしまいます. 最初に書いたようにこれは, 昔のタイムスタンプの最小値からのナノ秒です. その値は…"-2147483648" …ってイコール 2の31乗ですね. signed 32bitの最小なのでそれはそう. そのぶん補正すると, ちゃんと同じタイムスタンプがとれます.

$ date --date=@$((0x35cc2acf0b940000/1000000000))
Mon Nov  3 12:14:08 PM JST 2092
$ date --date='Dec 13 20:45:52 UTC 1901' +%s
-2147483648
$ date --date=@$((0x35cc2acf0b940000/1000000000 - 2**31))
Wed Oct 16 09:00:00 AM JST 2024

ここでもう一度"old"を同じタイムスタンプでtouchして, umountして, dumpをとります. するとすると, ちゃんと古いフォーマットからbigtimeのフォーマットに変換されているのが見てとれます.

$ hexdump -C -s $((131 * 512)) -n $((512 * 1)) xfs2.img
00010600  49 4e 81 a4 03 02 00 00  00 00 00 00 00 00 00 00  |IN..............|
00010610  00 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00010620  35 cc 2a cf 0b 94 00 00  35 cc 2a cf 0b 94 00 00  |5.*.....5.*.....|
00010630  35 cc 2d fa 8d a4 e9 c7  00 00 00 00 00 00 00 00  |5.-.............|

00010640  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010650  00 00 18 01 00 00 00 00  00 00 00 00 56 5c 1c 43  |............V\.C|
...

むりやり online convertしよう!

さて上記まで見たところ, bigtimeを有効にしても既存のファイルのタイムスタンプはただちに変換されていないことがわかります. 各inodeにフラグがあるので, 次の更新までは元のフォーマットのまま放置していてもかまいせん. xfs_adminがやっていることは結局スーパーブロックのbitを1つ立てているにすぎません.

そんなたかだか1bitのために, umountするなんて嫌じゃないですか? bit1つぐらい実行時で立ててもいいじゃん. 理論上できるでしょ. 立ててやりましょう. kernel moduleで.

!!! 以下のコードは絶対に実行しないようにしましょう. ファイルシステムやシステムが破壊される可能性があります. 責任は一切とれません !!!

お使いのエディタから以下のコードを書きました. 今日はnvimにしました.

"/mnt/tmp"にmountされているXFSを確認し, そのfeatureフラグを立てます. そのフラグはメモリ上にしか影響しないので, diskに書かれるsuperblockのフラグも立てて, 後でxfs_repairに怒られないようにします. ただそれだけのkernel moduleです.

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/module.h>
#include <linux/nsproxy.h>
#include <linux/namei.h>

#if 0
#include "xfs/xfs.h"
#include "xfs/libxfs/xfs_types.h"
#include "xfs/libxfs/xfs_format.h"
#include "xfs/libxfs/xfs_shared.h"
#include "xfs/libxfs/xfs_trans_resv.h"
#include "xfs/xfs_mount.h"
#endif

static int init_xfs_conv(void)
{
    return 0;
    const char *MNT_DIR = "/mnt/tmp";

    pr_info("xfs online convert start");

    struct path path __free(path_put) = {};
    int ret;

    ret = kern_path(MNT_DIR, LOOKUP_FOLLOW, &path);
    if (ret) {
        pr_err("failed to open %s %d", MNT_DIR, ret);
        return ret;
    }

    struct super_block *sb = path.mnt->mnt_sb;
    pr_info("xfs sb %px", sb);
    struct xfs_mount *mp = XFS_M(sb);
    pr_info("xfs magic %x", mp->m_sb.sb_magicnum);
    if (mp->m_sb.sb_magicnum != XFS_SB_MAGIC) {
        pr_err("invalid xfs magic");
        return -EINVAL;
    }

    uint64_t features = READ_ONCE(mp->m_features);
    pr_info("xfs feature %llx", features);
    if (features & XFS_FEAT_BIGTIME) {
        pr_info("already has XFS_FEAT_BIGTIME");
        return 0;
    }
    return 0;

    features |= XFS_FEAT_BIGTIME;
    WRITE_ONCE(mp->m_features, features);

    uint64_t sb_feautres = READ_ONCE(mp->m_sb.sb_features_incompat);
    sb_feautres |= XFS_SB_FEAT_INCOMPAT_BIGTIME;
    WRITE_ONCE(mp->m_sb.sb_features_incompat, sb_feautres);

    pr_info("BIGTIME enabled");

    return 0;
}

static void exit_xfs_conv(void)
{
    pr_info("xfs online convert finish");
}

module_init(init_xfs_conv);
module_exit(exit_xfs_conv);

MODULE_DESCRIPTION("DO NOT USE!!! Online Convert XFS!");
MODULE_LICENSE("GPL");

同様に, XFSをmountして"old"をtouchします. さっきのコードをコンパイルしてinsmodしてrmmodします. なんかいい感じに動いたみたい.

$ (mountとか)
$ sudo touch -t 202410160900 /mnt/tmp/old
$ sudo insmod ./xfs-conv.ko; sudo rmmod xfs-conv; dmesg|grep xfs_conv:
[86968.662739] xfs_conv: xfs online convert start
[86968.662765] xfs_conv: xfs sb ffff8ad326471000
[86968.662768] xfs_conv: xfs magic 58465342
[86968.662769] xfs_conv: xfs feature 49ff6ab
[86968.662771] xfs_conv: BIGTIME enabled
[86968.694864] xfs_conv: xfs online convert finish

できたかな?ということで, 同様に"new"を作ってsyncします. それぞれdumpを見ます.

まずは"old"の方. ちゃんと昔のタイムスタンプ.

$ hexdump -C -s $((131 * 512)) -n $((512 * 1)) xfs.img
00010600  49 4e 81 a4 03 02 00 00  00 00 00 00 00 00 00 00  |IN..............|
00010610  00 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00010620  67 0f 02 00 00 00 00 00  67 0f 02 00 00 00 00 00  |g.......g.......|
00010630  67 0f 46 7e 03 cf 92 12  00 00 00 00 00 00 00 00  |g.F~............|

00010640  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010650  00 00 18 01 00 00 00 00  00 00 00 00 e2 73 9e fe  |.............s..|
...

そして"new"の方. やったぜ新しいタイムスタンプフォーマットで書かれています.

$ hexdump -C -s $((132 * 512)) -n $((512 * 1)) xfs.img
00010800  49 4e 81 a4 03 02 00 00  00 00 00 00 00 00 00 00  |IN..............|
00010810  00 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00010820  35 cc 2a cf 0b 94 00 00  35 cc 2a cf 0b 94 00 00  |5.*.....5.*.....|
00010830  35 cc 3a cc e7 3c 8c d0  00 00 00 00 00 00 00 00  |5.:..<..........|

00010840  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010850  00 00 18 01 00 00 00 00  00 00 00 00 41 48 72 11  |............AHr.|
...

"old"の方をもう一度touchしてsyncなどします. dumpを見ると…新しいフォーマットに変換されました.

$ sudo touch  /mnt/tmp/old
$ hexdump -C -s $((131 * 512)) -n $((512 * 1)) xfs.img
00010600  49 4e 81 a4 03 02 00 00  00 00 00 00 00 00 00 00  |IN..............|
00010610  00 00 00 01 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00010620  35 cc 2a cf 0b 94 00 00  35 cc 2a cf 0b 94 00 00  |5.*.....5.*.....|
00010630  35 cc 3a d4 a5 d3 2f 09  00 00 00 00 00 00 00 00  |5.:.../.........|

00010640  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00010650  00 00 18 01 00 00 00 00  00 00 00 00 e2 73 9e fe  |.............s..|
...

つまり, kernel moduleを書いてbitを立てればいいだけなので, XFSのbigtimeはonline変換できて安心だよということになります.

…というのは言いすぎにしろ, このように(trivialなcaseだけど)実際にonline変換はできるのであるから, そのうちちゃんと実装されるかもしれませんね.

convertできない場合

XFSのbigtime有効化について, offlineでの変換・onlineでの変換を見てきました. しかし, このような変換ができない場合もあります. 現在のXFSのフォーマットはv5ですが, v4以前のフォーマットのものは, このような変換はできません. v4からv5へはバックアップ・リストア以外に方法はないです. v5が出たのは2013年なので, その頃より前のファイルシステムではv4であるかもしれません.

v5では, メタデータCRCがついたり, 全てのメタデータがself describing metadata (metadata冒頭に"IN"など, マジックが入る)となるなどの変更が入っています. この変更はあまりに大規模なので, バックアップ・リストアになるしかないのでしょう.

ということでみなさん快適な2038年をお過ごしください.

P.S.

こういう話が好きな人は11月9日に富山に行くといいようですよ.

kernelvm.connpass.com