web-dev-qa-db-ja.com

リンク解除後のファイルのバインドマウントがENOENTで失敗するのはなぜですか?

リンクを解除した後にバインドマウントするとENOENTが表示される理由がわかりません。

kduda@penguin:/tmp$ echo hello > a
kduda@penguin:/tmp$ touch b c
kduda@penguin:/tmp$ Sudo unshare -m
root@penguin:/tmp# mount -B a b
root@penguin:/tmp# rm a
root@penguin:/tmp# cat b
hello
root@penguin:/tmp# mount -B b c
mount: mount(2) failed: No such file or directory

これは私にはバグのようです。同じaを指す "a"を再作成することもできますが、同じ結果が得られます。

kduda@penguin:/tmp$ echo hello > a
kduda@penguin:/tmp$ ln a a-save
kduda@penguin:/tmp$ Sudo unshare -m
root@penguin:/tmp# mount -B a b
root@penguin:/tmp# rm a
root@penguin:/tmp# ln a-save a
root@penguin:/tmp# mount -B b c
mount: mount(2) failed: No such file or directory

世界で何が起こっているのですか?

4
Kenneth Duda

ソースコードを調べたところ、関連するENOENTが1つだけ見つかりました。つまり、リンクされていないディレクトリエントリに該当します。

_static int attach_recursive_mnt(struct mount *source_mnt,
            struct mount *dest_mnt,
            struct mountpoint *dest_mp,
            struct path *parent_path)
{
    [...]

