TenForward

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

マウントプロパゲーション(5)〜 mountinfoファイル 〜

これまでの続きです。

これまでと同様に完全に私個人が理解するための資料です。間違いの指摘は大歓迎です。

ここまでの説明でマウント情報を確認するために説明もなく/proc/self/mountinfoファイルを使っていました。少しこのファイルについて説明しておきましょう。/proc/selfは次のようにカレントプロセスの情報へのリンクになっています。

$ ls -l /proc/self
lrwxrwxrwx 1 root root 0  623日  21:55 /proc/self -> 5771/

/proc/[PID]以下にはそのPIDを持つプロセスの情報が収められています。/proc/[PID]/mountinfoは、そのPIDのプロセスから見えるマウント情報が含まれています。1行がひとつのマウントポイントの情報を表しています。

先の説明で使ったmountinfoの一行を見てみましょう。

714 30 0:74 / /root/test/orig/tmp rw,relatime shared:346 - tmpfs tmpfs rw

1行には11のフィールドが含まれています。このうち7番目がマウントプロパゲーションに関係するエントリです。この7番目のフィールドはオプショナルフィールドであり、7番目に上の例で8番目に表示されている-が表示されていれば、7番目のフィールドは存在しないということです(8番目のフィールドは7番目がオプショナルですので区切りのために存在します)。

この7番目のオプショナルフィールドには4種類の文字列が入ります。

表1 オプショナルフィールドの文字列の説明

文字列 説明
shared:X マウントはピアグループ"X"で共有されているsharedマウント
master:X マウントはピアグループ"X"のslaveマウント
propagate_from:X マウントはslaveマウント
unbindable マウントはバインドできない(unbindable)

ここでピアグループとは、同じマウントが伝播するグループで、ピアグループにはカーネルが割り当てる一意なIDが割り当てられます。

sharedの場合

マウントプロパゲーション(1)〜 shared mount 〜 - TenForward」のsharedマウントと同様の操作をした状態でmountinfoファイルを見ると次のようになります。

31 1 252:2 / / rw,relatime shared:1 - ext4 /dev/vda2 rw
131 31 252:2 /root/test/test/orig /root/test/test/bind rw,relatime shared:1 - ext4 /dev/vda2 rw

先の例ではバインドマウントした側と、元の/のマウント側の両方でお互いにマウント操作が伝播していました。上を見ると双方ともshared:1となっており、両方ともsharedでかつIDが1で同じですのでマウントが相互に伝播したわけです。

private場合

次にprivateの説明(マウントプロパゲーション(2)〜 private mount 〜 - TenForward)で--make-privateした際のmountinfoの該当行は次のようになっていました。

613 30 253:0 /root/test/orig /root/test/bind rw,relatime - ext4 /dev/mapper/ubuntu--vg-ubuntu--lv rw

7番目のフィールドが区切りとなる-となっています。他とマウントが共有されないのでオプショナルフィールドが存在しないということです。

slave場合

slaveの場合(マウントプロパゲーション(3)〜 slave mount 〜 - TenForward)は次のようなエントリとなっていました。

30 1 253:0 / / rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-ubuntu--lv rw
133 30 253:0 /root/test/orig /root/test/bind rw,relatime master:1 - ext4 /dev/mapper/ubuntu--vg-ubuntu--lv rw

sharedに設定されている/ではsharedとなっており、IDは1です。それに対するslaveマウントの方はmaster:1となっており、sharedでマウントされている/のID=1のマウントの伝播を受け取ることがわかります。

残りは次回に。

マウントプロパゲーション(4)〜 unbindable mount 〜

引き続きマウントプロパゲーションについて書いていきます。完全に私個人が理解するための資料です。間違いの指摘は大歓迎です。そもそも、このシリーズ、カーネル付属ドキュメントと mount_namespaces(7) に書いてあるんですよねw

tenforward.hatenablog.com

tenforward.hatenablog.com

tenforward.hatenablog.com

