fsopen(2)というシステムコールを試したけどEINVALには勝てなかったよ...という話

すっかり遅くなってしまいすみません、[Linux Advent Calendar 2020](https://qiita.com/advent-calendar/2020/linux) 20日目の記事です。
今日はLinux-5.9.3で提供された[fsopen(2)](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24dcb3d90a1f67fe08c68a004af37df059d74005)を試してみた(けどサンプルを動かすとこまでたどり着けなかった...)という話をしようと思います。

## 背景

個人的に気が向いた時に[The Linux Kernel Archives](https://www.kernel.org/)にアクセスし、Linuxカーネルの新バージョンがリリースされていたら手元でビルドしてみる、という趣味(？)があるのですが、Linux-5.9.3のソースコードにて[fsopen(2)というシステムコールを追加した](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24dcb3d90a1f67fe08c68a004af37df059d74005)というコミットログを見かけました。
fsopen(2)...？聞いたことのないシステムコールだな...と思ったのでちょっと試してみたというのが今日のお話になります。

## fsopen(2)とは？

fsopen(2)は新しいシステムコールのようで、ブロックデバイスを渡すことでプロセス実行時に動的にファイルシステムをmountするための機能に見えます(具体的なユースケースが個人的には明確化できていないです...)。

fsopen(2)は `linux-5.9.3fs/fsopen.c` で定義されています。引数 `_fs_name` で指定したファイルシステムへの操作用ファイルディスクリプタを取得するような動作に見えます。

```c
108 /*
109  * Open a filesystem by name so that it can be configured for mounting.
110  *
111  * We are allowed to specify a container in which the filesystem will be
112  * opened, thereby indicating which namespaces will be used (notably, which
113  * network namespace will be used for network filesystems).
114  */
115 SYSCALL_DEFINE2(fsopen, const char __user *, _fs_name, unsigned int, flags)
116 {
...
128     fs_name = strndup_user(_fs_name, PAGE_SIZE);
129     if (IS_ERR(fs_name))
130         return PTR_ERR(fs_name);
131
132     fs_type = get_fs_type(fs_name);
133     kfree(fs_name);
134     if (!fs_type)
135         return -ENODEV;
136
137     fc = fs_context_for_mount(fs_type, 0);
...
148     return fscontext_create_fd(fc, flags & FSOPEN_CLOEXEC ? O_CLOEXEC : 0);
```

引数 `_fs_name` に指定する値は[fsopen(2)のコミットメッセージ](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24dcb3d90a1f67fe08c68a004af37df059d74005)にサンプルの形で記載されています。以下はコミットメッセージからの引用です。 `ext4` とか `afs` といったファイルシステム名をそのまま指定すれば良さそうです。

```
For example:

	sfd = fsopen("ext4", FSOPEN_CLOEXEC);
...
	sfd = fsopen("afs", -1);
```

さらにコミットメッセージをみると、fsopen(2)で取得したファイルディスクリプタに対して `fsconfig(2)` を呼び出すことで必要なオプションを設定して行くようです。そして最後に `fsmount(2)` `move_mount(2)` を呼ぶことで実行時にファイルシステムをmountするような挙動に見えます。

```
	sfd = fsopen("ext4", FSOPEN_CLOEXEC);
	fsconfig(sfd, FSCONFIG_SET_PATH, "source", "/dev/sda1", AT_FDCWD);
	fsconfig(sfd, FSCONFIG_SET_FLAG, "noatime", NULL, 0);
	fsconfig(sfd, FSCONFIG_SET_FLAG, "acl", NULL, 0);
	fsconfig(sfd, FSCONFIG_SET_FLAG, "user_xattr", NULL, 0);
	fsconfig(sfd, FSCONFIG_SET_STRING, "sb", "1", 0);
	fsconfig(sfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0);
	fsinfo(sfd, NULL, ...); // query new superblock attributes
	mfd = fsmount(sfd, FSMOUNT_CLOEXEC, MS_RELATIME);
	move_mount(mfd, "", sfd, AT_FDCWD, "/mnt", MOVE_MOUNT_F_EMPTY_PATH);
```

### サンプルを例にユーザランド側からfsopen(2)を呼び出してみる

`Linux-5.9.3/samples/vfs/test-fsmount.c` には `fsopen(2)` を使用するサンプルが用意されています。これを使用して実際に `fsopen(2)` の挙動を見てみましょう。
(が、後述しますが **現時点ではサンプルをうまく動かすことができませんでした** ...(T_T))

#### サンプルをビルドする際のハマり所

`Linux-5.9.3/samples/vfs/test-fsmount.c` と [fsopen(2)のコミットメッセージ](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24dcb3d90a1f67fe08c68a004af37df059d74005)内のサンプルコードを比べると、オプション名が微妙に異なったりしているケースがあるようです。実際にサンプルを動かす際には、ソースコードと照らし合わせながら、現状の定義(マクロ定数とか)に読み替える必要がありそうです。

また、馴染み深い(？) `open(2)` のようなシステムコールは、ユーザランド向けのシステムコール呼び出し定義が用意されていますが、 `Linux-5.9.3` をビルドしてカーネルをインストールするケースにおいては、この部分を自前でなんとかする形になります。

`Linux-5.9.3/samples/vfs/test-fsmount.c` を見ると以下のような定義があり、 `__NR_fsopen` にシステムコール番号を定義しておき、 `syscall()` でシステムコールを直接呼び出すという実装になっています。さらに、これらの値は `-1` になっており、ソースコードコメントの"Hope -1 isn't a syscall"から察するに、 `fsopen(2)` などのシステムコール番号をちゃんと設定しないとダメそうです...。

```c
/* Hope -1 isn't a syscall */
#ifndef __NR_fsopen
#define __NR_fsopen -1
#endif
#ifndef __NR_fsmount
#define __NR_fsmount -1
#endif
#ifndef __NR_fsconfig
#define __NR_fsconfig -1
#endif
#ifndef __NR_move_mount
#define __NR_move_mount -1
#endif
...
static inline int fsopen(const char *fs_name, unsigned int flags)
{
        return syscall(__NR_fsopen, fs_name, flags);
}
```

#### システムコール番号を把握する

カーネルソースコード内で `__NR_fsopen` を検索してみると、以下のようなマクロ定数が見つかります。どうやらこの値をサンプルプログラム側で使用すれば良さそうです。

```sh
$ find arch/x86 -type f | grep \\.h | xargs egrep '__NR_fsopen|__NR_fsmount|__NR_fsconfig|__NR_move_mount' | grep unistd_64
arch/x86/include/generated/uapi/asm/unistd_64.h:#define __NR_move_mount 429
arch/x86/include/generated/uapi/asm/unistd_64.h:#define __NR_fsopen 430
arch/x86/include/generated/uapi/asm/unistd_64.h:#define __NR_fsconfig 431
arch/x86/include/generated/uapi/asm/unistd_64.h:#define __NR_fsmount 432
```

#### 必要なヘッダファイルの用意

サンプルファイルは他にもヘッダファイルを参照しており、Linux環境にインストールされているヘッダファイルではなく、 `Linux-5.9.3` に含まれているヘッダファイルを使用する必要があります。

ここでは以下の手順で、サンプルプログラムと同じ場所に `include/linux` と `uapi` ディレクトリを作成し、そこに必要なヘッダファイルを配置しました。

```sh
$ cd /usr/src/samples/vfs/
$ mkdir -p include/linux
$ cp -r ../../include/linux/ ./include/
$ cp -r ../../include/uapi/ ./uapi
```

#### サンプルのビルド

以下の手順でサンプルプログラムをコンパイルできます(ただしコンパイル時にwarningが出ます...)。

```sh
$ gcc -o test-fsmount test-fsmount.c -I./uapi -I./include/
```

### サンプル用のext4ファイルシステムの作成

サンプルプログラム内の `fsopen(2)` で参照するファイルシステムとして、 `/dev/sdb` 上にext4のファイルシステムを作成しておきます。

```sh
$ dmesg | grep sdb
[    4.375191] sd 3:0:0:0: [sdb] 30714 512-byte logical blocks: (15.7 MB/15.0 MiB)
[    4.375750] sd 3:0:0:0: [sdb] Write Protect is off
[    4.376263] sd 3:0:0:0: [sdb] Mode Sense: 00 3a 00 00
[    4.376284] sd 3:0:0:0: [sdb] Write cache: enabled, read cache: enabled, doesn't support DPO or FUA
[    4.385157] sd 3:0:0:0: [sdb] Attached SCSI disk
$ sudo mkfs.ext4 /dev/sdb
$ sudo mount /dev/sdb /mnt
$ df -h | egrep 'ファイル|mnt'
ファイルシス   サイズ  使用  残り 使用% マウント位置
/dev/sdb          14M  252K   13M    2% /mnt
$
$ sudo umount /mnt
```

### サンプルプログラムを実行してみる

プログラムを実行する前に、サンプルコードをざっと見てみます。 `fsopen(2)` でファイルディスクリプタを取得し、 `E_fsconfig` (関数内で `fsconfig(2)` を呼んでいる)でファイルシステムのパラメータ設定、 `fsmount(2)` と `move_mount(2)` でファイルシステムのmountという流れになっています。

```c
#define __NR_fsopen     430
#define __NR_fsmount    432
#define __NR_fsconfig   431
#define __NR_move_mount 429
...
int main(int argc, char *argv[])
{
        int fsfd, mfd;

        /* Mount a publically available AFS filesystem */
        fsfd = fsopen("ext4", FSOPEN_CLOEXEC);
        if (fsfd == -1) {
                perror("fsopen");
                exit(1);
        }

        E_fsconfig(fsfd, FSCONFIG_SET_STRING, "source", "/dev/sdb", 0);
        E_fsconfig(fsfd, FSCONFIG_SET_FLAG, "noatime", NULL, 0);
        E_fsconfig(fsfd, FSCONFIG_SET_FLAG, "acl", NULL, 0);
        E_fsconfig(fsfd, FSCONFIG_SET_FLAG, "user_xattr", NULL, 0);
        E_fsconfig(fsfd, FSCONFIG_SET_STRING, "sb", "1", 0);

        mfd = fsmount(fsfd, FSMOUNT_CLOEXEC, MOUNT_ATTR_RDONLY|MOUNT_ATTR_NOATIME);
        if (mfd < 0)
                mount_error(fsfd, "fsmount");
        E(close(fsfd));

        if (move_mount(mfd, "", AT_FDCWD, "/mnt", MOVE_MOUNT_F_EMPTY_PATH) < 0) {
                perror("move_mount");
                exit(1);
        }
...
```

が、挙動を調べるためにサンプルプログラムを実行すると"Invalid argument"で `fsmount(2)` が失敗してしまいます...。

```sh
$ ./test-fsmount
fsmount: Invalid argument
```

### fsmount(2)のエラー個所を探す

エラーメッセージは"Invalid argument"なので、システムコール的には `EINVAL` を返しているはずです。 `fsmount(2)` のソースコードを参照しながら、エラーの原因を探ってゆきましょう。

```sh
/*
 * Create a kernel mount representation for a new, prepared superblock
 * (specified by fs_fd) and attach to an open_tree-like file descriptor.
 */
SYSCALL_DEFINE3(fsmount, int, fs_fd, unsigned int, flags,
        unsigned int, attr_flags)
{
...
    if ((flags & ~(FSMOUNT_CLOEXEC)) != 0)
        return -EINVAL;

    if (attr_flags & ~(MOUNT_ATTR_RDONLY |
               MOUNT_ATTR_NOSUID |
               MOUNT_ATTR_NODEV |
               MOUNT_ATTR_NOEXEC |
               MOUNT_ATTR__ATIME |
               MOUNT_ATTR_NODIRATIME))
        return -EINVAL;
...
    switch (attr_flags & MOUNT_ATTR__ATIME) {
    case MOUNT_ATTR_STRICTATIME:
        printk(KERN_WARNING "--> MOUNT_ATTR_STRICTATIME\n");
        break;
    case MOUNT_ATTR_NOATIME:
        printk(KERN_WARNING "--> MOUNT_ATTR_NOATIME\n");
        mnt_flags |= MNT_NOATIME;
        break;
    case MOUNT_ATTR_RELATIME:
        printk(KERN_WARNING "--> MOUNT_ATTR_RELATIME\n");
        mnt_flags |= MNT_RELATIME;
        break;
    default:
        printk(KERN_WARNING "--> default\n");
        return -EINVAL;
    }
...
    ret = -EINVAL;
    if (f.file->f_op != &fscontext_fops)
        goto err_fsfd;
...
    /* There must be a valid superblock or we can't mount it */
    ret = -EINVAL;
    if (!fc->root)
        goto err_unlock;
...
err_path:
    path_put(&newmount);
err_unlock:
    mutex_unlock(&fc->uapi_mutex);
err_fsfd:
    fdput(f);
    return ret;
}
```

`EINVAL` を返す可能性のある個所はいくつかあるようです。順に見てゆくと、まず、引数について指定可能な値以外が渡されてきた場合に `EINVAL` になるようです。この点については、有効な引数のみをサンプルプログラム内で指定し、この部分ではエラーにならないようにしています。

`printk()` で通過した個所を確認しながら調べてみると、以下の `if (!fc->root)` に引っかかることで `EINVAL` が返っていることが分かりました。 `fc->root` は `f.file->private_data->root` を参照しており、ソースコードコードコメントを見るにファイルシステムのsuper blockを指しているようです。

```c
    fc = f.file->private_data;
    ...
    /* There must be a valid superblock or we can't mount it */
    ret = -EINVAL;
    if (!fc->root)
        goto err_unlock;
```

...と、ここまでは把握できたのですが、 `fsopen(2)` で渡している `/dev/sdb` は `mkfs.ext4` でファイルシステムを作ってあるため、super blockが見つけれないのも解せないところです...。

今回はこれ以上の調査はできなかったため、とりあえずサンプルプログラムをビルドするところまでで、 `EINVAL` が発生する原因については後ほど調査する感じになりそうです。

## まとめ

Linux-5.9.3で提供されている[fsopen(2)](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24dcb3d90a1f67fe08c68a004af37df059d74005)というシステムコールを試してみました。サンプルプログラムは用意されていますが、現状の実装に合わせて読み替え・修正が必要となっています。また、サンプルプログラム自体をうまく動かせていないため、引き続き調査が必要そうです。