* [PATCH] ns: skip namespace_init LSM hook for internal namespaces
@ 2026-11-14 14:13 Zhiqi Zhang
2026-11-14 14:31 ` sashiko-bot
0 siblings, 1 reply; 2+ messages in thread
From: Zhiqi Zhang @ 2026-11-14 14:13 UTC (permalink / raw)
To: Christian Brauner, Alexander Viro
Cc: Jan Kara, Mickaël Salaün, Günther Noack,
Paul Moore, James Morris, Serge E . Hallyn, David Sterba,
linux-fsdevel, linux-security-module, linux-btrfs, linux-nfs,
linux-kernel
mount_subtree() allocates a temporary mount namespace while resolving a
path inside a detached mount. btrfs mounts use this path on every mount
(btrfs_get_tree() -> btrfs_get_tree_subvol() -> mount_subvol() ->
mount_subtree()), as does NFS for referrals. The namespace is private to
mount_subtree(): it is never installed in an nsproxy, is not reachable
from user space, and is freed before mount_subtree() returns.
__ns_common_init() calls security_namespace_init() for every namespace,
which runs the LSM namespace_init hook. Landlock implements
LANDLOCK_PERMISSION_NAMESPACE_USE through that hook, so a task in a
domain that handles the permission, allows every namespace type except
CLONE_NEWNS, and is otherwise allowed to mount gets -EPERM from a btrfs
mount. mount(2) is not covered by the permission and the task never
acquires a namespace.
Skip LSM initialization for kernel-internal namespaces.
__ns_common_init() gains an lsm parameter, and a new
ns_common_init_inum_internal() passes false. It is used by
mount_subtree() only; user-visible namespace allocations (clone,
unshare, setns, open_tree, fsmount) keep calling the hook.
Reproduced on 7.4-rc2 with a loop-mounted btrfs image and a Landlock
domain that handles NAMESPACE_USE and allows every CLONE_NEW* type except
CLONE_NEWNS:
mount("/dev/loop0", "/mnt", "btrfs", 0, NULL); -> -EPERM
The mount succeeds when CLONE_NEWNS is also allowed, and succeeds under
the first domain with this fix. With the first domain,
unshare(CLONE_NEWNS) and open_tree(OPEN_TREE_CLONE) remain denied.
Fixes: f675d2e95569 ("lsm: add LSM blob and hooks for namespaces")
Signed-off-by: Zhiqi Zhang <zhiqizhangg@gmail.com>
---
AI assistance:
- DeepSeek (deepseek-flash) generated the code change.
- I wrote the tests and reviewed the code.
- I wrote the documentation and the changelog, and used DeepSeek to
polish the wording.
fs/namespace.c | 24 ++++++++++++++++--------
include/linux/ns_common.h | 20 +++++++++++++++++---
kernel/nscommon.c | 18 ++++++++++++------
security/security.c | 7 ++++++-
4 files changed, 51 insertions(+), 18 deletions(-)
diff --git a/fs/namespace.c b/fs/namespace.c
index aecd05ac0..622f5094b 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -2353,7 +2353,7 @@ void drop_collected_paths(const struct path *paths, const struct path *prealloc)
kfree(paths);
}
-static struct mnt_namespace *alloc_mnt_ns(struct user_namespace *, bool);
+static struct mnt_namespace *alloc_mnt_ns(struct user_namespace *, bool, bool);
/* Consumes the caller's reference to @mnt. */
void dissolve_on_fput(struct vfsmount *mnt)
@@ -3149,7 +3149,7 @@ static struct mnt_namespace *get_detached_copy(const struct path *path, unsigned
struct user_namespace *user_ns = mnt_ns->user_ns;
struct mount *mnt, *p;
- ns = alloc_mnt_ns(user_ns, true);
+ ns = alloc_mnt_ns(user_ns, true, true);
if (IS_ERR(ns))
return ns;
@@ -3230,7 +3230,7 @@ static struct mnt_namespace *create_new_namespace(struct path *path,
if (foreign)
copy_flags |= CL_SLAVE | CL_COPY_UNBINDABLE;
- new_ns = alloc_mnt_ns(user_ns, false);
+ new_ns = alloc_mnt_ns(user_ns, false, true);
if (IS_ERR(new_ns))
return ERR_CAST(new_ns);
@@ -4369,7 +4369,7 @@ static void free_mnt_ns(struct mnt_namespace *ns)
mnt_ns_tree_remove(ns);
}
-static struct mnt_namespace *alloc_mnt_ns(struct user_namespace *user_ns, bool anon)
+static struct mnt_namespace *alloc_mnt_ns(struct user_namespace *user_ns, bool anon, bool lsm)
{
struct mnt_namespace *new_ns;
struct ucounts *ucounts;
@@ -4385,7 +4385,9 @@ static struct mnt_namespace *alloc_mnt_ns(struct user_namespace *user_ns, bool a
return ERR_PTR(-ENOMEM);
}
- if (anon)
+ if (!lsm)
+ ret = ns_common_init_inum_internal(new_ns, MNT_NS_ANON_INO);
+ else if (anon)
ret = ns_common_init_inum(new_ns, MNT_NS_ANON_INO);
else
ret = ns_common_init(new_ns);
@@ -4422,7 +4424,7 @@ struct mnt_namespace *copy_mnt_ns(u64 flags, struct mnt_namespace *ns,
old = ns->root;
- new_ns = alloc_mnt_ns(user_ns, false);
+ new_ns = alloc_mnt_ns(user_ns, false, true);
if (IS_ERR(new_ns))
return new_ns;
@@ -4509,7 +4511,13 @@ struct dentry *mount_subtree(struct vfsmount *m, const char *name)
struct path path;
int err;
- ns = alloc_mnt_ns(&init_user_ns, true);
+ /*
+ * Private to mount_subtree() and never exposed to user space: skip
+ * the LSM namespace_init hook, which authorizes a task acquiring
+ * access to a namespace. btrfs mounts and NFS referrals use this
+ * path.
+ */
+ ns = alloc_mnt_ns(&init_user_ns, true, false);
if (IS_ERR(ns)) {
mntput(m);
return ERR_CAST(ns);
@@ -4699,7 +4707,7 @@ SYSCALL_DEFINE3(fsmount, int, fs_fd, unsigned int, flags,
return FD_ADD((flags & FSMOUNT_CLOEXEC) ? O_CLOEXEC : 0,
open_new_namespace(&new_path, MOUNT_COPY_NEW));
- ns = alloc_mnt_ns(current->nsproxy->mnt_ns->user_ns, true);
+ ns = alloc_mnt_ns(current->nsproxy->mnt_ns->user_ns, true, true);
if (IS_ERR(ns))
return PTR_ERR(ns);
mnt = real_mount(new_path.mnt);
diff --git a/include/linux/ns_common.h b/include/linux/ns_common.h
index e8b2bf0da..a21fc1a9b 100644
--- a/include/linux/ns_common.h
+++ b/include/linux/ns_common.h
@@ -10,7 +10,7 @@
bool is_current_namespace(struct ns_common *ns);
int __ns_common_init(struct ns_common *ns, u32 ns_type, const struct proc_ns_operations *ops,
- int inum, u64 ns_id);
+ int inum, u64 ns_id, bool lsm);
void __ns_common_free(struct ns_common *ns);
struct ns_common *__must_check ns_owner(struct ns_common *ns);
@@ -47,13 +47,27 @@ static __always_inline bool is_ns_init_id(const struct ns_common *ns)
ns_common_type(__ns), \
to_ns_operations(__ns), \
(((__ns) == ns_init_ns(__ns)) ? ns_init_inum(__ns) : 0), \
- (((__ns) == ns_init_ns(__ns)) ? ns_init_id(__ns) : 0))
+ (((__ns) == ns_init_ns(__ns)) ? ns_init_id(__ns) : 0), \
+ true)
#define ns_common_init_inum(__ns, __inum) \
__ns_common_init(to_ns_common(__ns), \
ns_common_type(__ns), \
to_ns_operations(__ns), \
- __inum, 0)
+ __inum, 0, true)
+
+/*
+ * Like ns_common_init_inum(), but for namespaces that stay kernel-internal and
+ * are never exposed to user space (e.g. mount_subtree()'s temporary mount
+ * namespace). The LSM namespace_init hook authorizes a task acquiring access
+ * to a namespace; these allocations are not such acquisitions and skip LSM
+ * initialization.
+ */
+#define ns_common_init_inum_internal(__ns, __inum) \
+ __ns_common_init(to_ns_common(__ns), \
+ ns_common_type(__ns), \
+ to_ns_operations(__ns), \
+ __inum, 0, false)
#define ns_common_free(__ns) __ns_common_free(to_ns_common((__ns)))
diff --git a/kernel/nscommon.c b/kernel/nscommon.c
index 4438c6b66..78e1b6d32 100644
--- a/kernel/nscommon.c
+++ b/kernel/nscommon.c
@@ -55,7 +55,7 @@ static void ns_debug(struct ns_common *ns, const struct proc_ns_operations *ops)
#endif
int __ns_common_init(struct ns_common *ns, u32 ns_type, const struct proc_ns_operations *ops,
- int inum, u64 ns_id)
+ int inum, u64 ns_id, bool lsm)
{
int ret = 0;
@@ -85,11 +85,17 @@ int __ns_common_init(struct ns_common *ns, u32 ns_type, const struct proc_ns_ope
/* Allocate the id before the LSM hook so it can track the namespace by it. */
__ns_tree_gen_id(ns, ns_id);
- ret = security_namespace_init(ns);
- if (ret) {
- if (!inum)
- proc_free_inum(ns->inum);
- return ret;
+ /*
+ * Kernel-internal namespaces (lsm == false) are never exposed to
+ * user space and are not reported to the namespace_init LSM hook.
+ */
+ if (lsm) {
+ ret = security_namespace_init(ns);
+ if (ret) {
+ if (!inum)
+ proc_free_inum(ns->inum);
+ return ret;
+ }
}
/*
diff --git a/security/security.c b/security/security.c
index 4727b8347..454ceebcc 100644
--- a/security/security.c
+++ b/security/security.c
@@ -3387,6 +3387,10 @@ int security_create_user_ns(const struct cred *cred)
* is available via ns->ns_type, and the owning user namespace (if any)
* via ns->ops->owner(ns).
*
+ * Kernel-internal namespaces that are never exposed to user space (e.g. the
+ * temporary mount namespace of mount_subtree()) skip this function: they have
+ * no LSM security blob and are not reported to the namespace_init hook.
+ *
* Return: Returns 0 if successful, otherwise < 0 error code.
*/
int security_namespace_init(struct ns_common *ns)
@@ -3409,7 +3413,8 @@ int security_namespace_init(struct ns_common *ns)
* @ns: the namespace being freed
*
* Release security data attached to the namespace. Called before the
- * namespace structure is freed.
+ * namespace structure is freed. Kernel-internal namespaces skipped by
+ * security_namespace_init() have no blob, so this is a no-op for them.
*/
void security_namespace_free(struct ns_common *ns)
{
--
2.54.0 (Apple Git-157)
^ permalink raw reply related [flat|nested] 2+ messages in thread
* Re: [PATCH] ns: skip namespace_init LSM hook for internal namespaces
2026-11-14 14:13 [PATCH] ns: skip namespace_init LSM hook for internal namespaces Zhiqi Zhang
@ 2026-11-14 14:31 ` sashiko-bot
0 siblings, 0 replies; 2+ messages in thread
From: sashiko-bot @ 2026-11-14 14:31 UTC (permalink / raw)
Cc: linux-security-module
> mount_subtree() allocates a temporary mount namespace while resolving a
> path inside a detached mount. btrfs mounts use this path on every mount
> (btrfs_get_tree() -> btrfs_get_tree_subvol() -> mount_subvol() ->
> mount_subtree()), as does NFS for referrals. The namespace is private to
> mount_subtree(): it is never installed in an nsproxy, is not reachable
> from user space, and is freed before mount_subtree() returns.
>
> __ns_common_init() calls security_namespace_init() for every namespace,
> which runs the LSM namespace_init hook. Landlock implements
> LANDLOCK_PERMISSION_NAMESPACE_USE through that hook, so a task in a
> domain that handles the permission, allows every namespace type except
> CLONE_NEWNS, and is otherwise allowed to mount gets -EPERM from a btrfs
> mount. mount(2) is not covered by the permission and the task never
> acquires a namespace.
>
> [ ... ]
> the first domain with this fix. With the first domain,
> unshare(CLONE_NEWNS) and open_tree(OPEN_TREE_CLONE) remain denied.
>
> Fixes: f675d2e95569 ("lsm: add LSM blob and hooks for namespaces")
> Signed-off-by: Zhiqi Zhang <zhiqizhangg@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261114141323.19029-1-zhiqizhangg@gmail.com?part=1
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-11-14 14:31 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-14 14:13 [PATCH] ns: skip namespace_init LSM hook for internal namespaces Zhiqi Zhang
2026-11-14 14:31 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.