什麼程式碼可以防止掛載命名空間循環?在涉及安裝傳播的更複雜的情況下
背景資料
假設 /tmp
private
根據核心預設值(但不是 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/mnt
between 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);