为什么取消链接后绑定挂载文件会失败并显示 ENOENT?

为什么取消链接后绑定挂载文件会失败并显示 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”,指向同一个 inode,但您会得到相同的结果:

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

世界上到底发生了什么事?

答案1

系统mount(2)调用将通过挂载和符号链接完全解析其路径,但与 不同的是open(2),它不会接受已删除文件的路径,即解析为未链接的目录条目的路径。

<filename> (deleted)(与的路径类似/proc/PID/fd/FD,procfs 会将未链接的目录显示为<filename>//deleted/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

所有这些过去都可以在较旧的内核中工作,但从 v4.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);
+

看起来这种效果并不是更改所有意为之的。此后其他无关的变化堆积如山,更加令人困惑。

这样做的结果是,它还可以防止通过打开的 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,指向完全相同的 inode,但您会得到相同的结果

这与“符号链接”是一样的/proc/PID/fd/FD。内核足够聪明,可以通过直接重命名来跟踪文件,但不能通过ln+ rm( link(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 ...

答案2

浏览源代码,我找到了一个ENOENT相关的代码,即未链接的目录条目:

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#L3100

get_mountpoint()通常应用于目标,而不是源。在此函数中,由于挂载传播而调用它。有必要强制执行这样的规则:在挂载传播期间,您不能在已删除的文件之上添加挂载。但强制执行正在急切地进行,即使没有发生需要这样做的挂载传播。我认为检查像这样保持一致是件好事,只是编码比我理想中的更晦涩一些。

不管怎样,我认为强制执行这一点是合理的。只要它有助于减少需要分析的奇怪案例的数量,并且没有人有特别令人信服的反驳论点。

答案3

一个全面的答案是:需要理解三件事,然后这一切才有意义。

首先,绑定安装的源是目录项,不是索引节点。也就是说,您不会在名称上绑定安装 inode;而是将其绑定到名称上。您将一个目录项绑定安装到另一个目录项上。要查看差异,请查看如果将不同链接挂载到同一 inode 会发生什么情况;坐骑是不同的,因为即使 inode 相同,源 dentry 也不同:

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,...

要理解的第二件事是,当您安装的东西本身就是早期绑定安装的目标时,它与绑定安装的源是相同的目录项对象(这就是绑定安装;一个目录项覆盖另一个目录项。)因此,如果a1is Mounted on b1,则 Mounting b1on与 Mounting onc1完全相同,因为名称和引用相同的目录项。a1c1a1b1

第三件需要理解的事情是,内核禁止绑定挂载已删除的目录项,因为……我看不出有什么充分的理由。看来错误检查的目的是目标一个挂载(防止挂载到已删除的目录项上,这样做是没有意义的,因为你永远无法引用你的新挂载)没有充分的理由申请来源坐骑也是如此。这是这里的代码:

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);

这三个事实的结果是(继续上面的 shell 会话)在if被删除时ENOENT挂载:b2c2a2

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安装是有效的,并且顺序不重要:在某些东西上安装已删除的目录项是合法的,还是不合法的,不应该当它被删除时很重要。然而,理性的人并不同意。

感谢大家。

相关内容