unbindableは、そのマウント自身が他でバインドマウントできないようにするための指定です。マウントプロパゲーションとは少し違うものに見えます。しかし、これはprivateと同じでさらにバインドマウントを禁止するフラグがついたものと考えることができます。

unbindableはセキュリティ対策的な意味合いがあります。配下に多数のマウントを持ったマウントポイントがあったとします(例えば / なんかは通常はそうですよね)。その多数のマウントを持ったマウントを再帰的にバインドマウントするオプション(--rbind)を指定して何度もバインドマウントするとどうでしょう? 配下には大量のバインドマウントが再帰的に出現することになり、「バインドマウントボム(bomb)攻撃」とも言える状態になってしまいます。これを防ぐためにこのような指定ができるようになっています。

簡単に試してみましょう。まずはトップレベルのマウントとしてtestというディレクトリを作成し、ここをtmpfsでマウントします。このtestというマウント配下にふたつディレクトリ(ab)を作成し、その片方aもtmpfsをマウントします。

# mkdir test
# mount -t tmpfs tmpfs test (ディレクトリtestをtmpfsマウントする)
# mkdir test/a
# mount -t tmpfs tmpfs test/a (test/aディレクトリを作成しtmpfsマウントする)
# mkdir test/b
# tree .
.
└── test  (←tmpfsマウント)
    ├── a (←tmpfsマウント)
    └── b (←ディレクトリ)

3 directories, 0 files

上のようにtmpfs配下にtmpfsとディレクトリがひとつずつある状態になります。

ここでディレクトbに親ディレクトリであるtestをバインドマウントします。この際、再帰的にマウントするように--rbindを指定します。

# mount --rbind test test/b
# tree .
.
└── test
    ├── a (←tmpfsマウント)
    └── b (←バインドマウント)
        ├── a (←tmpfsマウント)
        └── b

5 directories, 0 files

再帰的にマウントしていますので、b配下のatest/b/a)はtmpfsとしてマウントされたまま見えています。

ここでtest配下にcというディレクトリを作成し、ここも上で行ったbのように再帰的にマウントします。

# mkdir test/c
# mount --rbind test test/c

再帰的にマウントしていますので、c配下にはさきほど再帰的にマウントしたb配下のマウントもすべて再帰的にマウントされています。そして、先に行ったバインドマウントb以下にもcが出現し、その配下でもb配下のマウントが見えています。

# tree .
.
└── test
    ├── a (←tmpfsマウント)
    ├── b (←バインドマウント)
    │   ├── a (←tmpfsマウント)
    │   ├── b
    │   └── c (←バインドマウント)
    │       ├── a
    │       ├── b (←バインドマウント)
    │       │   ├── a (←tmpfsマウント)
    │       │   ├── b
    │       │   └── c
    │       └── c
    └── c (←バインドマウント)
        ├── a (←tmpfsマウント)
        ├── b (←バインドマウント)
        │   ├── a
        │   ├── b
        │   └── c
        └── c

19 directories, 0 files

これが繰り返されると、延々と再帰的なマウントが出現することになり、リソースを食いつぶすDoS攻撃が成り立ちます。このようなことを防ぐためにunbindableが存在します。

# mount -t tmpfs tmpfs test
# mkdir test/{a,b}
# mount -t tmpfs tmpfs test/a

ここまでは先程の例と同じです。さきほどの例では--rbindとだけ指定したところで同時に--make-unbindableを指定してバインドマウントします。

# mount --rbind --make-unbindable test test/b
# tree .
.
└── test
    ├── a (←tmpfsマウント)
    └── b (←バインドマウント)
        ├── a (←tmpfsマウント,unbindable)
        └── b

5 directories, 0 files

ここでさきほどと同様にtest/cを作成し、test--make-unbindableでバインドマウントします。

# mkdir test/c
# mount --rbind --make-unbindable test test/c
# tree .
.
└── test
    ├── a (←tmpfsマウント)
    ├── b (←バインドマウント)
    │   ├── a (←tmpfsマウント,unbindable)
    │   ├── b
    │   └── c
    └── c (←バインドマウント)
        ├── a (←tmpfsマウント)
        ├── b
        └── c

