btrfs? Dropboxさん, あなたが見ているのはext4ですよ

注意: この記事の内容はわりと危険な部分があります. 使いたくなってもよく注意して理解して使ってください

わたしもまだ大して実動テストしてません. Dropboxの中身が消失したり, FSがこわれたり, なんかおもしろいことになっても責任は一切とれません. 技術的な観賞用に留めるのが無難です

はじめに

Dropboxext4しか対応しなくなるらしいですね. btrfs愛用者としてはハチャメチャめんどいです. ということで, なんとかしたくなりますね.

1つの方法として, 下の記事のようにbtrfs上にext4のイメージファイルを作ってあげるという方法があります.

qiita.com

ただこの方法だと, kernelの中で無駄にext4Dropboxのためだけに動かすことになりますね. ext4のmoduleなんか置いておきたくない. そう思いませんか

システムコールの結果を書きかえる

なんとかDropboxをだましてみましょう.

Dropboxは, statfs()システムコールを使ってファイルシステムの種類を取得しているようです. では, このシステムコールの結果を書きかえてやればDropboxをだませるんじゃない?

statfs()システムコールを呼ぶと, 以下のstruct statfsが返ってきます. このうち, f_typeにファイルシステムマジックナンバーが書かれています. ここをext4のものに書きかえてやればいいわけです.

struct statfs {
__fsword_t f_type; /* Type of filesystem (see below) */
__fsword_t f_bsize; /* Optimal transfer block size */
fsblkcnt_t f_blocks; /* Total data blocks in filesystem */
fsblkcnt_t f_bfree; /* Free blocks in filesystem */
fsblkcnt_t f_bavail; /* Free blocks available to
unprivileged user */
fsfilcnt_t f_files; /* Total file nodes in filesystem */
fsfilcnt_t f_ffree; /* Free file nodes in filesystem */
fsid_t f_fsid; /* Filesystem ID */
__fsword_t f_namelen; /* Maximum length of filenames */
__fsword_t f_frsize; /* Fragment size (since Linux 2.6) */
__fsword_t f_flags; /* Mount flags of filesystem
(since Linux 2.6.36) */
__fsword_t f_spare[xxx];
/* Padding bytes reserved for future use */
};
view raw statfs.c hosted with ❤ by GitHub

gist1b945b7172bf97560cf9bf15aa73f5ce

livepatchで書きかえる

こんな時にべんりなのがLinux kernelのLivepatchなんですよ*1

この機能を使うと, カーネルのいろんな関数を自分の書いたものに差し換えられてべんりです

statfs()システムコールを呼ぶと, いろいろあってuser_statfs()関数にたどりつきます. このコードは下のようにvfs_statfs()を呼んでいます. vfs_statfs()がさらにファイルシステムごとのstatfsの関数を呼びます.

本当はvfs_statfs()を書きかえると, fstatfs()システムコールにも対応できていいんですが, シンボルの解決が面倒なのでこっちで済ませます.

int user_statfs(const char __user *pathname, struct kstatfs *st)
{
struct path path;
int error;
unsigned int lookup_flags = LOOKUP_FOLLOW|LOOKUP_AUTOMOUNT;
retry:
error = user_path_at(AT_FDCWD, pathname, lookup_flags, &path);
if (!error) {
error = vfs_statfs(&path, st);
path_put(&path);
if (retry_estale(error, lookup_flags)) {
lookup_flags |= LOOKUP_REVAL;
goto retry;
}
}
return error;
}
view raw user_statfs.c hosted with ❤ by GitHub

gist261c74c6dfdf19f76bdc58e829d99ae5

これを書きかえて, こんなふうな関数を作ります. vfs_statfs()が正しく終了したら, st->f_typeEXT4_SUPER_MAGIC を書いてるだけです

int livepatch_user_statfs(const char __user *pathname, struct kstatfs *st)
{
struct path path;
int error;
unsigned int lookup_flags = LOOKUP_FOLLOW|LOOKUP_AUTOMOUNT;
retry:
error = user_path_at(AT_FDCWD, pathname, lookup_flags, &path);
if (!error) {
error = vfs_statfs(&path, st);
if (!error)
st->f_type = EXT4_SUPER_MAGIC;
path_put(&path);
if (retry_estale(error, lookup_flags)) {
lookup_flags |= LOOKUP_REVAL;
goto retry;
}
}
return error;
}

gist5b144de1e2396aab8e717a63c39561f8

これにlivepatchするためのなんやかんやを書いて, kernel moduleとしてビルドできるようにしたものが, 以下のファイル

github.com

動かしてみる

これでDropboxをだませるんでしょうか? やってみましょう

まずは比較用にbtrfs上のdropboxをvanilla kernelで動かします. わたしは通知を表示するのにdunstを使っていますが, dunst -printで実行してやると, 通知内容を表示してくれるのでべんりです. その上でdropboxを起動すると, 以下のようにうるさい身勝手な通知がとんできてます

$ dunst -print
{
	appname: 'Dropbox'
	summary: 'Move Dropbox by Nov 2018'
	body: 'Dropbox is on a file system that will no longer be supported. Details...'
	icon: 'dialog-information'
	raw_icon set: false
	category: 
	timeout: 10000
	urgency: NORMAL
	transient: 0
	formatted: '<strong>Move Dropbox by Nov 2018</strong>
Dropbox is on a file system that will no longer be supported. Details...'
	fg: #ffffff
	bg: #285577
	frame: #aaaaaa
	id: 2
	actions:
	{
		[default,default]
	}
	actions_dmenu: #default [Dropbox]
	script: (null)
}