    /* Preallocate a mountpoint in case the new mounts need
     * to be tucked under other mounts.
     */
    smp = get_mountpoint(source_mnt->mnt.mnt_root);
_
_static struct mountpoint *get_mountpoint(struct dentry *dentry)
{
    struct mountpoint *mp, *new = NULL;
    int ret;

    if (d_mountpoint(dentry)) {
        /* might be worth a WARN_ON() */
        if (d_unlinked(dentry))
            return ERR_PTR(-ENOENT);
_

https://elixir.bootlin.com/linux/v5.2/source/fs/namespace.c#L31

get_mountpoint()は通常、ソースではなくターゲットに適用されます。この関数では、マウントの伝播のために呼び出されます。マウントの伝播中は、削除されたファイルの上にマウントを追加できないというルールを適用する必要があります。しかし、これを必要とするマウントの伝播が発生しない場合でも、強制は熱心に行われています。チェックがこのように一貫しているのは良いことだと思います。それは、理想的に好むよりも少しあいまいにコード化されているだけです。

どちらにしても、これを実施するのは妥当だと思います。それが分析する奇妙なケースの数を減らすのに役立ち、誰も特に説得力のある反論を持っている限り。

2
sourcejedi

mount(2)システムコールは、マウントとシンボリックリンクを介してパスを完全に解決しますが、open(2)とは異なり、削除されたファイルへのパス、つまりリンクされていないディレクトリエントリに解決されるパスは受け入れません。 。

(_/proc/PID/fd/FD_の<filename> (deleted)パスと同様に、procfsはリンクされていないエントリを _<filename>//deleted_ in _/proc/PID/mountinfo_として表示します)

_# unshare -m
# echo foo > foo; touch bar baz quux
# mount -B foo bar
# mount -B bar baz
# grep foo /proc/self/mountinfo
56 38 8:7 /tmp/foo /tmp/bar ...
57 38 8:7 /tmp/foo /tmp/baz ...

# rm foo
# grep foo /proc/self/mountinfo
56 38 8:7 /tmp/foo//deleted /tmp/bar ...
57 38 8:7 /tmp/foo//deleted /tmp/baz ...
# mount -B baz quux
mount: mount(2) failed: /tmp/quux: No such file or directory
_

これは以前は古いカーネルで機能していましたが、v-4.19以降は この変更 によって最初に導入されました:

_commit 1064f874abc0d05eeed8993815f584d847b72486
Author: Eric W. Biederman <[email protected]>
Date:   Fri Jan 20 18:28:35 2017 +1300

    mnt: Tuck mounts under others instead of creating shadow/side mounts.
...
+       /* Preallocate a mountpoint in case the new mounts need
+        * to be tucked under other mounts.
+        */
+       smp = get_mountpoint(source_mnt->mnt.mnt_root);
+       if (IS_ERR(smp))
+               return PTR_ERR(smp);
+
_

この影響は変更によって意図されていなかったようです。それ以来、他の無関係な 変更 が積み重なっており、さらに混乱しています。

その結果、削除されたファイルが名前空間の別の場所にopen fdを介して固定されることも防止されます。

_# exec 7>foo; touch bar
# rm foo
# mount -B /proc/self/fd/7 bar
mount: mount(2) failed: /tmp/bar: No such file or directory
_

OPと同じ状態のため、最後のコマンドは失敗します。

aを再作成して、まったく同じiノードを指すようにすることもできますが、同じ結果が得られます。

_/proc/PID/fd/FD_ "symlinks"と同じです。カーネルは、ln + rmlink(2) + unlink(2))を介してではなく、単純な名前変更を介してファイルを追跡するのに十分なほどスマートです。

_# unshare -m
# echo foo > foo; touch bar baz
# mount -B foo bar
# mount -B bar baz
# grep foo /proc/self/mountinfo
56 38 8:7 /tmp/foo /tmp/bar ...
57 38 8:7 /tmp/foo /tmp/baz ...

# mv foo quux
# grep bar /proc/self/mountinfo
56 38 8:7 /tmp/quux /tmp/bar ...

# ln quux foo; rm quux
# grep bar /proc/self/mountinfo
56 38 8:7 /tmp/quux//deleted /tmp/bar ...
_
5
mosvy

包括的な答えは次のとおりです。理解すべき3つのことがあり、これはすべて理にかなっています。

まず、バインドマウントのソースはdentryであり、iノードではありません。つまり、名前にiノードをバインドマウントしないでください。あるデントリーを別のデントリーの上にバインドマウントします。違いを確認するには、同じiノードに異なるリンクをマウントするとどうなるかを見てください。マウントはdifferentです。これは、inodeが同じ場合でも、ソースデントリが異なるためです。

root@penguin:/tmp# echo hello > a1
root@penguin:/tmp# ln a1 a2
root@penguin:/tmp# touch b1 b2
root@penguin:/tmp# mount -B a1 b1
root@penguin:/tmp# mount -B a2 b2
root@penguin:/tmp# ls -li a1 a2 b1 b2
9552271 -rw-r--r-- 2 root root 6 Aug 25 05:16 a1
9552271 -rw-r--r-- 2 root root 6 Aug 25 05:16 a2
9552271 -rw-r--r-- 2 root root 6 Aug 25 05:16 b1
9552271 -rw-r--r-- 2 root root 6 Aug 25 05:16 b2
root@penguin:/tmp# grep /tmp/ /proc/self/mountinfo
421 364 0:38 /lxd/.../rootfs/tmp/a1 /tmp/b1 rw,...
422 364 0:38 /lxd/.../rootfs/tmp/a2 /tmp/b2 rw,...

2番目に理解する必要があるのは、それ自体が以前のバインドマウントのターゲットであるものをマウントする場合、それはバインドマウントのソースと同じdentryオブジェクトです(つまり、バインドマウントとは、あるデントが別のデントを上書きするということです)。したがって、a1b1にマウントされている場合、b1c1にマウントすることは、a1c1にマウントすることとまったく同じです。名前a1b1は同じデントリーを指します。

理解すべき3番目のことは、削除されたデントリーのバインドマウントをカーネルが禁止していることです。マウントのtargetを対象としたエラーチェック(削除されたデントリーへのマウントを防止します。これは、新しいマウントを参照できないため、意味がありません)は有効ではありません。マウントのsourceの理由も同様です。それがこのコードです:

static struct mountpoint *get_mountpoint(struct dentry *dentry)
{
    struct mountpoint *mp, *new = NULL;
    int ret;

    if (d_mountpoint(dentry)) {
        /* might be worth a WARN_ON() */
        if (d_unlinked(dentry))
            return ERR_PTR(-ENOENT);

これらの3つの事実の結果は(上記のシェルセッションを続行する)はb2c2ENOENTマウントa2が削除された場合です:

root@penguin:/tmp# touch c1 c2
root@penguin:/tmp# rm a2
root@penguin:/tmp# mount -B b1 c1
root@penguin:/tmp# mount -B b2 c2
mount: mount(2) failed: /tmp/c2: No such file or directory
root@penguin:/tmp# 

マウント後にa2を削除した場合、b2-on-c2マウントは有効であり、順序は重要ではないため、これはバグとして私に警告します。削除されたデントリーを何かにマウントすることは合法的かそうでないかのどちらかです。削除されたときの問題。しかし、合理的な人々は同意しません。

みんな、ありがとう。

0
Kenneth Duda