10 directories, 0 files

unbindableを指定した以外は同じ操作を行っていますが、test/b配下もtest/c配下でもバインドマウントは再帰的に行われていません(tmpfsはマウントされています)。

このようにunbindableを指定すると爆発的にバインドマウントが増殖することを防げます。

マウントプロパゲーション(3)〜 slave mount 〜

引き続きマウントプロパゲーションについて書いていきます。完全に私個人が理解するための資料です。間違いの指摘は大歓迎です。

tenforward.hatenablog.com

tenforward.hatenablog.com

shared と private を説明すれば、頭の良い皆さんはもう slave がどのような動きになるのかわかったのではないでしょうか。

slaveは、複数のマウントの間に主従関係が生じます。マスター配下のマウント操作はスレーブ配下に伝播しますが、スレーブ配下のマウント操作はマスターには伝播しません。

これまでと同じような例を用いて説明しましょう。まずはorigbindにバインドマウントします。

# mount --bind orig bind
# cat /proc/self/mountinfo | grep bind
133 30 253:0 /root/test/orig /root/test/bind rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-ubuntu--lv rw

そしてorig側のマウントをsharedに、bind側のマウントをslaveに設定します。

# mount --make-shared /
# mount --make-slave bind
# cat /proc/self/mountinfo
  :(略)
30 1 253:0 / / rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-ubuntu--lv rw
  :(略)
133 30 253:0 /root/test/orig /root/test/bind rw,relatime master:1 - ext4 /dev/mapper/ubuntu--vg-ubuntu--lv rw
# tree .
.
├── bind
│   ├── tmp
│   └── tmp2
└── orig
    ├── tmp
    └── tmp2

6 directories, 0 files

/proc/self/mountinfoを確認すると、orig側のマウントポイントである/はsharedに、bindmasterと書かれており、/のsharedのあとに書かれた番号(1)と同じ番号が書かれています。これはbind/(つまりorig側)のslaveであることを示しています。origにはこれまでの例のようにあらかじめディレクトリがふたつ作成されています。

ここで、これまでの例と同様にorig/tmpにtmpfsをマウントしてファイルを作成しましょう。

# mount -t tmpfs tmpfs orig/tmp
# touch orig/tmp/testfile
# tree .
.
├── bind
│   ├── tmp
│   │   └── testfile
│   └── tmp2
└── orig
    ├── tmp
    │   └── testfile
    └── tmp2

6 directories, 2 files

orig側はsharedですのでbind側にも同じマウントが伝播し、同じようなツリーが見えます。

それでは逆にbind/tmp2にtmpfsをマウントし、ファイルを作成します。

# mount -t tmpfs tmpfs bind/tmp2
# touch bind/tmp2/testfile2
# tree .
.
├── bind
│   ├── tmp
│   │   └── testfile
│   └── tmp2
│       └── testfile2
└── orig
    ├── tmp
    │   └── testfile
    └── tmp2

6 directories, 3 files

確認してみると、bind/tmp2で行ったtmpfsのマウントはorigでは見えていないことがわかります。つまりorig側のマウントはbind側に伝播しますが、逆は伝播しないということです。

マウント情報も確認しておきましょう。

# cat /proc/self/mountinfo
  :(略)
714 30 0:74 / /root/test/orig/tmp rw,relatime shared:346 - tmpfs tmpfs rw
729 133 0:74 / /root/test/bind/tmp rw,relatime master:346 - tmpfs tmpfs rw
742 133 0:75 / /root/test/bind/tmp2 rw,relatime - tmpfs tmpfs rw
  :(略)

orig/tmpはsharedで、bind/tmporig/tmpを参照するslaveであり、bind/tmp2はprivateでorig/tmp側には伝播していなことが確認できます。

マウントプロパゲーション(2)〜 private mount 〜

昨日のエントリの続きです。引き続き完全に私個人が理解するための資料です。間違いの指摘は大歓迎です。

