Mount

什麼程式碼可以防止掛載命名空間循環?在涉及安裝傳播的更複雜的情況下

  • August 6, 2021

背景資料

假設 /tmpprivate根據核心預設值(但不是 systemd 預設值)通過傳播掛載,並作為單獨的文件系統進行掛載。如有必要,您可以通過在內部執行命令unshare -m和/或使用mount --bind /tmp /tmp.

# findmnt -n -o propagation /tmp
private

請注意,以下命令返回錯誤:

# touch /tmp/a
# mount --bind /proc/self/ns/mnt /tmp/a
mount: /tmp/a: wrong fs type, bad option, bad superblock on /proc/self/ns/mnt, missing codepage or helper program, or other error.

這是因為核心程式碼(參見下面的摘錄)阻止了簡單的掛載命名空間循環。程式碼註釋解釋了為什麼不允許這樣做。掛載命名空間的生命週期由一個簡單的引用計數跟踪。如果您有一個循環,其中掛載命名空間 A 和 B 都引用另一個,那麼 A 和 B 將始終至少有一個引用,並且它們永遠不會被釋放。分配的記憶體將失去,直到您重新啟動整個系統。

為了比較,核心允許以下內容,這不是循環:

# unshare -m
# echo $$
8456
# kill -STOP $$
[1]+  Stopped                 unshare -m

# touch /tmp/a
# mount --bind /proc/8456/ns/mnt /tmp/a
#
# umount /tmp/a  # now clean up
# kill %1; echo
#

問題

核心程式碼在哪里區分以下兩種情況?

如果我嘗試使用掛載傳播創建循環,則會失敗:

# mount --make-shared /tmp
# unshare -m --propagation shared
# echo $$
8456
# kill -STOP $$
[1]+  Stopped                 unshare -m

# mount --bind /proc/8456/ns/mnt /tmp/a
mount: /tmp/a: wrong fs type, bad option, bad superblock on /proc/9061/ns/mnt, missing codepage or helper program, or other error.

但是,如果我刪除掛載傳播,則不會創建循環,並且會成功:

# unshare -m --propagation private
# echo $$
8456
# kill -STOP $$
[1]+  Stopped                 unshare -m

# mount --bind /proc/8456/ns/mnt /tmp/a
# 
# umount /tmp/a  # cleanup    

處理更簡單情況的核心程式碼

https://elixir.bootlin.com/linux/v4.18/source/fs/namespace.c

static bool mnt_ns_loop(struct dentry *dentry)
{
   /* Could bind mounting the mount namespace inode cause a
    * mount namespace loop?
    */
   struct mnt_namespace *mnt_ns;
   if (!is_mnt_ns_file(dentry))
       return false;

   mnt_ns = to_mnt_ns(get_proc_ns(dentry->d_inode));
   return current->nsproxy->mnt_ns->seq >= mnt_ns->seq;
}

   err = -EINVAL;
   if (mnt_ns_loop(old_path.dentry))
       goto out;

* Assign a sequence number so we can detect when we attempt to bind
* mount a reference to an older mount namespace into the current
* mount namespace, preventing reference counting loops.  A 64bit
* number incrementing at 10Ghz will take 12,427 years to wrap which
* is effectively never, so we can ignore the possibility.
*/
static atomic64_t mnt_ns_seq = ATOMIC64_INIT(1);

static struct mnt_namespace *alloc_mnt_ns(struct user_namespace *user_ns)

請參閱 commit 4ce5d2b1a8fd, vfs: Don’t copy mount bind mounts of /proc/<pid>/ns/mntbetween namespaces

propagate_one()copy_tree()CL_COPY_MNT_NS_FILE. 在這種情況下,如果樹根是 NS 文件的掛載,則會copy_tree()失敗並顯示錯誤EINVAL。術語“NS 文件”是指其中一個文件/proc/*/ns/mnt

進一步閱讀,我注意到如果樹根不是 NS 文件,但其中一個子掛載是,它將被排除在傳播之外(與不可綁定掛載的方式相同)。

在傳播過程中靜默跳過 NS 文件的範例

# mount --make-shared /tmp
# cd /tmp
# mkdir private_mnt
# mount --bind private_mnt private_mnt
# mount --make-private private_mnt
# touch private_mnt/child_ns
# unshare --mount=private_mnt/child_ns --propagation=shared ls -l /proc/self/ns/mnt
lrwxrwxrwx. 1 root root 0 Oct  7 18:25 /proc/self/ns/mnt -> 'mnt:[4026532807]'
# findmnt | grep /tmp
├─/tmp                                tmpfs                             tmpfs           ...
│ ├─/tmp/private_mnt                  tmpfs[/private_mnt]               tmpfs           ...
│ │ └─/tmp/private_mnt/child_ns       nsfs[mnt:[4026532807]]            nsfs            ...

讓我們創建一個普通的掛載進行比較

# mkdir private_mnt/child_mnt
# mount --bind private_mnt/child_mnt private_mnt/child_mnt

現在嘗試傳播一切。(創建private_mnt內部的遞歸綁定掛載/tmp. /tmp是共享掛載)。

# mkdir shared_mnt
# mount --rbind private_mnt shared_mnt
# findmnt | grep /tmp/shared_mnt
│ └─/tmp/shared_mnt                   tmpfs[/private_mnt]               tmpfs           ...
│   ├─/tmp/shared_mnt/child_ns        nsfs[mnt:[4026532809]]            nsfs            ...
│   └─/tmp/shared_mnt/child_mnt       tmpfs[/private_mnt/child_mnt]     tmpfs           ...
# nsenter --mount=/tmp/private_mnt/child_ns findmnt|grep /tmp/shared_mnt
│ └─/tmp/shared_mnt                   tmpfs[/private_mnt]               tmpfs           ...
│   └─/tmp/shared_mnt/child_mnt       tmpfs[/private_mnt/child_mnt]     tmpfs           ...

核心程式碼

以下是目前版本程式碼的摘錄,這些摘錄已添加到上面連結的送出中。

https://elixir.bootlin.com/linux/v4.18/source/fs/pnode.c#L226

static int propagate_one(struct mount *m)
{
...
   /* Notice when we are propagating across user namespaces */
   if (m->mnt_ns->user_ns != user_ns)
       type |= CL_UNPRIVILEGED;
   child = copy_tree(last_source, last_source->mnt.mnt_root, type);
   if (IS_ERR(child))
       return PTR_ERR(child);

https://elixir.bootlin.com/linux/v4.18/source/fs/namespace.c#L1790

struct mount *copy_tree(struct mount *mnt, struct dentry *dentry,
                   int flag)
{
   struct mount *res, *p, *q, *r, *parent;

   if (!(flag & CL_COPY_UNBINDABLE) && IS_MNT_UNBINDABLE(mnt))
       return ERR_PTR(-EINVAL);

   if (!(flag & CL_COPY_MNT_NS_FILE) && is_mnt_ns_file(dentry))
       return ERR_PTR(-EINVAL);

引用自:https://unix.stackexchange.com/questions/473717