TenForward

技術ブログ。はてなダイアリーから移転しました

ShiftFS ふたたび 〜 Ubuntu 19.04で導入された ShiftFS を試してみた

ちょうど 2 年ほど前、Open Source Summit で話を聞いて ShiftFS について試したことがありました。

tenforward.hatenablog.com

その後はとりあえずカーネルにパッチを当てたりはしてましたが、あまり使わないままでした。少し前に gihyo.jp の Ubuntu の記事を読んでいると、

discoのカーネルフリーズが4月4日に迫る中,興味深いパッチが投稿されています。パッチの中身は「ShiftFS」と呼ばれる,コンテナ環境でのセキュリティ機能を提供するためのラッパーファイルシステムです
https://gihyo.jp/admin/clip/01/ubuntu-topics/201903/29

なんて書かれています。その後の記事で

ShiftFSは, 前回取り上げた後,4月4日に無事にマージに辿り着いています。
https://gihyo.jp/admin/clip/01/ubuntu-topics/201904/19

と書かれていたので、また変わった所がないか調べて試してみたいなと思っていました。

そこでようやく重い腰を上げて、Ubuntu 19.04 環境を用意して試してみました。

ShiftFS について

詳しくは先に紹介した 2 年前の記事を参照してください。

簡単に言うと、非特権コンテナを起動する場合、どの uid/gid で起動するかは一般的に決まっているわけではないですが、世間で流通しているコンテナイメージ内のファイルの所有権は作成時点で一意に決まりますし、自作のコンテナイメージでも同様でしょう。

そこで非特権コンテナを起動するときには、それを起動するユーザの uid/gid に合わせて chown してあげる必要があります。

この問題を解消するために考えられたのが ShiftFS です。

ShiftFS は OverlayFS のように重ね合わせのファイルシステムとして実装されているようです。元のファイルシステムを「下層」(lower filesystem)とし、その上に「上層」(upper filesystem)のファイルシステムを重ねて所有権を調整しているのでしょう(たぶん)。

この ShiftFS は Ubuntu 19.04 の 5.0 カーネルにパッチを適用して使えるようにしており、upstream のカーネルには導入されていない機能です(されそうなのかしら?)。

Ubuntu は 12.04 だったか 14.04 の頃も、まだ upstream な Linux カーネルに導入されていない OverlayFS を先取りして入れていました(カーネルに入ったときには仕様が変わっていて LXC にパッチ送ったりしたのはいい思い出)。また 16.04 なんかでも、かなり後のカーネルで導入される機能がバックポートされていたりして、かなり意欲的に自分たちの求める機能を入れてきます。

ShiftFS によるマウント

マウント方法は2 年前の記事と大きく変わっていません。マウントオプションが増えた程度でしょうか。

マウントは 2 段階で行います。

ShiftFS がないとき

これも2 年前の記事と同じなのですが、簡単に紹介しておきましょう。

ここではファイルシステムとして LXC コンテナのイメージを展開して使用しています(alpineイメージ)。検証のために一部ディレクトリの所有権などをいじっています。root 権限で作成されたコンテナです。

コンテナファイルシステムの root(/)は /var/lib/lxc/ct01/rootfs です。

$ sudo ls -l /var/lib/lxc/ct01/rootfs
total 68
drwxr-xr-x  2 root root 4096 Jul  1 13:00 bin
drwxr-xr-x  3 root root 4096 Jul  2 07:17 dev
drwxr-xr-x 19 root root 4096 Jul  2 07:17 etc
drwxr-xr-x  2 root root 4096 Jun 19 17:14 home
drwxr-xr-x  6 root root 4096 Jul  1 13:00 lib
drwxr-xr-x  5 root root 4096 Jun 19 17:14 media
  : (snip)

上の例のように rootfs 以下は root 所有です。

現在のユーザーをコンテナ内の rootマッピングして User Namespace を作成します。

$ id -u && id -g
1000
1000
$ unshare --pid --user --map-root-user --mount --mount-proc --fork -- /bin/bash
# id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)
# ls -l /var/lib/lxc/ct01/rootfs/
total 68
drwxr-xr-x  2 nobody nogroup 4096 Jul  1 13:00 bin
drwxr-xr-x  3 nobody nogroup 4096 Jul  2 07:17 dev
drwxr-xr-x 19 nobody nogroup 4096 Jul  2 07:17 etc
drwxr-xr-x  2 nobody nogroup 4096 Jun 19 17:14 home
drwxr-xr-x  6 nobody nogroup 4096 Jul  1 13:00 lib
drwxr-xr-x  5 nobody nogroup 4096 Jun 19 17:14 media
  : (snip)

上のように uid/gid=1000/1000 のユーザーをコンテナ内の uid/gid=0/0 にマッピングしただけなので、マッピングされていない uid/gid については nobody/nogroup の 65534/65534 となってしまっています。

ShiftFS があるとき

まずはコンテナの root(/)である /var/lib/lxc/ct01/rootfs を ShiftFS の下層(lower filesystem)としてマークします。この場合、マウントオプションに mark を指定します。

$ sudo mount -t shiftfs -o mark /var/lib/lxc/ct01/rootfs /var/lib/lxc/ct01/rootfs
[sudo] password for ubuntu: 
$ grep shiftfs /proc/self/mountstats 
device /var/lib/lxc/ct01/rootfs mounted on /var/lib/lxc/ct01/rootfs with fstype shiftfs

