마운트된 파일 바인딩이 실패하고 링크를 취소한 후 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

이것은 나에게 버그처럼 보입니다. 동일한 inode를 가리키는 "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

세상에 무슨 일이 일어나고 있나요?

답변1

시스템 mount(2)호출은 마운트 및 심볼릭 링크를 통해 경로를 완전히 확인하지만 와 달리 open(2)삭제된 파일에 대한 경로, 즉 링크되지 않은 디렉터리 항목을 확인하는 경로는 허용하지 않습니다.

<filename> (deleted)(경로와 유사하게 /proc/PID/fd/FDprocfs는 연결되지 않은 디렉토리를 다음과 같이 표시합니다.<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와 동일한 상황으로 인해 마지막 명령이 실패했습니다.

완전히 동일한 inode를 가리키면서 다시 생성할 수도 있지만 a동일한 결과를 얻게 됩니다.

이는 "심볼릭 링크"와 동일합니다 /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,...

두 번째로 이해해야 할 점은 그 자체가 초기 바인드 마운트의 대상인 것을 설치할 때 바인드 마운트의 소스와 동일한 디렉토리 항목 객체라는 것입니다(이것이 바인드 마운트입니다. 한 디렉토리 항목이 다른 디렉토리 항목을 덮어씁니다. 디렉터리 항목.) 따라서 a1에 마운트된 경우 이름과 참조가 동일한 디렉터리 항목이므로 b1Mounting b1on은 Mounting on과 완전히 동일합니다 .c1a1c1a1b1

세 번째로 이해해야 할 점은 커널이 삭제된 디렉토리 항목을 마운트하는 바인드를 비활성화한다는 것입니다. 왜냐하면... 타당한 이유가 보이지 않습니다. 오류 검사의 목적은 다음과 같습니다.표적마운트(삭제된 디렉토리 항목에 마운트하는 것을 방지하기 위해 새 마운트를 참조할 수 없으므로 그렇게 할 필요가 없음)가 정당한 이유 없이 요청되었습니다.원천마운트도 마찬가지입니다. 여기에 있는 코드는 다음과 같습니다.

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

이 세 가지 사실의 결과는 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 설치가 유효하고 순서가 중요하지 않기 때문에 이것이 버그라고 생각하게 만듭니다. 삭제된 디렉토리 항목을 무언가에 설치하는 것이 합법적입니까, 아니면 불법이며 설치해야 합니다. 언제 제거해도 상관없습니다. 그러나 합리적인 사람들은 이에 동의하지 않습니다.

모두 감사합니다.

관련 정보