tenforward.hatenablog.com

それでは今日はprivateを見てみましょう。privateに設定したマウントのサブマウントは、他のマウントには反映されません。

sharedのときと同じディレクトリで試してみましょう。

# tree .
.
├── bind
└── orig
    └── tmp

3 directories, 0 files

まだ何もマウントが行われていない状態です。ここで、sharedと同様にorigbindにバインドマウントしてみましょう。

# mount --bind orig bind (バインドマウント)
# grep bind /proc/self/mountinfo (マウント情報の確認)
613 30 253:0 /root/test/orig /root/test/bind rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-ubuntu--lv rw
(sharedでマウントされている)
# mount --make-private bind (privateに設定)
# grep bind /proc/self/mountinfo 
613 30 253:0 /root/test/orig /root/test/bind rw,relatime - ext4 /dev/mapper/ubuntu--vg-ubuntu--lv rw
(sharedの文字が消えている)

バインドマウントした状態ではsharedになっていたマウントで、--make-privateでprivateに設定した後はsharedの文字が消えています。

この状態でbind配下のtmpディレクトリにtmpfsをマウントします。

# tree .
.
├── bind
│   └── tmp
└── orig
    └── tmp

4 directories, 0 files
(バインドマウントされているので両方にtmpが存在する)
# mount -t tmpfs tmpfs bind/tmp (tmpfsマウント)
# egrep "bind.*tmpfs" /proc/self/mountinfo
627 613 0:74 / /root/test/bind/tmp rw,relatime - tmpfs tmpfs rw
(マウントされている)

bind/tmpにtmpfsがマウントされました。このtmpfs上にファイルを置いてみましょう。

# touch bind/tmp/testfile
# tree .
.
├── bind
│   └── tmp
│       └── testfile
└── orig
    └── tmp

4 directories, 1 file

bind以下には作成したファイルが見えていますが、orig以下ではファイルが見えません。つまりtmpfsはバインドマウント先のbind配下でだけ有効で、バインドマウント元のorigでは反映されていないことがわかります。

sharedのときと同様にorig以下で行ったマウントがbindで行われないことも確認おきましょう。

# mkdir orig/tmp2 (orig以下にtmp2ディレクトリを作成)
# tree .
.
├── bind
│   ├── tmp
│   │   └── testfile
│   └── tmp2
└── orig
    ├── tmp
    └── tmp2

6 directories, 1 file
# mount --make-private / (orig側のマウントをprivateに設定)
# mount -t tmpfs tmpfs orig/tmp2 (tmp2にtmpfsをマウント)
# touch orig/tmp2/testfile2 (ファイルをtmpfs上に置く)
# tree .
.
├── bind
│   ├── tmp
│   │   └── testfile
│   └── tmp2
└── orig
    ├── tmp
    └── tmp2
        └── testfile2

6 directories, 2 files
(orig側のtmpfsマウントはbind側には反映されていない)

上のようにorig側でのtmpfsマウントがbind側では反映されていないことがわかります。つまりtmpfsはバインドマウント元のorig配下でだけ有効で、バインドマウント先のbindには反映されていないことがわかります。

このようにprivateに設定したマウントのサブマウントは、他のマウント配下には反映されません。

マウントプロパゲーション(1)〜 shared mount 〜

完全に私個人が理解するための資料です。間違いの指摘は大歓迎です。

2001 年に 2.4 kernel でバインド(bind)マウントが、2002 年の 2.4.19 で mount namespace が導入されました。ただ、ユースケースによっては mount namespace が行う分離は徹底しているので、もう少しマウントが共有されても良いシーンがあったようで、shared マウントなどのマウントプロパゲーションの考え方が 2.6.15(2006 年 1 月)で導入されました。

そのマウントプロパゲーションについてまとめていきます。

説明中の「サブマウント」とは、あるマウントポイントがあったとして、そのツリー配下で行われるマウント処理のことを指すとします。例えば、ホストで/dev/sda1というデバイスをルート/にマウントします。その配下の/home/dev/sda2をマウントしたりしますが、この/homeをサブマウントと呼ぶことにします。