ShiftFS としてマウントされています(このイメージは root 所有なので、ここでは root ユーザーで mark しています)。

ここで、さきほどと同じように User Namespace を作成します。

$ unshare --pid --user --map-root-user --mount --mount-proc --fork -- /bin/bash
# id
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)

そしてさきほど mark した ShiftFS マウントを使って、権限のあるディレクトリに ShiftFS マウントしてみます。

# mount -t shiftfs /var/lib/lxc/ct01/rootfs/ /home/ubuntu/mnt
# grep shiftfs /proc/self/mountstats 
device /var/lib/lxc/ct01/rootfs mounted on /var/lib/lxc/ct01/rootfs with fstype shiftfs
device /var/lib/lxc/ct01/rootfs mounted on /home/ubuntu/mnt with fstype shiftfs

マウントされています。マウント先の /home/ubuntu/mnt を確認してみると、

# ls -l /home/ubuntu/mnt
total 68
drwxr-xr-x  2 root root 4096 Jul  1 13:00 bin
drwxr-xr-x  3 root root 4096 Jul  2 07:17 dev
drwxr-xr-x 19 root root 4096 Jul  2 07:17 etc
drwxr-xr-x  2 root root 4096 Jun 19 17:14 home
drwxr-xr-x  6 root root 4096 Jul  1 13:00 lib
drwxr-xr-x  5 root root 4096 Jun 19 17:14 media
  : (snip)

ちゃんと root 所有になっており、ShiftFS が効いていることがわかります。

ちなみにこの「下層」(mark)のマウントはホストのNamespaceで行っているので、ホストから見えますが、ホストでは「上層」は見えません。

$ grep shiftfs /proc/self/mountstats (ホスト上で実行)
device /var/lib/lxc/ct01/rootfs mounted on /var/lib/lxc/ct01/rootfs with fstype shiftfs
$

コンテナ起動後は、余計な情報を見せないためにホスト上ではこの mark 用 ShiftFS は umount できます。

動きは 2 年前の変わっていないようです。

passthrough オプション

Ubuntuのパッチでは ShiftFS のオプションとして passthrough オプションが追加されているようです。2 年前の記事の時点ではおそらくなかったオプションだと思います。

さきほどの例では何も指定していませんので、passthrough=0 が指定されているのと同じ意味になるようです。

さきほどの例でマウントした ShiftFS で 1 が指定されていない効果を確認してみます。

# stat -f /home/ubuntu/mnt/root
  File: "/home/ubuntu/mnt/root"
    ID: ad6dc6d23bfb15c1 Namelen: 255     Type: UNKNOWN (0x6a656a62)
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 4111327    Free: 3014986    Available: 2801214
Inodes: Total: 1048576    Free: 972147

ファイルシステムタイプとして Type: UNKNOWN と返っています。

(2 の指定は気軽に試すには私にはハードルが高そうなので、)とりあえず 3 を指定してみます。

$ sudo mount -t shiftfs -o mark,passthrough=3 /var/lib/lxc/ct01/rootfs /var/lib/lxc/ct01/rootfs
$ grep shiftfs /proc/self/mountinfo 
540 28 0:53 / /var/lib/lxc/ct01/rootfs rw,relatime shared:294 - shiftfs /var/lib/lxc/ct01/rootfs rw,mark,passthrough=3

passthrough=3 付きでマウントされています。

では User namespace を作成しましょう。

$ unshare --pid --user --map-root-user --mount --mount-proc --fork -- /bin/bash
# mount -t shiftfs -o passthrough=3 /var/lib/lxc/ct01/rootfs /home/ubuntu/mnt
# stat -f /home/ubuntu/mnt/root
  File: "/home/ubuntu/mnt/root"
    ID: ad6dc6d23bfb15c1 Namelen: 255     Type: ext2/ext3
Block size: 4096       Fundamental block size: 4096
Blocks: Total: 4111327    Free: 3019139    Available: 2805367
Inodes: Total: 1048576    Free: 972149

stat コマンドで確認すると Type: ext2/ext3 と返ってきています。

2 の ioctl() 周りですが、ShiftFS のパッチ(の一部) を見る限りでは、

static long shiftfs_ioctl(struct file *file, unsigned int cmd,
              unsigned long arg)
{
    switch (cmd) {
    case FS_IOC_GETVERSION:
        /* fall through */
    case FS_IOC_GETFLAGS:
        /* fall through */
    case FS_IOC_SETFLAGS:
        break;
    default:
        return -ENOTTY;
    }

    return shiftfs_real_ioctl(file, cmd, arg);
}

static long shiftfs_compat_ioctl(struct file *file, unsigned int cmd,
                 unsigned long arg)
{
    switch (cmd) {
    case FS_IOC32_GETVERSION:
        /* fall through */
    case FS_IOC32_GETFLAGS:
        /* fall through */
    case FS_IOC32_SETFLAGS:
        break;
    default:
        return -ENOIOCTLCMD;
    }

    return shiftfs_real_ioctl(file, cmd, arg);
}

のような実装になってるので、ここに挙がってるリクエストは許可されているのでしょう(知らんけど)。

LXD

ちなみに LXD ではすでに ShiftFS が使えるようになっています(3.12以降)。

github.com

使い方は次で説明されています。

discuss.linuxcontainers.org