ついでにコマンドでも確かめてやるとこんな感じ

$ stat -f ~/Dropbox/
  File: "/home/naota/Dropbox/"
    ID: 546b23f0cd0e1903 Namelen: 255     Type: btrfs
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 116433920  Free: 103094488  Available: 102796532
Inodes: Total: 0          Free: 0

 Type: btrfsで怒られてますね〜

ここでさっきのlivepatch moduleをロードします. ロードすると, user_statfs()関数が自分の書いたlivepatch_user_statfs()に置きかえられます.

すると, statコマンドの結果が"Type: ext2/ext3"と変わります. さらにここでdropboxを起動しても, (なにも出てこてないので見せようがないが)さっきのうるさい通知が出てきません.

$ sudo insmod module/fake-ext4-v0.ko
$ stat -f /home/naota/Dropbox
  File: "/home/naota/Dropbox"
    ID: 546b23f0cd0e1903 Namelen: 255     Type: ext2/ext3
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 116433920  Free: 103096727  Available: 102798307
Inodes: Total: 0          Free: 0

めでたくDropboxを黙らせることに成功しました. やったぜ

 

しかし, これは随分と大ざっぱなことをやっています. dropbox以外でも全てのstatfs()の結果がext4に書きかわっているわけです. 大丈夫なんでしょうか?

たとえば, うちはFSのrootからbtrfsなんですが…これもext4に見えています.

$ stat -f /
  File: "/"
    ID: 546b23f0cd0e1852 Namelen: 255     Type: ext2/ext3
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 116433920  Free: 103093875  Available: 102795927
Inodes: Total: 0          Free: 0

 ここでbtrfsコマンドでsubvolumeの一覧を見るコマンドをたたいてみると……なんということでしょう, コマンドが動かなくなってしまいました

$ sudo btrfs subvolume list /
ERROR: not a btrfs filesystem: /
ERROR: can't access '/'

Dropboxディレクトリだけ書きかえる

さっきの方法だとあまりに大雑把でいろいろ影響が出てきそうです. やばくならないうちに, livepatchを解除しておきましょう.

$ echo 0 | sudo tee /sys/kernel/livepatch/fake_ext4_v0/enabled
$ rmmod fake_ext4_v0 # しばらくたたないとダメな時もある

なんとかDropboxディレクトリだけext4に見せかけることができないでしょうか?

これにはstatfs()に指定されたパスを見ていくなどいくつか方法はあると思います. うちではDropboxディレクトリが個別のbtrfsのsubvolumeになっているので, ファイルシステムID(FSID)で識別することにしました.

さきほどの"stat -f ~/Dropbox", "stat -f /"のIDの部分を見てください. "~/Dropbox"では"ID: 546b23f0cd0e1903"で, "/"では"ID: 546b23f0cd0e1852"になっています. これは作成したファイルシステム固有のIDで, btrfsではsubvolumeごとに別のIDになります. ここでDropboxディレクトリかどうか見分けてやりましょう.

"stat -f"で見たIDを定義して, さっきはエラーチェックだけしていた部分にFSIDの比較を追加します.

#define MY_FSID_VAL0 0x546b23f0
#define MY_FSID_VAL1 0xcd0e1903
...
if (!error &&
st->f_fsid.val[0] == MY_FSID_VAL0 &&
st->f_fsid.val[1] == MY_FSID_VAL1)
st->f_type = EXT4_SUPER_MAGIC;

gist5579cbbdea96629710934be8e86e777f

ではこれで, livepatchしてみましょう. moduleをロードしてstatしてみると? ちゃんと/はbtrfsでありながら, Dropboxext4に見えています. もちろんdropboxも変な通知をとばしてきませんし, btrfsコマンドも動いている感じです.

$ sudo insmod module/fake-ext4-v1.ko
$ stat -f / ~/Dropbox/ File: "/" ID: 546b23f0cd0e1852 Namelen: 255 Type: btrfs Block size: 4096 Fundamental block size: 4096 Blocks: Total: 116433920 Free: 103093043 Available: 102795175 Inodes: Total: 0 Free: 0 File: "/home/naota/Dropbox/" ID: 546b23f0cd0e1903 Namelen: 255 Type: ext2/ext3 Block size: 4096 Fundamental block size: 4096 Blocks: Total: 116433920 Free: 103093043 Available: 102795175 Inodes: Total: 0 Free: 0 $ sudo btrfs subvolume list / ID 260 gen 79047 top level 5 path var/log ID 261 gen 79050 top level 5 path home/naota ID 262 gen 79045 top level 5 path var/tmp ID 263 gen 79003 top level 5 path etc ID 265 gen 23700 top level 5 path srv ID 266 gen 493 top level 5 path var/lib/portables ID 267 gen 494 top level 5 path var/lib/machines ID 340 gen 79045 top level 261 path home/naota/Dropbox

終わりに

Dropboxの中がやばいことになる可能性はあるので特に覚悟がなければ片付けておきましょう.

$ echo 0 | sudo tee /sys/kernel/livepatch/fake_ext4_v1/enabled
$ rmmod fake_ext4_v1 # しばらくたたないとダメな時もある

コードはここにまとめておきます. 覚悟があれば livepatchしてdropboxを黙らせてみるのもいいでしょう. kernelの更新で, user_statfs()が書き変わったら追従するなどしないとすごいことになることもあると思います.

github.com

 

*1:本来こんなことに使うのではないと思うが