プロパゲーションの設定 説明
shared マウント元のサブマウントはマウント先にも反映される。マウント先配下のサブマウントはマウント元にも反映される
private あるマウント配下でのサブマウントは他のマウント配下には公開されない
slave マウント元のサブマウントはマウント先にも反映されるが、マウント先のサブマウントはマウント元には反映されない
unbindable privateと同様でさらにこのマウントをバインドマウントできない

上のそれぞれについて、配下のマウントに再帰的に設定を反映させることができます。

プロパゲーションの設定 説明
rshared 配下で行われるマウントすべてを shared にする
rprivate 配下で行われるマウントすべてを private にする
rslave 配下で行われるマウントすべてを slave にする
runbindable 配下で行われるマウントすべてを unbindable にする

では、バインドマウントを使ってマウントプロパゲーションの説明をしていきましょう。

shared

まずはsharedです。sharedは、複数のマウント配下で行ったマウントがお互いに伝播します。

まず、バインドマウント用にorigbindというディレクトリをふたつ作ります。そしてバインドマウントを行います。バインドマウント元ディレクトorig内にtmpというディレクトリを作りました。

# mkdir orig bind
# mount --bind orig bind (origをbindにバインドマウント)
# mkdir orig/tmp (orig内にtmpというディレクトリを作成)

origbindにもバインドマウントされているので、次のように両方のディレクトリにtmpディレクトリが見えます。

# tree .
.
├── bind
│   └── tmp
└── orig
    └── tmp

4 directories, 0 files

ここでbindをsharedマウントとしてみましょう。sharedにするにはmountコマンドに--make-sharedを指定します。

# mount --make-shared bind
# grep bind /proc/self/mountinfo (マウント情報の確認)
518 30 253:0 /root/test/orig /root/test/bind rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-ubuntu--lv rw
(shared指定されている)

sharedになっていることを確認するために/proc/self/mountinfoを確認したところ、きちんと"shared"という文字がみえます。ここでshared指定されたbind以下でマウント操作を行ってみます。ここではtmpディレクトリにtmpfsをマウントしてみましょう。

# mount -t tmpfs tmpfs bind/tmp (bind以下のtmpにtmpfsマウント)
# egrep "bind.*tmpfs" /proc/self/mountinfo (マウントされたのを確認)
546 518 0:49 / /root/test/bind/tmp rw,relatime shared:253 - tmpfs tmpfs rw

tmpfsがマウントされていますので、このtmpfs上にファイルを置いてみましょう。そしてorigbindの配下がどうなっているのかを確認してみます。

# touch bind/tmp/testfile (tmpfs上にファイルを置く)
# tree .
.
├── bind
│   └── tmp
│       └── testfile
└── orig
    └── tmp
        └── testfile

4 directories, 2 files

バインドマウント先のbind内でマウントしたtmpfsがバインドマウント元でもマウントされた状態になっています。

この逆もやってみましょう。origディレクトリはマウントポイントではありませんので、ここではorigディレクトリが所属するマウントのマウントポイントであるルート(/)でsharedを指定します。ここは環境によって違いますので、実際に試す場合は適宜変更してください。

# mount --make-shared /

そしてorigディレクトリ以下にtmp2ディレクトリを作成しtmpfsをマウントし、ファイルを置きます。

# mkdir orig/tmp2
# mount -t tmpfs tmpfs orig/tmp2/ (tmp2にtmpfsをマウント)
# touch orig/tmp2/testfile2 (tmpfs上にファイルを置く)
# tree .
.
├── bind
│   ├── tmp
│   │   └── testfile
│   └── tmp2
│       └── testfile2
└── orig
    ├── tmp
    │   └── testfile
    └── tmp2
        └── testfile2

6 directories, 4 files

上のように、orig以下で行ったマウントがきちんとbind以下でも見えています。

このようにsharedに設定したマウント配下で行ったマウントは、他のマウント配下にも反映されます。