* Re: [PATCH bpf-next v3 1/2] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling
From: David Windsor @ 2026-06-23 3:49 UTC (permalink / raw)
To: Alexei Starovoitov
Cc: viro, brauner, jack, ast, daniel, john.fastabend, andrii, eddyz87,
memxor, martin.lau, song, yonghong.song, jolsa, emil, kpsingh,
mattbobrowski, paul, jmorris, serge, zohar, roberto.sassu,
dmitry.kasatkin, eric.snowberg, stephen.smalley.work, omosnace,
casey, shuah, linux-kernel, linux-fsdevel, bpf,
linux-security-module, linux-integrity, selinux, linux-kselftest
In-Reply-To: <DJFZGYZFMN73.1799LMXW49WZO@gmail.com>
On Mon, Jun 22, 2026 at 7:57 PM Alexei Starovoitov
<alexei.starovoitov@gmail.com> wrote:
>
> On Thu Jun 18, 2026 at 1:34 PM PDT, David Windsor wrote:
> > +
> > +static int __bpf_init_inode_xattr(struct xattr_ctx *xattr_ctx,
> > + const char *name__str,
> > + const struct bpf_dynptr *value_p)
> > +{
> > + struct bpf_dynptr_kern *value_ptr = (struct bpf_dynptr_kern *)value_p;
> > + size_t name_len;
> > + void *xattr_value;
> > + struct xattr *xattr;
> > + struct xattr *xattrs;
> > + int *xattr_count;
> > + const void *value;
> > + u32 value_len;
> > +
> > + if (!xattr_ctx || !name__str)
> > + return -EINVAL;
> > +
> > + xattrs = xattr_ctx->xattrs;
> > + xattr_count = xattr_ctx->xattr_count;
> > + if (!xattrs || !xattr_count)
> > + return -EINVAL;
> > + if (bpf_xattrs_used(xattr_ctx) >= BPF_LSM_INODE_INIT_XATTRS)
> > + return -ENOSPC;
>
> This check is good to have, but it's enough. No need to duplicate it.
> More below.
>
> > +
> > static int bpf_fs_kfuncs_filter(const struct bpf_prog *prog, u32 kfunc_id)
> > {
> > if (!btf_id_set8_contains(&bpf_fs_kfunc_set_ids, kfunc_id) ||
> > - prog->type == BPF_PROG_TYPE_LSM)
> > + prog->type == BPF_PROG_TYPE_LSM) {
> > + /* bpf_init_inode_xattr only attaches to inode_init_security. */
> > + if (kfunc_id == bpf_init_inode_xattr_btf_ids[0] &&
> > + prog->aux->attach_btf_id != bpf_lsm_inode_init_security_btf_ids[0])
> > + return -EACCES;
>
> This is unnecessary. Only one hook will have xattr_ctx type.
> The normal verifier type enforcement will do its work.
>
Good point, thanks.
> > diff --git a/kernel/bpf/trampoline.c b/kernel/bpf/trampoline.c
> > index 1a721fc4bef5..b41b02173e24 100644
> > --- a/kernel/bpf/trampoline.c
> > +++ b/kernel/bpf/trampoline.c
> > @@ -859,6 +859,9 @@ static int bpf_trampoline_add_prog(struct bpf_trampoline *tr,
> > }
> > if (cnt >= BPF_MAX_TRAMP_LINKS)
> > return -E2BIG;
> > + if (node->link->prog->aux->attach_limit &&
> > + tr->progs_cnt[kind] >= node->link->prog->aux->attach_limit)
> > + return -E2BIG;
>
> No need. The check inside kfunc is enough.
>
Paul wanted this check because it occurs at bpf prog attach time,
whereas the one in the kfunc is at inode creation time.
> > if (!hlist_unhashed(&node->tramp_hlist))
> > /* prog already linked */
> > return -EBUSY;
> > diff --git a/security/bpf/hooks.c b/security/bpf/hooks.c
> > index 40efde233f3a..d7c44c5c0e30 100644
> > --- a/security/bpf/hooks.c
> > +++ b/security/bpf/hooks.c
> > @@ -30,6 +30,7 @@ static int __init bpf_lsm_init(void)
> >
> > struct lsm_blob_sizes bpf_lsm_blob_sizes __ro_after_init = {
> > .lbs_inode = sizeof(struct bpf_storage_blob),
> > + .lbs_xattr_count = BPF_LSM_INODE_INIT_XATTRS,
> > };
> >
> > DEFINE_LSM(bpf) = {
> > diff --git a/security/integrity/evm/evm_main.c b/security/integrity/evm/evm_main.c
> > index b59e3f121b8a..e0a05162accc 100644
> > --- a/security/integrity/evm/evm_main.c
> > +++ b/security/integrity/evm/evm_main.c
> > @@ -1062,14 +1062,16 @@ static int evm_inode_copy_up_xattr(struct dentry *src, const char *name)
> > * evm_inode_init_security - initializes security.evm HMAC value
> > */
> > int evm_inode_init_security(struct inode *inode, struct inode *dir,
> > - const struct qstr *qstr, struct xattr *xattrs,
> > - int *xattr_count)
> > + const struct qstr *qstr,
> > + struct xattr_ctx *xattr_ctx)
>
> the rest looks good.
> Pls split the patch. Introduce xattr_ctx first across the LSMs.
> Then another patch with a new kfunc.
>
> pw-bot: cr
^ permalink raw reply
* Re: [PATCH 1/2] bpf: lsm: disable xfrm_decode_session hook attachment
From: Alexei Starovoitov @ 2026-06-23 3:11 UTC (permalink / raw)
To: include
Cc: LSM List, bpf, LKML, stable, KP Singh, Matt Bobrowski,
Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau,
Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis,
Florent Revest, Brendan Jackman
In-Reply-To: <20260619130305.27779-1-include@grrlz.net>
On Fri, Jun 19, 2026 at 6:03 AM Bradley Morgan <include@grrlz.net> wrote:
>
> BPF LSM programs can currently attach to xfrm_decode_session(). That
> hook may return an error, but security_skb_classify_flow() calls it
> from a void path and triggers BUG_ON() if an error is returned.
>
> Disable BPF attachment to the hook to prevent a BPF LSM program from
> turning packet classification into a full panic.
>
> Fixes: 9e4e01dfd325 ("bpf: lsm: Implement attach, detach and execution")
> Cc: stable@vger.kernel.org
> Signed-off-by: Bradley Morgan <include@grrlz.net>
> ---
> kernel/bpf/bpf_lsm.c | 3 +++
> 1 file changed, 3 insertions(+)
>
> diff --git a/kernel/bpf/bpf_lsm.c b/kernel/bpf/bpf_lsm.c
> index 564071a92d7d..1433809bb166 100644
> --- a/kernel/bpf/bpf_lsm.c
> +++ b/kernel/bpf/bpf_lsm.c
> @@ -51,6 +51,9 @@ BTF_ID(func, bpf_lsm_key_getsecurity)
> #ifdef CONFIG_AUDIT
> BTF_ID(func, bpf_lsm_audit_rule_match)
> #endif
> +#ifdef CONFIG_SECURITY_NETWORK_XFRM
> +BTF_ID(func, bpf_lsm_xfrm_decode_session)
> +#endif
Applied this fix to bpf tree.
^ permalink raw reply
* [PATCH] apparmor: mv get_loaddata_common_ref() into CONFIG_SECURITY_APPARMOR_EXPORT_BINARY block
From: yaolu @ 2026-06-23 1:50 UTC (permalink / raw)
To: john.johansen, paul, jmorris, serge
Cc: apparmor, linux-security-module, linux-kernel, Lu Yao, k2ci
From: Lu Yao <yaolu@kylinos.cn>
When SECURITY_APPARMOR_EXPORT_BINARY is not set, the compiler emits an
unused-function warning which is promoted to an error with -Werror:
security/apparmor/apparmorfs.c:177:28: error: ‘get_loaddata_common_ref’ defined but not used [-Werror=unused-function]
Move the function into the #ifdef block to match its only call site,
silencing the warning.
Fixes: 8e135b8aee5a ("apparmor: fix race between freeing data and fs accessing it")
Reported-by: k2ci <kernel-bot@kylinos.cn>
Signed-off-by: Lu Yao <yaolu@kylinos.cn>
---
security/apparmor/apparmorfs.c | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/security/apparmor/apparmorfs.c b/security/apparmor/apparmorfs.c
index ededaf46f3ca..f762b101d682 100644
--- a/security/apparmor/apparmorfs.c
+++ b/security/apparmor/apparmorfs.c
@@ -174,14 +174,6 @@ static struct aa_proxy *get_proxy_common_ref(struct aa_common_ref *ref)
return NULL;
}
-static struct aa_loaddata *get_loaddata_common_ref(struct aa_common_ref *ref)
-{
- if (ref)
- return aa_get_i_loaddata(container_of(ref, struct aa_loaddata,
- count));
- return NULL;
-}
-
static void aa_put_common_ref(struct aa_common_ref *ref)
{
if (!ref)
@@ -1318,6 +1310,14 @@ static const struct file_operations seq_rawdata_ ##NAME ##_fops = { \
.release = seq_rawdata_release, \
} \
+static struct aa_loaddata *get_loaddata_common_ref(struct aa_common_ref *ref)
+{
+ if (ref)
+ return aa_get_i_loaddata(container_of(ref, struct aa_loaddata,
+ count));
+ return NULL;
+}
+
static int seq_rawdata_open(struct inode *inode, struct file *file,
int (*show)(struct seq_file *, void *))
{
--
2.25.1
^ permalink raw reply related
* Re: [PATCH bpf-next v3 1/2] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling
From: Alexei Starovoitov @ 2026-06-22 23:57 UTC (permalink / raw)
To: David Windsor, viro, brauner, jack, ast, daniel, john.fastabend,
andrii, eddyz87, memxor, martin.lau, song, yonghong.song, jolsa,
emil, kpsingh, mattbobrowski, paul, jmorris, serge, zohar,
roberto.sassu, dmitry.kasatkin, eric.snowberg,
stephen.smalley.work, omosnace, casey, shuah
Cc: linux-kernel, linux-fsdevel, bpf, linux-security-module,
linux-integrity, selinux, linux-kselftest
In-Reply-To: <20260618203411.73917-2-dwindsor@gmail.com>
On Thu Jun 18, 2026 at 1:34 PM PDT, David Windsor wrote:
> +
> +static int __bpf_init_inode_xattr(struct xattr_ctx *xattr_ctx,
> + const char *name__str,
> + const struct bpf_dynptr *value_p)
> +{
> + struct bpf_dynptr_kern *value_ptr = (struct bpf_dynptr_kern *)value_p;
> + size_t name_len;
> + void *xattr_value;
> + struct xattr *xattr;
> + struct xattr *xattrs;
> + int *xattr_count;
> + const void *value;
> + u32 value_len;
> +
> + if (!xattr_ctx || !name__str)
> + return -EINVAL;
> +
> + xattrs = xattr_ctx->xattrs;
> + xattr_count = xattr_ctx->xattr_count;
> + if (!xattrs || !xattr_count)
> + return -EINVAL;
> + if (bpf_xattrs_used(xattr_ctx) >= BPF_LSM_INODE_INIT_XATTRS)
> + return -ENOSPC;
This check is good to have, but it's enough. No need to duplicate it.
More below.
> +
> + name_len = strlen(name__str);
> + if (name_len == 0 || name_len > XATTR_NAME_MAX)
> + return -EINVAL;
> + if (strncmp(name__str, XATTR_BPF_LSM_SUFFIX,
> + sizeof(XATTR_BPF_LSM_SUFFIX) - 1))
> + return -EPERM;
> +
> + value_len = __bpf_dynptr_size(value_ptr);
> + if (value_len == 0 || value_len > XATTR_SIZE_MAX)
> + return -EINVAL;
> +
> + value = __bpf_dynptr_data(value_ptr, value_len);
> + if (!value)
> + return -EINVAL;
> +
> + /* Combine xattr value + name into one allocation. */
> + xattr_value = kmalloc(value_len + name_len + 1, GFP_KERNEL);
> + if (!xattr_value)
> + return -ENOMEM;
> +
> + memcpy(xattr_value, value, value_len);
> + memcpy(xattr_value + value_len, name__str, name_len);
> + ((char *)xattr_value)[value_len + name_len] = '\0';
> +
> + xattr = lsm_get_xattr_slot(xattr_ctx);
> + if (!xattr) {
> + kfree(xattr_value);
> + return -ENOSPC;
> + }
> +
> + xattr->value = xattr_value;
> + xattr->name = (const char *)xattr_value + value_len;
> + xattr->value_len = value_len;
> +
> + return 0;
> +}
> +
> +/**
> + * bpf_init_inode_xattr - set an xattr on a new inode from inode_init_security
> + * @xattr_ctx: inode_init_security xattr state from the hook context
> + * @name__str: xattr name (e.g., "bpf.file_label")
> + * @value_p: dynptr containing the xattr value
> + *
> + * Only callable from lsm/inode_init_security programs.
> + *
> + * Return: 0 on success, negative error on failure.
> + */
> +__bpf_kfunc int bpf_init_inode_xattr(struct xattr_ctx *xattr_ctx,
> + const char *name__str,
> + const struct bpf_dynptr *value_p)
> +{
> + return __bpf_init_inode_xattr(xattr_ctx, name__str, value_p);
> +}
> +
> __bpf_kfunc_end_defs();
>
> BTF_KFUNCS_START(bpf_fs_kfunc_set_ids)
> @@ -385,13 +477,25 @@ BTF_ID_FLAGS(func, bpf_get_file_xattr, KF_SLEEPABLE)
> BTF_ID_FLAGS(func, bpf_set_dentry_xattr, KF_SLEEPABLE)
> BTF_ID_FLAGS(func, bpf_remove_dentry_xattr, KF_SLEEPABLE)
> BTF_ID_FLAGS(func, bpf_real_inode, KF_SLEEPABLE | KF_RET_NULL)
> +BTF_ID_FLAGS(func, bpf_init_inode_xattr, KF_SLEEPABLE)
> BTF_KFUNCS_END(bpf_fs_kfunc_set_ids)
>
> +BTF_ID_LIST(bpf_lsm_inode_init_security_btf_ids)
> +BTF_ID(func, bpf_lsm_inode_init_security)
> +
> +BTF_ID_LIST(bpf_init_inode_xattr_btf_ids)
> +BTF_ID(func, bpf_init_inode_xattr)
> +
> static int bpf_fs_kfuncs_filter(const struct bpf_prog *prog, u32 kfunc_id)
> {
> if (!btf_id_set8_contains(&bpf_fs_kfunc_set_ids, kfunc_id) ||
> - prog->type == BPF_PROG_TYPE_LSM)
> + prog->type == BPF_PROG_TYPE_LSM) {
> + /* bpf_init_inode_xattr only attaches to inode_init_security. */
> + if (kfunc_id == bpf_init_inode_xattr_btf_ids[0] &&
> + prog->aux->attach_btf_id != bpf_lsm_inode_init_security_btf_ids[0])
> + return -EACCES;
This is unnecessary. Only one hook will have xattr_ctx type.
The normal verifier type enforcement will do its work.
> return 0;
> + }
> return -EACCES;
> }
>
> diff --git a/include/linux/bpf.h b/include/linux/bpf.h
> index 7719f6528445..f14bfcda78db 100644
> --- a/include/linux/bpf.h
> +++ b/include/linux/bpf.h
> @@ -1752,6 +1752,7 @@ struct bpf_prog_aux {
> u32 real_func_cnt; /* includes hidden progs, only used for JIT and freeing progs */
> u32 func_idx; /* 0 for non-func prog, the index in func array for func prog */
> u32 attach_btf_id; /* in-kernel BTF type id to attach to */
> + u32 attach_limit; /* max concurrent attachments (0 = unlimited) */
no need.
> u32 attach_st_ops_member_off;
> u32 ctx_arg_info_size;
> u32 max_rdonly_access;
> diff --git a/include/linux/bpf_lsm.h b/include/linux/bpf_lsm.h
> index 143775a27a2a..b655c708818e 100644
> --- a/include/linux/bpf_lsm.h
> +++ b/include/linux/bpf_lsm.h
> @@ -19,6 +19,9 @@
> #include <linux/lsm_hook_defs.h>
> #undef LSM_HOOK
>
> +/* max bpf xattrs per inode */
> +#define BPF_LSM_INODE_INIT_XATTRS 4
> +
> struct bpf_storage_blob {
> struct bpf_local_storage __rcu *storage;
> };
> diff --git a/include/linux/evm.h b/include/linux/evm.h
> index 913f4573b203..0aa151288b36 100644
> --- a/include/linux/evm.h
> +++ b/include/linux/evm.h
> @@ -12,6 +12,8 @@
> #include <linux/integrity.h>
> #include <linux/xattr.h>
>
> +struct xattr_ctx;
> +
> #ifdef CONFIG_EVM
> extern int evm_set_key(void *key, size_t keylen);
> extern enum integrity_status evm_verifyxattr(struct dentry *dentry,
> @@ -21,8 +23,8 @@ extern enum integrity_status evm_verifyxattr(struct dentry *dentry,
> int evm_fix_hmac(struct dentry *dentry, const char *xattr_name,
> const char *xattr_value, size_t xattr_value_len);
> int evm_inode_init_security(struct inode *inode, struct inode *dir,
> - const struct qstr *qstr, struct xattr *xattrs,
> - int *xattr_count);
> + const struct qstr *qstr,
> + struct xattr_ctx *xattr_ctx);
> extern bool evm_revalidate_status(const char *xattr_name);
> extern int evm_protected_xattr_if_enabled(const char *req_xattr_name);
> extern int evm_read_protected_xattrs(struct dentry *dentry, u8 *buffer,
> @@ -63,8 +65,7 @@ static inline int evm_fix_hmac(struct dentry *dentry, const char *xattr_name,
>
> static inline int evm_inode_init_security(struct inode *inode, struct inode *dir,
> const struct qstr *qstr,
> - struct xattr *xattrs,
> - int *xattr_count)
> + struct xattr_ctx *xattr_ctx)
> {
> return 0;
> }
> diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
> index 65c9609ec207..f62780fbeb9e 100644
> --- a/include/linux/lsm_hook_defs.h
> +++ b/include/linux/lsm_hook_defs.h
> @@ -116,8 +116,8 @@ LSM_HOOK(int, 0, inode_alloc_security, struct inode *inode)
> LSM_HOOK(void, LSM_RET_VOID, inode_free_security, struct inode *inode)
> LSM_HOOK(void, LSM_RET_VOID, inode_free_security_rcu, void *inode_security)
> LSM_HOOK(int, -EOPNOTSUPP, inode_init_security, struct inode *inode,
> - struct inode *dir, const struct qstr *qstr, struct xattr *xattrs,
> - int *xattr_count)
> + struct inode *dir, const struct qstr *qstr,
> + struct xattr_ctx *xattr_ctx)
> LSM_HOOK(int, 0, inode_init_security_anon, struct inode *inode,
> const struct qstr *name, const struct inode *context_inode)
> LSM_HOOK(int, 0, inode_create, struct inode *dir, struct dentry *dentry,
> diff --git a/include/linux/lsm_hooks.h b/include/linux/lsm_hooks.h
> index b4f8cad53ddb..710e48caaeba 100644
> --- a/include/linux/lsm_hooks.h
> +++ b/include/linux/lsm_hooks.h
> @@ -200,20 +200,18 @@ extern struct lsm_static_calls_table static_calls_table __ro_after_init;
>
> /**
> * lsm_get_xattr_slot - Return the next available slot and increment the index
> - * @xattrs: array storing LSM-provided xattrs
> - * @xattr_count: number of already stored xattrs (updated)
> + * @ctx: xattr state shared by inode_init_security hooks
> *
> - * Retrieve the first available slot in the @xattrs array to fill with an xattr,
> - * and increment @xattr_count.
> + * Retrieve the first available slot in the @ctx->xattrs array to fill with an
> + * xattr, and increment @ctx->xattr_count.
> *
> - * Return: The slot to fill in @xattrs if non-NULL, NULL otherwise.
> + * Return: The slot to fill in @ctx->xattrs if non-NULL, NULL otherwise.
> */
> -static inline struct xattr *lsm_get_xattr_slot(struct xattr *xattrs,
> - int *xattr_count)
> +static inline struct xattr *lsm_get_xattr_slot(struct xattr_ctx *ctx)
> {
> - if (unlikely(!xattrs))
> + if (unlikely(!ctx || !ctx->xattrs || !ctx->xattr_count))
> return NULL;
> - return &xattrs[(*xattr_count)++];
> + return &ctx->xattrs[(*ctx->xattr_count)++];
> }
>
> #endif /* ! __LINUX_LSM_HOOKS_H */
> diff --git a/include/linux/security.h b/include/linux/security.h
> index 153e9043058f..1f8e84e7dd7e 100644
> --- a/include/linux/security.h
> +++ b/include/linux/security.h
> @@ -68,6 +68,11 @@ struct watch;
> struct watch_notification;
> struct lsm_ctx;
>
> +struct xattr_ctx {
> + struct xattr *xattrs;
> + int *xattr_count;
> +};
> +
> /* Default (no) options for the capable function */
> #define CAP_OPT_NONE 0x0
> /* If capable should audit the security request */
> diff --git a/kernel/bpf/bpf_lsm.c b/kernel/bpf/bpf_lsm.c
> index 564071a92d7d..86a8e188b900 100644
> --- a/kernel/bpf/bpf_lsm.c
> +++ b/kernel/bpf/bpf_lsm.c
> @@ -113,6 +113,9 @@ void bpf_lsm_find_cgroup_shim(const struct bpf_prog *prog,
> }
> #endif
>
> +BTF_ID_LIST_SINGLE(bpf_lsm_inode_init_security_btf_ids, func,
> + bpf_lsm_inode_init_security)
> +
> int bpf_lsm_verify_prog(struct bpf_verifier_log *vlog,
> const struct bpf_prog *prog)
> {
> @@ -137,6 +140,12 @@ int bpf_lsm_verify_prog(struct bpf_verifier_log *vlog,
> return -EINVAL;
> }
>
> + /* bpf reserves a fixed number of xattr slots for itself.
> + * Set the attach limit so the trampoline rejects excess attaches.
> + */
> + if (btf_id == bpf_lsm_inode_init_security_btf_ids[0])
> + prog->aux->attach_limit = BPF_LSM_INODE_INIT_XATTRS;
> +
> return 0;
> }
>
> @@ -315,6 +324,7 @@ BTF_ID(func, bpf_lsm_inode_create)
> BTF_ID(func, bpf_lsm_inode_free_security)
> BTF_ID(func, bpf_lsm_inode_getattr)
> BTF_ID(func, bpf_lsm_inode_getxattr)
> +BTF_ID(func, bpf_lsm_inode_init_security)
> BTF_ID(func, bpf_lsm_inode_mknod)
> BTF_ID(func, bpf_lsm_inode_need_killpriv)
> BTF_ID(func, bpf_lsm_inode_post_setxattr)
> diff --git a/kernel/bpf/trampoline.c b/kernel/bpf/trampoline.c
> index 1a721fc4bef5..b41b02173e24 100644
> --- a/kernel/bpf/trampoline.c
> +++ b/kernel/bpf/trampoline.c
> @@ -859,6 +859,9 @@ static int bpf_trampoline_add_prog(struct bpf_trampoline *tr,
> }
> if (cnt >= BPF_MAX_TRAMP_LINKS)
> return -E2BIG;
> + if (node->link->prog->aux->attach_limit &&
> + tr->progs_cnt[kind] >= node->link->prog->aux->attach_limit)
> + return -E2BIG;
No need. The check inside kfunc is enough.
> if (!hlist_unhashed(&node->tramp_hlist))
> /* prog already linked */
> return -EBUSY;
> diff --git a/security/bpf/hooks.c b/security/bpf/hooks.c
> index 40efde233f3a..d7c44c5c0e30 100644
> --- a/security/bpf/hooks.c
> +++ b/security/bpf/hooks.c
> @@ -30,6 +30,7 @@ static int __init bpf_lsm_init(void)
>
> struct lsm_blob_sizes bpf_lsm_blob_sizes __ro_after_init = {
> .lbs_inode = sizeof(struct bpf_storage_blob),
> + .lbs_xattr_count = BPF_LSM_INODE_INIT_XATTRS,
> };
>
> DEFINE_LSM(bpf) = {
> diff --git a/security/integrity/evm/evm_main.c b/security/integrity/evm/evm_main.c
> index b59e3f121b8a..e0a05162accc 100644
> --- a/security/integrity/evm/evm_main.c
> +++ b/security/integrity/evm/evm_main.c
> @@ -1062,14 +1062,16 @@ static int evm_inode_copy_up_xattr(struct dentry *src, const char *name)
> * evm_inode_init_security - initializes security.evm HMAC value
> */
> int evm_inode_init_security(struct inode *inode, struct inode *dir,
> - const struct qstr *qstr, struct xattr *xattrs,
> - int *xattr_count)
> + const struct qstr *qstr,
> + struct xattr_ctx *xattr_ctx)
the rest looks good.
Pls split the patch. Introduce xattr_ctx first across the LSMs.
Then another patch with a new kfunc.
pw-bot: cr
^ permalink raw reply
* [PATCH] tests: add a regression test for the TCP Fast Open connect-mediation bypass
From: Bryam Vargas via B4 Relay @ 2026-06-22 23:27 UTC (permalink / raw)
To: apparmor; +Cc: linux-security-module, John Johansen, Ryan Lee
From: Bryam Vargas <hexlabsecurity@proton.me>
sendto(MSG_FASTOPEN) performs an implicit connect that the kernel's
apparmor_socket_sendmsg() did not mediate as a connect, so a profile
granting inet/inet6 stream send but denying connect was bypassed. Add a
test that, under such a profile, asserts connect(2) is denied AND
sendto(MSG_FASTOPEN) is also denied -- the latter requires the kernel fix
"apparmor: mediate the implicit connect of TCP fast open sendmsg".
It exercises both producers the fix guards -- plain TCP (inet/inet6) and
MPTCP (IPPROTO_MPTCP) -- plus a positive control where connect is allowed.
The test red-baselines on a vulnerable kernel and skips cleanly when the
required fine-grained network mediation or TCP Fast Open is unavailable
(requires_any_of_kernel_features / requires_parser_support, plus a
tcp_fastopen guard); the MPTCP cases are skipped if MPTCP is disabled.
Signed-off-by: Bryam Vargas <hexlabsecurity@proton.me>
---
This is the userspace regression test for the AppArmor TCP Fast Open
connect-mediation kernel fix posted to linux-security-module:
https://lore.kernel.org/all/20260622-b4-disp-aba401c6-v1-1-9d74343c7ced@proton.me/
It mirrors the SELinux testsuite test ("[PATCH testsuite] tests/inet_socket:
add tests for TCP Fast Open", Stephen Smalley) and was requested by the
AppArmor team (Ryan Lee).
It covers both producers the kernel fix mediates -- plain TCP (inet/inet6)
and MPTCP -- plus a positive control. It red-baselines on a vulnerable
kernel (the fastopen assertions fail) and skips cleanly when TCP Fast Open
or fine-grained network mediation is unavailable.
---
tests/regression/apparmor/Makefile | 2 +
tests/regression/apparmor/net_inet_tcp_fastopen.c | 241 +++++++++++++++++++++
tests/regression/apparmor/net_inet_tcp_fastopen.sh | 119 ++++++++++
3 files changed, 362 insertions(+)
diff --git a/tests/regression/apparmor/Makefile b/tests/regression/apparmor/Makefile
index 345f39968..18e408f5c 100644
--- a/tests/regression/apparmor/Makefile
+++ b/tests/regression/apparmor/Makefile
@@ -151,6 +151,7 @@ SRC=access.c \
named_pipe.c \
net_inet_rcv.c \
net_inet_snd.c \
+ net_inet_tcp_fastopen.c \
net_raw.c \
open.c \
openat.c \
@@ -325,6 +326,7 @@ TESTS=aa_exec \
namespaces \
net_iface \
net_inet \
+ net_inet_tcp_fastopen \
net_raw \
overlayfs_kernel \
open \
diff --git a/tests/regression/apparmor/net_inet_tcp_fastopen.c b/tests/regression/apparmor/net_inet_tcp_fastopen.c
new file mode 100644
index 000000000..cfc1ff9c5
--- /dev/null
+++ b/tests/regression/apparmor/net_inet_tcp_fastopen.c
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2026 Canonical, Ltd.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, version 2 of the
+ * License.
+ */
+
+/*
+ * TCP Fast Open connect-mediation bypass regression test.
+ *
+ * Under an AppArmor profile that grants inet/inet6 stream "send" but DENIES
+ * "connect", a plain connect(2) must be refused (EACCES/EPERM). Historically
+ * the kernel's TFO fast path (sendto(..., MSG_FASTOPEN, ...), which performs
+ * an implicit connect) only checked the send permission (AA_NET_SEND 0x02)
+ * and skipped the connect permission (AA_NET_CONNECT 0x40), so a confined
+ * task could open an outbound connection that connect(2) would have blocked.
+ * The kernel fix mediates both producers: plain TCP and MPTCP (IPPROTO_MPTCP).
+ *
+ * This binary takes a mode and asserts the operation is DENIED:
+ * argv[1] = "connect" -> baseline: connect(2) must be denied
+ * argv[1] = "fastopen" -> the bug: sendto(MSG_FASTOPEN) must be denied
+ * argv[2] = family: "inet"/"inet6" (TCP) or "minet"/"minet6" (MPTCP)
+ * argv[3] = port (the listener port, set up by this same process)
+ *
+ * Output contract (parsed by checktestfg in prologue.inc):
+ * "PASS\n" -> the operation was DENIED as required (regression OK)
+ * "FAIL ..."-> the operation was ALLOWED (connect bypass) OR a setup error
+ *
+ * The .sh runs this with expected outcome "pass"; it also enables TCP Fast
+ * Open first, so an EOPNOTSUPP here is a real setup error, not a skip.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <signal.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <arpa/inet.h>
+
+#ifndef MSG_FASTOPEN
+#define MSG_FASTOPEN 0x20000000
+#endif
+
+#ifndef IPPROTO_MPTCP
+#define IPPROTO_MPTCP 262
+#endif
+
+/* Map a family token to (AF_*, protocol). "minet"/"minet6" select MPTCP.
+ * Returns 0 on success, -1 on an unknown token.
+ */
+static int parse_family(const char *tok, int *family, int *proto)
+{
+ *proto = 0;
+ if (strcmp(tok, "inet") == 0) {
+ *family = AF_INET;
+ } else if (strcmp(tok, "inet6") == 0) {
+ *family = AF_INET6;
+ } else if (strcmp(tok, "minet") == 0) {
+ *family = AF_INET;
+ *proto = IPPROTO_MPTCP;
+ } else if (strcmp(tok, "minet6") == 0) {
+ *family = AF_INET6;
+ *proto = IPPROTO_MPTCP;
+ } else {
+ return -1;
+ }
+ return 0;
+}
+
+/* Build a loopback sockaddr for the requested family. Returns addrlen. */
+static socklen_t make_addr(int family, int port, struct sockaddr_storage *ss)
+{
+ memset(ss, 0, sizeof(*ss));
+ if (family == AF_INET) {
+ struct sockaddr_in *a = (struct sockaddr_in *)ss;
+
+ a->sin_family = AF_INET;
+ a->sin_port = htons(port);
+ inet_pton(AF_INET, "127.0.0.1", &a->sin_addr);
+ return sizeof(*a);
+ }
+ {
+ struct sockaddr_in6 *a = (struct sockaddr_in6 *)ss;
+
+ a->sin6_family = AF_INET6;
+ a->sin6_port = htons(port);
+ inet_pton(AF_INET6, "::1", &a->sin6_addr);
+ return sizeof(*a);
+ }
+}
+
+/* Start a plain TCP listener so the connect/TFO target exists. Returns the fd
+ * or -1. A TCP listener accepts both TCP and MPTCP clients, which keeps the
+ * test on the client-side mediation under examination. bind/listen perms are
+ * granted by the profile so this must succeed.
+ */
+static int start_listener(int family, int port)
+{
+ int s, one = 1;
+ struct sockaddr_storage ss;
+ socklen_t len = make_addr(family, port, &ss);
+
+ s = socket(family, SOCK_STREAM, 0);
+ if (s < 0) {
+ printf("FAIL - listener socket: %m\n");
+ return -1;
+ }
+ (void)setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
+ /* Enable TFO on the listener (qlen). Best-effort; the mediation check
+ * under test happens on the client side. */
+ (void)setsockopt(s, IPPROTO_TCP, TCP_FASTOPEN, &one, sizeof(one));
+ if (bind(s, (struct sockaddr *)&ss, len) < 0) {
+ printf("FAIL - listener bind: %m\n");
+ close(s);
+ return -1;
+ }
+ if (listen(s, 5) < 0) {
+ printf("FAIL - listener listen: %m\n");
+ close(s);
+ return -1;
+ }
+ return s;
+}
+
+/* Returns 1 if the kernel DENIED the operation (EACCES/EPERM) => regression OK.
+ * Returns 0 if the operation was ALLOWED (connect bypass) => regression FAIL.
+ * Returns -1 on a setup error.
+ */
+static int try_connect(int family, int proto, int port)
+{
+ int s, rc;
+ struct sockaddr_storage ss;
+ socklen_t len = make_addr(family, port, &ss);
+
+ s = socket(family, SOCK_STREAM, proto);
+ if (s < 0)
+ return -1;
+ rc = connect(s, (struct sockaddr *)&ss, len);
+ if (rc == 0) {
+ close(s);
+ return 0; /* allowed */
+ }
+ if (errno == EACCES || errno == EPERM) {
+ close(s);
+ return 1; /* denied by AppArmor */
+ }
+ /* ECONNREFUSED/ETIMEDOUT mean it reached the network: mediation did not
+ * block it, so count as allowed. */
+ close(s);
+ return (errno == ECONNREFUSED || errno == ETIMEDOUT) ? 0 : -1;
+}
+
+static int try_fastopen(int family, int proto, int port)
+{
+ int s;
+ ssize_t rc;
+ char msg[] = "tfo";
+ struct sockaddr_storage ss;
+ socklen_t len = make_addr(family, port, &ss);
+
+ s = socket(family, SOCK_STREAM, proto);
+ if (s < 0)
+ return -1;
+
+ /* The bug: this implicit-connect send must be mediated as a connect. */
+ rc = sendto(s, msg, sizeof(msg), MSG_FASTOPEN,
+ (struct sockaddr *)&ss, len);
+ if (rc >= 0) {
+ close(s);
+ return 0; /* allowed: connect bypass */
+ }
+ if (errno == EACCES || errno == EPERM) {
+ close(s);
+ return 1; /* denied by AppArmor */
+ }
+ if (errno == EOPNOTSUPP || errno == EINVAL) {
+ /* The .sh enabled TCP Fast Open before running, so this is a
+ * real setup error, not an expected condition. Fail loudly
+ * rather than masking it as a denial. */
+ close(s);
+ return -1;
+ }
+ close(s);
+ return (errno == ECONNREFUSED || errno == ETIMEDOUT) ? 0 : -1;
+}
+
+int main(int argc, char *argv[])
+{
+ int family, proto, port, denied, listener;
+ const char *mode;
+
+ if (argc < 4) {
+ printf("FAIL - usage: %s connect|fastopen inet|inet6|minet|minet6 port\n",
+ argv[0]);
+ return 1;
+ }
+ mode = argv[1];
+ if (parse_family(argv[2], &family, &proto) < 0) {
+ printf("FAIL - unknown family '%s'\n", argv[2]);
+ return 1;
+ }
+ port = atoi(argv[3]);
+
+ signal(SIGPIPE, SIG_IGN);
+
+ listener = start_listener(family, port);
+ if (listener < 0)
+ return 1; /* FAIL already printed */
+
+ if (strcmp(mode, "connect") == 0) {
+ denied = try_connect(family, proto, port);
+ } else if (strcmp(mode, "fastopen") == 0) {
+ denied = try_fastopen(family, proto, port);
+ } else {
+ printf("FAIL - unknown mode '%s'\n", mode);
+ close(listener);
+ return 1;
+ }
+
+ close(listener);
+
+ if (denied == 1) {
+ printf("PASS\n");
+ return 0;
+ }
+ if (denied == 0) {
+ printf("FAIL - %s was ALLOWED despite deny connect "
+ "(connect-mediation bypass)\n", mode);
+ return 1;
+ }
+ printf("FAIL - %s setup error: %m\n", mode);
+ return 1;
+}
diff --git a/tests/regression/apparmor/net_inet_tcp_fastopen.sh b/tests/regression/apparmor/net_inet_tcp_fastopen.sh
new file mode 100755
index 000000000..76300c53f
--- /dev/null
+++ b/tests/regression/apparmor/net_inet_tcp_fastopen.sh
@@ -0,0 +1,119 @@
+#! /bin/bash
+# Copyright (C) 2026 Canonical, Ltd.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation, version 2 of the
+# License.
+
+#=NAME net_inet_tcp_fastopen
+#=DESCRIPTION
+# Regression test for the TCP Fast Open connect-mediation bypass. Under a
+# profile that grants inet/inet6 stream "send" but DENIES "connect", a plain
+# connect(2) is refused, and sendto(..., MSG_FASTOPEN, ...) (which performs an
+# implicit connect) MUST also be refused -- for both plain TCP and MPTCP. Pre-fix
+# the TFO path checked only the send permission (AA_NET_SEND 0x02) and skipped
+# connect (AA_NET_CONNECT 0x40).
+#=END
+
+pwd=`dirname $0`
+pwd=`cd $pwd ; /bin/pwd`
+
+bin=$pwd
+
+. "$bin/prologue.inc"
+
+# Need fine-grained inet mediation (connect/send are separable only there).
+requires_any_of_kernel_features network_v8/af_inet network_v9/af_inet
+requires_parser_support "network (send) ip=::1,"
+
+settest net_inet_tcp_fastopen
+
+tfo_sysctl=/proc/sys/net/ipv4/tcp_fastopen
+tfo_saved=""
+
+cleanup()
+{
+ # restore the original tcp_fastopen value if we changed it
+ if [ -n "$tfo_saved" ]; then
+ echo "$tfo_saved" > "$tfo_sysctl" 2>/dev/null || true
+ fi
+}
+do_onexit="cleanup"
+
+# The sendto(MSG_FASTOPEN) client path needs the TCP Fast Open client bit
+# (0x1). Enable it for the run; if it is unavailable (no sysctl, or it cannot
+# be enabled) the bug cannot be exercised at all, so skip rather than report a
+# spurious failure.
+if [ ! -w "$tfo_sysctl" ]; then
+ echo " TCP Fast Open sysctl ($tfo_sysctl) not available. Skipping tests ..."
+ exit 0
+fi
+tfo_saved=`cat "$tfo_sysctl"`
+echo $((tfo_saved | 1)) > "$tfo_sysctl" 2>/dev/null || true
+if [ $(($(cat "$tfo_sysctl") & 1)) -ne 1 ]; then
+ echo " Could not enable the TCP Fast Open client bit. Skipping tests ..."
+ exit 0
+fi
+
+# add ::1 if not already present (loopback usually has it)
+ip -6 addr add ::1/128 dev lo 2>/dev/null || true
+
+# pick a free port for the listener this binary creates
+port=4321
+while lsof -i:$port >/dev/null 2>&1; do
+ let port=$port+1
+done
+
+# Profile: allow stream send/receive + the perms needed to stand up the
+# in-process listener (bind/listen/accept), allow setopt/getopt for TFO
+# sockopts, but explicitly DENY connect on both inet and inet6.
+gen_send_no_connect()
+{
+ genprofile \
+ "network;(send,receive,accept,listen,bind);ip=127.0.0.1;port=$port" \
+ "network;(send,receive,accept,listen,bind);ip=::1;port=$port" \
+ "network;(send,receive);peer=(ip=127.0.0.1)" \
+ "network;(send,receive);peer=(ip=::1)" \
+ "network;(setopt,getopt);ip=0.0.0.0;port=0" \
+ "network;(setopt,getopt);ip=::0;port=0" \
+ "qual=deny:network;(connect);ip=127.0.0.1" \
+ "qual=deny:network;(connect);ip=::1"
+}
+
+# ---- inet (IPv4) ----
+gen_send_no_connect
+# baseline: a normal connect(2) must be denied -> binary prints PASS (denied),
+# expected outcome 'pass'
+runchecktest "TFO inet - connect(2) denied" pass connect inet $port
+# the bug: sendto(MSG_FASTOPEN) must ALSO be denied post-fix
+runchecktest "TFO inet - sendto(MSG_FASTOPEN) denied" pass fastopen inet $port
+
+# ---- inet6 (IPv6) ----
+gen_send_no_connect
+runchecktest "TFO inet6 - connect(2) denied" pass connect inet6 $port
+runchecktest "TFO inet6 - sendto(MSG_FASTOPEN) denied" pass fastopen inet6 $port
+
+# ---- MPTCP: the second producer the fix guards (IPPROTO_MPTCP) ----
+# The deny-connect rule is family/type based, so it covers MPTCP (inet/inet6
+# stream) too. Only run when MPTCP is enabled.
+if [ "`cat /proc/sys/net/mptcp/enabled 2>/dev/null`" = "1" ]; then
+ gen_send_no_connect
+ runchecktest "TFO MPTCP inet - connect(2) denied" pass connect minet $port
+ runchecktest "TFO MPTCP inet - sendto(MSG_FASTOPEN) denied" pass fastopen minet $port
+ gen_send_no_connect
+ runchecktest "TFO MPTCP inet6 - connect(2) denied" pass connect minet6 $port
+ runchecktest "TFO MPTCP inet6 - sendto(MSG_FASTOPEN) denied" pass fastopen minet6 $port
+fi
+
+# ---- positive control: when connect IS allowed, both succeed (no false deny) ----
+genprofile \
+ "network;(connect,send,receive,accept,listen,bind);ip=127.0.0.1;port=$port" \
+ "network;(connect,send,receive);peer=(ip=127.0.0.1)" \
+ "network;(setopt,getopt);ip=0.0.0.0;port=0"
+# Here the binary's "denied" assertion is FALSE (op allowed), so it prints
+# FAIL; we expect that, i.e. expected outcome 'fail'.
+runchecktest "TFO inet - connect allowed (control)" fail connect inet $port
+runchecktest "TFO inet - fastopen allowed (control)" fail fastopen inet $port
+
+exit 0
---
base-commit: bdccc1ebd2e1a1b75ceb8f87b23831fe273b9ebb
change-id: 20260622-b4-disp-220b400d-3d7fd53bce49
Best regards,
--
bryamzxz <hexlabsecurity@proton.me>
^ permalink raw reply related
* [PATCH] apparmor: mediate the implicit connect of TCP fast open sendmsg
From: Bryam Vargas via B4 Relay @ 2026-06-22 20:57 UTC (permalink / raw)
To: Ryan Lee, John Johansen
Cc: Mickael Salaun, Stephen Smalley, James Morris, Matthieu Buffet,
linux-security-module, Mikhail Ivanov, Serge E. Hallyn,
linux-kernel, Paul Moore, apparmor
From: Bryam Vargas <hexlabsecurity@proton.me>
sendmsg()/sendto() with MSG_FASTOPEN is a combination of connect(2) and
write(2): it opens the connection in the SYN. apparmor_socket_sendmsg()
only checks AA_MAY_SEND, so a profile that grants send but denies connect
lets a confined task open an outbound TCP/MPTCP connection that connect(2)
would have refused, bypassing connect mediation.
Mediate the implicit connect when MSG_FASTOPEN is set and a destination
is supplied. Add it to apparmor_socket_sendmsg() (not the shared
aa_sock_msg_perm() helper, which recvmsg also uses) and call aa_sk_perm()
directly, mirroring the selinux and tomoyo fixes. sk_is_tcp() does not
cover MPTCP fast open, so the SOCK_STREAM/IPPROTO_MPTCP arm is explicit.
Fixes: cf60af03ca4e ("net-tcp: Fast Open client - sendmsg(MSG_FASTOPEN)")
Cc: stable@vger.kernel.org
Signed-off-by: Bryam Vargas <hexlabsecurity@proton.me>
---
This is the patch and reproducer requested in [1]. A userspace regression test
(tests/regression/apparmor/net_inet_tcp_fastopen) follows separately to the
apparmor tree, as suggested.
Reproducer (behavioral; the bypassed value is policy, not bus state, so no special
hardware). Under a profile that grants inet/inet6 stream send but denies connect, on
the current Debian security kernel 6.12.94 (apparmor active):
[TCP ] connect(2)=EACCES sendto(MSG_FASTOPEN)=OK -> connect bypassed (listener accepted)
[TCP6] connect(2)=EACCES sendto(MSG_FASTOPEN)=OK -> connect bypassed
The kernel audit shows the connect(2) denial and no connect record for the fastopen
sendto:
apparmor="DENIED" operation="connect" profile="egress_restricted" comm="lsm_tfo_ab"
family="inet" sock_type="stream" protocol=6 requested_mask="connect" denied_mask="connect"
With this patch the fastopen sendto hits that same connect denial. Full reproducer
available on request.
Same-class fixes: selinux [2], tomoyo [3]; the original cross-LSM report (landlock,
the first instance) is [4].
[1] https://lore.kernel.org/r/20260619011138.264578-1-hexlabsecurity@proton.me
[2] https://lore.kernel.org/r/20260618175513.112443-2-stephen.smalley.work@gmail.com
[3] https://lore.kernel.org/r/20260619002207.61104-1-matthieu@buffet.re
[4] https://lore.kernel.org/r/20260616201615.275032-1-hexlabsecurity@proton.me
---
security/apparmor/lsm.c | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/security/apparmor/lsm.c b/security/apparmor/lsm.c
index 3491e9f60194..e01efdf50efa 100644
--- a/security/apparmor/lsm.c
+++ b/security/apparmor/lsm.c
@@ -1422,7 +1422,21 @@ static int aa_sock_msg_perm(const char *op, u32 request, struct socket *sock,
static int apparmor_socket_sendmsg(struct socket *sock,
struct msghdr *msg, int size)
{
- return aa_sock_msg_perm(OP_SENDMSG, AA_MAY_SEND, sock, msg, size);
+ int error = aa_sock_msg_perm(OP_SENDMSG, AA_MAY_SEND, sock, msg, size);
+
+ if (error)
+ return error;
+
+ /* TCP fast open carries connect() semantics in sendmsg(); mediate
+ * the implicit connect so it cannot bypass the connect permission.
+ */
+ if ((msg->msg_flags & MSG_FASTOPEN) && msg->msg_name &&
+ (sk_is_tcp(sock->sk) ||
+ (sk_is_inet(sock->sk) && sock->sk->sk_type == SOCK_STREAM &&
+ sock->sk->sk_protocol == IPPROTO_MPTCP)))
+ error = aa_sk_perm(OP_CONNECT, AA_MAY_CONNECT, sock->sk);
+
+ return error;
}
static int apparmor_socket_recvmsg(struct socket *sock,
---
base-commit: 4549871118cf616eecdd2d939f78e3b9e1dddc48
change-id: 20260622-b4-disp-aba401c6-f02842c82975
Best regards,
--
bryamzxz <hexlabsecurity@proton.me>
^ permalink raw reply related
* Re: AppArmor: TCP Fast Open bypasses connect mediation (last unaddressed LSM)
From: Ryan Lee @ 2026-06-22 16:01 UTC (permalink / raw)
To: Bryam Vargas
Cc: John Johansen, linux-security-module, apparmor, Paul Moore,
James Morris, Serge E . Hallyn, Mickael Salaun, Stephen Smalley,
Matthieu Buffet, Mikhail Ivanov, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, netdev, linux-kernel
In-Reply-To: <20260619011138.264578-1-hexlabsecurity@proton.me>
On Thu, Jun 18, 2026 at 6:12 PM Bryam Vargas <hexlabsecurity@proton.me> wrote:
>
> Hello John, and LSM folks,
>
> I have been working on the Landlock TCP Fast Open connect bypass [1]. Stephen
> Smalley's SELinux fix for the same issue [3] -- "Similar to Landlock, SELinux was
> not updated when TCP Fast Open support was introduced ..." -- made me go back and
> check the rest of the connect-mediating LSMs, since I had only been looking at
> Landlock. With Landlock [2], SELinux [3], and now TOMOYO [4] all getting fixes,
> AppArmor is the last one with the same gap and no fix yet.
>
> Root cause (shared with the others)
> -----------------------------------
> security_socket_connect() has a single call site, net/socket.c (the connect(2)
> syscall). TCP Fast Open performs an implicit connect inside sendmsg:
>
> tcp_sendmsg -> tcp_sendmsg_fastopen -> __inet_stream_connect(..., is_sendmsg=1)
> -> sk->sk_prot->connect() net/ipv4/{tcp.c,af_inet.c}
>
> This never calls security_socket_connect(); the only LSM hook on the path is
> security_socket_sendmsg(). mptcp_sendmsg_fastopen reaches the same code and is a
> second producer.
>
> AppArmor
> --------
> apparmor_socket_connect() requests AA_MAY_CONNECT; apparmor_socket_sendmsg() (via
> aa_sock_msg_perm) requests AA_MAY_SEND. These are distinct bits, and apparmor_parser
> compiles them independently: "network send inet stream," yields accept mask 0x02
> while "network connect inet stream," yields 0x40. So an egress-restriction profile
> that grants send but not connect is bypassed by MSG_FASTOPEN.
>
> Reproduced on 6.12.88 with apparmor active. Under a profile granting the inet/inet6
> stream lifecycle except connect:
>
> aa-exec -p egress_restricted -- ./probe
> [TCP ] connect(2)=EACCES(blocked) sendto(MSG_FASTOPEN)=OK(reached) => connection established
> [TCP6] connect(2)=EACCES(blocked) sendto(MSG_FASTOPEN)=OK(reached) => connection established
>
> (The coarse "network inet stream," idiom grants connect anyway, so this only bites the
> fine-grained "allow send, deny connect" policy that the asymmetry is meant to serve.)
>
> Fix
> ---
> Same shape as the TOMOYO [4] and SELinux [3] fixes: in apparmor_socket_sendmsg (or
> aa_sock_msg_perm), when MSG_FASTOPEN is set and msg_name carries a destination on a
> not-yet-connected stream socket, additionally require aa_sk_perm(OP_CONNECT,
> AA_MAY_CONNECT, sk). I am happy to send that patch and the reproducer.
We would appreciate having the patch and the reproducer to look over.
Ideally, the reproducer could be integrated as a regression test into
the upstream repo at
https://gitlab.com/apparmor/apparmor/-/tree/master/tests/regression/apparmor?ref_type=heads,
but we can also assist with that step.
>
> (A single core check in __inet_stream_connect(), gated on is_sendmsg, would have
> covered all five LSMs and both the TCP and MPTCP producers in one place -- the kernel
> already mediates the analogous implicit-connect-on-send for AF_UNIX via
> security_unix_may_send and for SCTP via security_sctp_bind_connect. But since the
> other four LSMs are taking per-hook fixes, AppArmor matching them is the consistent
> move; mentioning the core option only in case it is preferred.)
>
> [1] Landlock: LANDLOCK_ACCESS_NET_CONNECT_TCP bypass via TCP Fast Open (report)
> https://lore.kernel.org/r/20260616201615.275032-1-hexlabsecurity@proton.me
> [2] landlock: fix TCP Fast Open connection bypass (Matthieu Buffet)
> https://lore.kernel.org/r/20260617180526.15627-2-matthieu@buffet.re
> [3] selinux: check connect-related permissions on TCP Fast Open (Stephen Smalley)
> https://lore.kernel.org/r/20260618175513.112443-2-stephen.smalley.work@gmail.com
> [4] tomoyo: Enforce connect policy in TCP Fast Open (Matthieu Buffet)
> https://lore.kernel.org/r/20260619002207.61104-1-matthieu@buffet.re
>
> Thanks,
> Bryam Vargas
>
>
^ permalink raw reply
* Re: [PATCH] keys: trusted: dcp: Make dcp_trusted_key_ops const
From: Jarkko Sakkinen @ 2026-06-22 15:24 UTC (permalink / raw)
To: Berkay Yürekli
Cc: david, upstream+dcp, James.Bottomley, zohar, linux-integrity,
keyrings, linux-security-module
In-Reply-To: <CAMyZDTsA-m0R1Ziy74Ck0EPj8UFJhUw-vE0_ACrQ3hiLLaNaKQ@mail.gmail.com>
On Sun, Jun 21, 2026 at 09:05:40PM -0400, Berkay Yürekli wrote:
> Mark the dcp_trusted_key_ops structure as const to improve kernel
> self-protection. This structure contains function pointers that are
> initialized once during module initialization and never modified.
>
> Making function pointers read-only protects against memory corruption
> attacks where an attacker might overwrite these pointers to redirect
> execution flow, as recommended in Documentation/security/self-protection.rst.
>
> Signed-off-by: mcss <ps1296@owsa.nl>
full name please
>
> ---
> include/keys/trusted_dcp.h | 2 +-
> security/keys/trusted-keys/trusted_dcp.c | 2 +-
> 2 files changed, 2 insertions(+), 2 deletions(-)
>
> diff --git a/include/keys/trusted_dcp.h b/include/keys/trusted_dcp.h
> index 9aaa42075b40..8ee24f2d21ff 100644
> --- a/include/keys/trusted_dcp.h
> +++ b/include/keys/trusted_dcp.h
> @@ -6,6 +6,6 @@
> #ifndef TRUSTED_DCP_H
> #define TRUSTED_DCP_H
>
> -extern struct trusted_key_ops dcp_trusted_key_ops;
> +extern const struct trusted_key_ops dcp_trusted_key_ops;
>
> #endif
> diff --git a/security/keys/trusted-keys/trusted_dcp.c b/security/keys/
> trusted-keys/trusted_dcp.c
> index 7b6eb655df0c..f638078640f9 100644
> --- a/security/keys/trusted-keys/trusted_dcp.c
> +++ b/security/keys/trusted-keys/trusted_dcp.c
> @@ -347,7 +347,7 @@ static void trusted_dcp_exit(void)
> unregister_key_type(&key_type_trusted);
> }
>
> -struct trusted_key_ops dcp_trusted_key_ops = {
> +const struct trusted_key_ops dcp_trusted_key_ops = {
> .exit = trusted_dcp_exit,
> .init = trusted_dcp_init,
> .seal = trusted_dcp_seal,
> --
> 2.54.0
>
BR, Jarkko
^ permalink raw reply
* [stable/linux-6.6.y 2/3] lsm: add backing_file LSM hooks
From: Cai Xinchen @ 2026-06-22 3:15 UTC (permalink / raw)
To: viro, brauner, jack, miklos, amir73il, paul, jmorris, serge,
stephen.smalley.work, omosnace, gregkh, bboscaccy
Cc: linux-fsdevel, linux-kernel, linux-unionfs, linux-security-module,
selinux, bpf, lujialin4
In-Reply-To: <20260622031556.2664089-1-caixinchen1@huawei.com>
From: Paul Moore <paul@paul-moore.com>
1. Mainline uses call_int_hook(FUNC, ...) with the default IRC baked
into the macro. Linux 6.6.y uses call_int_hook(FUNC, IRC, ...) requiring
an explicit default return value.
2. fs/backing-file.c does not exist in LTS
Linux 6.6.y places backing_file_open() in fs/open.c and lacks a
dedicated fs/backing-file.c. The backing_file_mmap() function and
scoped_with_creds() do not exist in 6.6.y. Therefore the LTS patch calls
security_mmap_backing_file() directly in ovl_mmap() in
fs/overlayfs/file.c rather than modifying backing_file_mmap().
3. Missing filesystems/modules
Linux 6.6.y does not have backing_tmpfile_open(), fs/fuse/passthrough.c,
or the erofs ishare mmap path that the mainline patch touches. These hunks
are dropped in the 6.6 LTS backport.
Original commit message:
Stacked filesystems such as overlayfs do not currently provide the
necessary mechanisms for LSMs to properly enforce access controls on the
mmap() and mprotect() operations. In order to resolve this gap, a LSM
security blob is being added to the backing_file struct and the following
new LSM hooks are being created:
security_backing_file_alloc()
security_backing_file_free()
security_mmap_backing_file()
The first two hooks are to manage the lifecycle of the LSM security blob
in the backing_file struct, while the third provides a new mmap() access
control point for the underlying backing file. It is also expected that
LSMs will likely want to update their security_file_mprotect() callback
to address issues with their mprotect() controls, but that does not
require a change to the security_file_mprotect() LSM hook.
There are a three other small changes to support these new LSM hooks:
* Pass the user file associated with a backing file down to
alloc_empty_backing_file() so it can be included in the
security_backing_file_alloc() hook.
* Add getter and setter functions for the backing_file struct LSM blob
as the backing_file struct remains private to fs/file_table.c.
* Constify the file struct field in the LSM common_audit_data struct to
better support LSMs that need to pass a const file struct pointer into
the common LSM audit code.
Thanks to Arnd Bergmann for identifying the missing EXPORT_SYMBOL_GPL()
and supplying a fixup.
Cc: stable@vger.kernel.org
Cc: linux-fsdevel@vger.kernel.org
Cc: linux-unionfs@vger.kernel.org
Cc: linux-erofs@lists.ozlabs.org
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
Reviewed-by: Serge Hallyn <serge@hallyn.com>
Reviewed-by: Christian Brauner <brauner@kernel.org>
Signed-off-by: Paul Moore <paul@paul-moore.com>
Signed-off-by: Cai Xinchen <caixinchen1@huawei.com>
---
fs/file_table.c | 27 +++++++--
fs/internal.h | 3 +-
fs/open.c | 7 ++-
fs/overlayfs/file.c | 8 ++-
include/linux/fs.h | 15 ++++-
include/linux/lsm_audit.h | 2 +-
include/linux/lsm_hook_defs.h | 5 ++
include/linux/lsm_hooks.h | 1 +
include/linux/security.h | 22 +++++++
security/security.c | 110 ++++++++++++++++++++++++++++++++++
10 files changed, 189 insertions(+), 11 deletions(-)
diff --git a/fs/file_table.c b/fs/file_table.c
index b4c208a77153..6b535e9c6166 100644
--- a/fs/file_table.c
+++ b/fs/file_table.c
@@ -48,6 +48,9 @@ static struct percpu_counter nr_files __cacheline_aligned_in_smp;
struct backing_file {
struct file file;
struct path real_path;
+#ifdef CONFIG_SECURITY
+ void *security;
+#endif
};
static inline struct backing_file *backing_file(struct file *f)
@@ -72,8 +75,21 @@ static void file_free_rcu(struct rcu_head *head)
kmem_cache_free(filp_cachep, f);
}
+#ifdef CONFIG_SECURITY
+void *backing_file_security(const struct file *f)
+{
+ return backing_file(f)->security;
+}
+
+void backing_file_set_security(struct file *f, void *security)
+{
+ backing_file(f)->security = security;
+}
+#endif /* CONFIG_SECURITY */
+
static inline void backing_file_free(struct backing_file *ff)
{
+ security_backing_file_free(&ff->file);
path_put(&ff->real_path);
}
@@ -257,10 +273,12 @@ struct file *alloc_empty_file_noaccount(int flags, const struct cred *cred)
return f;
}
-static int init_backing_file(struct backing_file *ff)
+static int init_backing_file(struct backing_file *ff,
+ const struct file *user_file)
{
memset(&ff->real_path, 0, sizeof(ff->real_path));
- return 0;
+ backing_file_set_security(&ff->file, NULL);
+ return security_backing_file_alloc(&ff->file, user_file);
}
/*
@@ -270,7 +288,8 @@ static int init_backing_file(struct backing_file *ff)
* This is only for kernel internal use, and the allocate file must not be
* installed into file tables or such.
*/
-struct file *alloc_empty_backing_file(int flags, const struct cred *cred)
+struct file *alloc_empty_backing_file(int flags, const struct cred *cred,
+ const struct file *user_file)
{
struct backing_file *ff;
int error;
@@ -287,7 +306,7 @@ struct file *alloc_empty_backing_file(int flags, const struct cred *cred)
/* The f_mode flags must be set before fput(). */
ff->file.f_mode |= FMODE_BACKING | FMODE_NOACCOUNT;
- error = init_backing_file(ff);
+ error = init_backing_file(ff, user_file);
if (unlikely(error)) {
fput(&ff->file);
return ERR_PTR(error);
diff --git a/fs/internal.h b/fs/internal.h
index d64ae03998cc..576026e1ce3a 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -93,7 +93,8 @@ extern void chroot_fs_refs(const struct path *, const struct path *);
*/
struct file *alloc_empty_file(int flags, const struct cred *cred);
struct file *alloc_empty_file_noaccount(int flags, const struct cred *cred);
-struct file *alloc_empty_backing_file(int flags, const struct cred *cred);
+struct file *alloc_empty_backing_file(int flags, const struct cred *cred,
+ const struct file *user_file);
static inline void put_file_access(struct file *file)
{
diff --git a/fs/open.c b/fs/open.c
index b5ea1dcbfb22..5b164481d80b 100644
--- a/fs/open.c
+++ b/fs/open.c
@@ -1175,18 +1175,19 @@ EXPORT_SYMBOL_GPL(kernel_file_open);
* the backing inode on the underlying filesystem, which can be
* retrieved using backing_file_real_path().
*/
-struct file *backing_file_open(const struct path *path, int flags,
+struct file *backing_file_open(const struct file *user_file, int flags,
const struct path *real_path,
const struct cred *cred)
{
+ const struct path *user_path = &user_file->f_path;
struct file *f;
int error;
- f = alloc_empty_backing_file(flags, cred);
+ f = alloc_empty_backing_file(flags, cred, user_file);
if (IS_ERR(f))
return f;
- f->f_path = *path;
+ f->f_path = *user_path;
path_get(real_path);
*backing_file_real_path(f) = *real_path;
error = do_dentry_open(f, d_inode(real_path->dentry), NULL);
diff --git a/fs/overlayfs/file.c b/fs/overlayfs/file.c
index 8be4dc050d1e..07f00cf91977 100644
--- a/fs/overlayfs/file.c
+++ b/fs/overlayfs/file.c
@@ -60,7 +60,7 @@ static struct file *ovl_open_realfile(const struct file *file,
if (!inode_owner_or_capable(real_idmap, realinode))
flags &= ~O_NOATIME;
- realfile = backing_file_open(&file->f_path, flags, realpath,
+ realfile = backing_file_open(file, flags, realpath,
current_cred());
}
revert_creds(old_cred);
@@ -527,6 +527,7 @@ static int ovl_fsync(struct file *file, loff_t start, loff_t end, int datasync)
static int ovl_mmap(struct file *file, struct vm_area_struct *vma)
{
+ struct file *user_file = vma->vm_file;
struct file *realfile = file->private_data;
const struct cred *old_cred;
int ret;
@@ -540,6 +541,11 @@ static int ovl_mmap(struct file *file, struct vm_area_struct *vma)
vma_set_file(vma, realfile);
old_cred = ovl_override_creds(file_inode(file)->i_sb);
+ ret = security_mmap_backing_file(vma, realfile, user_file);
+ if (ret) {
+ revert_creds(old_cred);
+ return ret;
+ }
ret = call_mmap(vma->vm_file, vma);
revert_creds(old_cred);
ovl_file_accessed(file);
diff --git a/include/linux/fs.h b/include/linux/fs.h
index 4cdeeaedaa40..5a897ee50f29 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -2515,11 +2515,24 @@ struct file *dentry_open(const struct path *path, int flags,
const struct cred *creds);
struct file *dentry_create(const struct path *path, int flags, umode_t mode,
const struct cred *cred);
-struct file *backing_file_open(const struct path *path, int flags,
+struct file *backing_file_open(const struct file *user_file, int flags,
const struct path *real_path,
const struct cred *cred);
struct path *backing_file_real_path(struct file *f);
+#ifdef CONFIG_SECURITY
+void *backing_file_security(const struct file *f);
+void backing_file_set_security(struct file *f, void *security);
+#else
+static inline void *backing_file_security(const struct file *f)
+{
+ return NULL;
+}
+static inline void backing_file_set_security(struct file *f, void *security)
+{
+}
+#endif /* CONFIG_SECURITY */
+
/*
* file_real_path - get the path corresponding to f_inode
*
diff --git a/include/linux/lsm_audit.h b/include/linux/lsm_audit.h
index 97a8b21eb033..c0a2839253fa 100644
--- a/include/linux/lsm_audit.h
+++ b/include/linux/lsm_audit.h
@@ -93,7 +93,7 @@ struct common_audit_data {
#endif
char *kmod_name;
struct lsm_ioctlop_audit *op;
- struct file *file;
+ const struct file *file;
struct lsm_ibpkey_audit *ibpkey;
struct lsm_ibendport_audit *ibendport;
int reason;
diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
index 2923754c13bc..567d7f16609c 100644
--- a/include/linux/lsm_hook_defs.h
+++ b/include/linux/lsm_hook_defs.h
@@ -169,6 +169,9 @@ LSM_HOOK(int, 0, kernfs_init_security, struct kernfs_node *kn_dir,
LSM_HOOK(int, 0, file_permission, struct file *file, int mask)
LSM_HOOK(int, 0, file_alloc_security, struct file *file)
LSM_HOOK(void, LSM_RET_VOID, file_free_security, struct file *file)
+LSM_HOOK(int, 0, backing_file_alloc, struct file *backing_file,
+ const struct file *user_file)
+LSM_HOOK(void, LSM_RET_VOID, backing_file_free, struct file *backing_file)
LSM_HOOK(int, 0, file_ioctl, struct file *file, unsigned int cmd,
unsigned long arg)
LSM_HOOK(int, 0, file_ioctl_compat, struct file *file, unsigned int cmd,
@@ -176,6 +179,8 @@ LSM_HOOK(int, 0, file_ioctl_compat, struct file *file, unsigned int cmd,
LSM_HOOK(int, 0, mmap_addr, unsigned long addr)
LSM_HOOK(int, 0, mmap_file, struct file *file, unsigned long reqprot,
unsigned long prot, unsigned long flags)
+LSM_HOOK(int, 0, mmap_backing_file, struct vm_area_struct *vma,
+ struct file *backing_file, struct file *user_file)
LSM_HOOK(int, 0, file_mprotect, struct vm_area_struct *vma,
unsigned long reqprot, unsigned long prot)
LSM_HOOK(int, 0, file_lock, struct file *file, unsigned int cmd)
diff --git a/include/linux/lsm_hooks.h b/include/linux/lsm_hooks.h
index dcb5e5b5eb13..e8a881bf2a1a 100644
--- a/include/linux/lsm_hooks.h
+++ b/include/linux/lsm_hooks.h
@@ -59,6 +59,7 @@ struct security_hook_list {
struct lsm_blob_sizes {
int lbs_cred;
int lbs_file;
+ unsigned int lbs_backing_file;
int lbs_inode;
int lbs_superblock;
int lbs_ipc;
diff --git a/include/linux/security.h b/include/linux/security.h
index 937840870d86..4866ffdb4e6c 100644
--- a/include/linux/security.h
+++ b/include/linux/security.h
@@ -389,11 +389,17 @@ int security_kernfs_init_security(struct kernfs_node *kn_dir,
int security_file_permission(struct file *file, int mask);
int security_file_alloc(struct file *file);
void security_file_free(struct file *file);
+int security_backing_file_alloc(struct file *backing_file,
+ const struct file *user_file);
+void security_backing_file_free(struct file *backing_file);
int security_file_ioctl(struct file *file, unsigned int cmd, unsigned long arg);
int security_file_ioctl_compat(struct file *file, unsigned int cmd,
unsigned long arg);
int security_mmap_file(struct file *file, unsigned long prot,
unsigned long flags);
+int security_mmap_backing_file(struct vm_area_struct *vma,
+ struct file *backing_file,
+ struct file *user_file);
int security_mmap_addr(unsigned long addr);
int security_file_mprotect(struct vm_area_struct *vma, unsigned long reqprot,
unsigned long prot);
@@ -984,6 +990,15 @@ static inline int security_file_alloc(struct file *file)
static inline void security_file_free(struct file *file)
{ }
+static inline int security_backing_file_alloc(struct file *backing_file,
+ const struct file *user_file)
+{
+ return 0;
+}
+
+static inline void security_backing_file_free(struct file *backing_file)
+{ }
+
static inline int security_file_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
@@ -1003,6 +1018,13 @@ static inline int security_mmap_file(struct file *file, unsigned long prot,
return 0;
}
+static inline int security_mmap_backing_file(struct vm_area_struct *vma,
+ struct file *backing_file,
+ struct file *user_file)
+{
+ return 0;
+}
+
static inline int security_mmap_addr(unsigned long addr)
{
return cap_mmap_addr(addr);
diff --git a/security/security.c b/security/security.c
index 1794860fd614..e77f5c948190 100644
--- a/security/security.c
+++ b/security/security.c
@@ -78,6 +78,7 @@ struct security_hook_heads security_hook_heads __ro_after_init;
static BLOCKING_NOTIFIER_HEAD(blocking_lsm_notifier_chain);
static struct kmem_cache *lsm_file_cache;
+struct kmem_cache *lsm_backing_file_cache;
static struct kmem_cache *lsm_inode_cache;
char *lsm_names;
@@ -200,6 +201,8 @@ static void __init lsm_set_blob_sizes(struct lsm_blob_sizes *needed)
lsm_set_blob_size(&needed->lbs_cred, &blob_sizes.lbs_cred);
lsm_set_blob_size(&needed->lbs_file, &blob_sizes.lbs_file);
+ lsm_set_blob_size(&needed->lbs_backing_file,
+ &blob_sizes.lbs_backing_file);
/*
* The inode blob gets an rcu_head in addition to
* what the modules might need.
@@ -374,6 +377,7 @@ static void __init ordered_lsm_init(void)
init_debug("cred blob size = %d\n", blob_sizes.lbs_cred);
init_debug("file blob size = %d\n", blob_sizes.lbs_file);
+ init_debug("backing_file blob size = %d\n", blob_sizes.lbs_backing_file);
init_debug("inode blob size = %d\n", blob_sizes.lbs_inode);
init_debug("ipc blob size = %d\n", blob_sizes.lbs_ipc);
init_debug("msg_msg blob size = %d\n", blob_sizes.lbs_msg_msg);
@@ -388,6 +392,11 @@ static void __init ordered_lsm_init(void)
lsm_file_cache = kmem_cache_create("lsm_file_cache",
blob_sizes.lbs_file, 0,
SLAB_PANIC, NULL);
+ if (blob_sizes.lbs_backing_file)
+ lsm_backing_file_cache = kmem_cache_create(
+ "lsm_backing_file_cache",
+ blob_sizes.lbs_backing_file,
+ 0, SLAB_PANIC, NULL);
if (blob_sizes.lbs_inode)
lsm_inode_cache = kmem_cache_create("lsm_inode_cache",
blob_sizes.lbs_inode, 0,
@@ -616,6 +625,30 @@ static int lsm_file_alloc(struct file *file)
return 0;
}
+/**
+ * lsm_backing_file_alloc - allocate a composite backing file blob
+ * @backing_file: the backing file
+ *
+ * Allocate the backing file blob for all the modules.
+ *
+ * Returns 0, or -ENOMEM if memory can't be allocated.
+ */
+static int lsm_backing_file_alloc(struct file *backing_file)
+{
+ void *blob;
+
+ if (!lsm_backing_file_cache) {
+ backing_file_set_security(backing_file, NULL);
+ return 0;
+ }
+
+ blob = kmem_cache_zalloc(lsm_backing_file_cache, GFP_KERNEL);
+ backing_file_set_security(backing_file, blob);
+ if (!blob)
+ return -ENOMEM;
+ return 0;
+}
+
/**
* lsm_inode_alloc - allocate a composite inode blob
* @inode: the inode that needs a blob
@@ -2630,6 +2663,57 @@ void security_file_free(struct file *file)
}
}
+/**
+ * security_backing_file_alloc() - Allocate and setup a backing file blob
+ * @backing_file: the backing file
+ * @user_file: the associated user visible file
+ *
+ * Allocate a backing file LSM blob and perform any necessary initialization of
+ * the LSM blob. There will be some operations where the LSM will not have
+ * access to @user_file after this point, so any important state associated
+ * with @user_file that is important to the LSM should be captured in the
+ * backing file's LSM blob.
+ *
+ * LSM's should avoid taking a reference to @user_file in this hook as it will
+ * result in problems later when the system attempts to drop/put the file
+ * references due to a circular dependency.
+ *
+ * Return: Return 0 if the hook is successful, negative values otherwise.
+ */
+int security_backing_file_alloc(struct file *backing_file,
+ const struct file *user_file)
+{
+ int rc;
+
+ rc = lsm_backing_file_alloc(backing_file);
+ if (rc)
+ return rc;
+ rc = call_int_hook(backing_file_alloc, 0, backing_file, user_file);
+ if (unlikely(rc))
+ security_backing_file_free(backing_file);
+
+ return rc;
+}
+
+/**
+ * security_backing_file_free() - Free a backing file blob
+ * @backing_file: the backing file
+ *
+ * Free any LSM state associate with a backing file's LSM blob, including the
+ * blob itself.
+ */
+void security_backing_file_free(struct file *backing_file)
+{
+ void *blob = backing_file_security(backing_file);
+
+ call_void_hook(backing_file_free, backing_file);
+
+ if (blob) {
+ backing_file_set_security(backing_file, NULL);
+ kmem_cache_free(lsm_backing_file_cache, blob);
+ }
+}
+
/**
* security_file_ioctl() - Check if an ioctl is allowed
* @file: associated file
@@ -2723,6 +2807,32 @@ int security_mmap_file(struct file *file, unsigned long prot,
return ima_file_mmap(file, prot, prot_adj, flags);
}
+/**
+ * security_mmap_backing_file - Check if mmap'ing a backing file is allowed
+ * @vma: the vm_area_struct for the mmap'd region
+ * @backing_file: the backing file being mmap'd
+ * @user_file: the user file being mmap'd
+ *
+ * Check permissions for a mmap operation on a stacked filesystem. This hook
+ * is called after the security_mmap_file() and is responsible for authorizing
+ * the mmap on @backing_file. It is important to note that the mmap operation
+ * on @user_file has already been authorized and the @vma->vm_file has been
+ * set to @backing_file.
+ *
+ * Return: Returns 0 if permission is granted.
+ */
+int security_mmap_backing_file(struct vm_area_struct *vma,
+ struct file *backing_file,
+ struct file *user_file)
+{
+ /* recommended by the stackable filesystem devs */
+ if (WARN_ON_ONCE(!(backing_file->f_mode & FMODE_BACKING)))
+ return -EIO;
+
+ return call_int_hook(mmap_backing_file, 0, vma, backing_file, user_file);
+}
+EXPORT_SYMBOL_GPL(security_mmap_backing_file);
+
/**
* security_mmap_addr() - Check if mmap'ing an address is allowed
* @addr: address
--
2.18.0.huawei.25
^ permalink raw reply related
* [stable/linux-6.6.y 3/3] selinux: fix overlayfs mmap() and mprotect() access checks
From: Cai Xinchen @ 2026-06-22 3:15 UTC (permalink / raw)
To: viro, brauner, jack, miklos, amir73il, paul, jmorris, serge,
stephen.smalley.work, omosnace, gregkh, bboscaccy
Cc: linux-fsdevel, linux-kernel, linux-unionfs, linux-security-module,
selinux, bpf, lujialin4
In-Reply-To: <20260622031556.2664089-1-caixinchen1@huawei.com>
From: Paul Moore <paul@paul-moore.com>
backing_file_user_path() not available
Mainline uses backing_file_user_path(file) to obtain the user-visible path
from a backing file. The 6.6.y version uses &file->f_path directly
Original commit message:
The existing SELinux security model for overlayfs is to allow access if
the current task is able to access the top level file (the "user" file)
and the mounter's credentials are sufficient to access the lower
level file (the "backing" file). Unfortunately, the current code does
not properly enforce these access controls for both mmap() and mprotect()
operations on overlayfs filesystems.
This patch makes use of the newly created security_mmap_backing_file()
LSM hook to provide the missing backing file enforcement for mmap()
operations, and leverages the backing file API and new LSM blob to
provide the necessary information to properly enforce the mprotect()
access controls.
Cc: stable@vger.kernel.org
Acked-by: Amir Goldstein <amir73il@gmail.com>
Signed-off-by: Paul Moore <paul@paul-moore.com>
Signed-off-by: Cai Xinchen <caixinchen1@huawei.com>
---
security/selinux/hooks.c | 242 ++++++++++++++++++++++--------
security/selinux/include/objsec.h | 11 ++
2 files changed, 189 insertions(+), 64 deletions(-)
diff --git a/security/selinux/hooks.c b/security/selinux/hooks.c
index 60092d0b013c..3f11c5ae8fbf 100644
--- a/security/selinux/hooks.c
+++ b/security/selinux/hooks.c
@@ -1717,49 +1717,72 @@ static inline int file_path_has_perm(const struct cred *cred,
static int bpf_fd_pass(const struct file *file, u32 sid);
#endif
-/* Check whether a task can use an open file descriptor to
- access an inode in a given way. Check access to the
- descriptor itself, and then use dentry_has_perm to
- check a particular permission to the file.
- Access to the descriptor is implicitly granted if it
- has the same SID as the process. If av is zero, then
- access to the file is not checked, e.g. for cases
- where only the descriptor is affected like seek. */
-static int file_has_perm(const struct cred *cred,
- struct file *file,
- u32 av)
+static int __file_has_perm(const struct cred *cred, const struct file *file,
+ u32 av, bool bf_user_file)
+
{
- struct file_security_struct *fsec = selinux_file(file);
- struct inode *inode = file_inode(file);
struct common_audit_data ad;
- u32 sid = cred_sid(cred);
+ struct inode *inode;
+ u32 ssid = cred_sid(cred);
+ u32 tsid_fd;
int rc;
- ad.type = LSM_AUDIT_DATA_FILE;
- ad.u.file = file;
+ if (bf_user_file) {
+ struct backing_file_security_struct *bfsec;
+ const struct path *path;
- if (sid != fsec->sid) {
- rc = avc_has_perm(sid, fsec->sid,
- SECCLASS_FD,
- FD__USE,
- &ad);
+ if (WARN_ON(!(file->f_mode & FMODE_BACKING)))
+ return -EIO;
+
+ bfsec = selinux_backing_file(file);
+ path = &file->f_path;
+ tsid_fd = bfsec->uf_sid;
+ inode = d_inode(path->dentry);
+
+ ad.type = LSM_AUDIT_DATA_PATH;
+ ad.u.path = *path;
+ } else {
+ struct file_security_struct *fsec = selinux_file(file);
+
+ tsid_fd = fsec->sid;
+ inode = file_inode(file);
+
+ ad.type = LSM_AUDIT_DATA_FILE;
+ ad.u.file = file;
+ }
+
+ if (ssid != tsid_fd) {
+ rc = avc_has_perm(ssid, tsid_fd, SECCLASS_FD, FD__USE, &ad);
if (rc)
- goto out;
+ return rc;
}
#ifdef CONFIG_BPF_SYSCALL
- rc = bpf_fd_pass(file, cred_sid(cred));
+ /* regardless of backing vs user file, use the underlying file here */
+ rc = bpf_fd_pass(file, ssid);
if (rc)
return rc;
#endif
/* av is zero if only checking access to the descriptor. */
- rc = 0;
if (av)
- rc = inode_has_perm(cred, inode, av, &ad);
+ return inode_has_perm(cred, inode, av, &ad);
-out:
- return rc;
+ return 0;
+}
+
+/* Check whether a task can use an open file descriptor to
+ access an inode in a given way. Check access to the
+ descriptor itself, and then use dentry_has_perm to
+ check a particular permission to the file.
+ Access to the descriptor is implicitly granted if it
+ has the same SID as the process. If av is zero, then
+ access to the file is not checked, e.g. for cases
+ where only the descriptor is affected like seek. */
+static inline int file_has_perm(const struct cred *cred,
+ const struct file *file, u32 av)
+{
+ return __file_has_perm(cred, file, av, false);
}
/*
@@ -3638,6 +3661,17 @@ static int selinux_file_alloc_security(struct file *file)
return 0;
}
+static int selinux_backing_file_alloc(struct file *backing_file,
+ const struct file *user_file)
+{
+ struct backing_file_security_struct *bfsec;
+
+ bfsec = selinux_backing_file(backing_file);
+ bfsec->uf_sid = selinux_file(user_file)->sid;
+
+ return 0;
+}
+
/*
* Check whether a task has the ioctl permission and cmd
* operation to an inode.
@@ -3755,42 +3789,55 @@ static int selinux_file_ioctl_compat(struct file *file, unsigned int cmd,
static int default_noexec __ro_after_init;
-static int file_map_prot_check(struct file *file, unsigned long prot, int shared)
+static int __file_map_prot_check(const struct cred *cred,
+ const struct file *file, unsigned long prot,
+ bool shared, bool bf_user_file)
{
- const struct cred *cred = current_cred();
- u32 sid = cred_sid(cred);
- int rc = 0;
+ struct inode *inode = NULL;
+ bool prot_exec = prot & PROT_EXEC;
+ bool prot_write = prot & PROT_WRITE;
+
+ if (file) {
+ if (bf_user_file)
+ inode = d_inode(file->f_path.dentry);
+ else
+ inode = file_inode(file);
+ }
+
+ if (default_noexec && prot_exec &&
+ (!file || IS_PRIVATE(inode) || (!shared && prot_write))) {
+ int rc;
+ u32 sid = cred_sid(cred);
- if (default_noexec &&
- (prot & PROT_EXEC) && (!file || IS_PRIVATE(file_inode(file)) ||
- (!shared && (prot & PROT_WRITE)))) {
/*
- * We are making executable an anonymous mapping or a
- * private file mapping that will also be writable.
- * This has an additional check.
+ * We are making executable an anonymous mapping or a private
+ * file mapping that will also be writable.
*/
- rc = avc_has_perm(sid, sid, SECCLASS_PROCESS,
- PROCESS__EXECMEM, NULL);
+ rc = avc_has_perm(sid, sid, SECCLASS_PROCESS, PROCESS__EXECMEM,
+ NULL);
if (rc)
- goto error;
+ return rc;
}
if (file) {
- /* read access is always possible with a mapping */
+ /* "read" always possible, "write" only if shared */
u32 av = FILE__READ;
-
- /* write access only matters if the mapping is shared */
- if (shared && (prot & PROT_WRITE))
+ if (shared && prot_write)
av |= FILE__WRITE;
-
- if (prot & PROT_EXEC)
+ if (prot_exec)
av |= FILE__EXECUTE;
- return file_has_perm(cred, file, av);
+ return __file_has_perm(cred, file, av, bf_user_file);
}
-error:
- return rc;
+ return 0;
+}
+
+static inline int file_map_prot_check(const struct cred *cred,
+ const struct file *file,
+ unsigned long prot, bool shared)
+{
+ return __file_map_prot_check(cred, file, prot, shared, false);
}
static int selinux_mmap_addr(unsigned long addr)
@@ -3806,36 +3853,80 @@ static int selinux_mmap_addr(unsigned long addr)
return rc;
}
-static int selinux_mmap_file(struct file *file,
- unsigned long reqprot __always_unused,
- unsigned long prot, unsigned long flags)
+static int selinux_mmap_file_common(const struct cred *cred, struct file *file,
+ unsigned long prot, bool shared)
{
- struct common_audit_data ad;
- int rc;
-
if (file) {
+ int rc;
+ struct common_audit_data ad;
+
ad.type = LSM_AUDIT_DATA_FILE;
ad.u.file = file;
- rc = inode_has_perm(current_cred(), file_inode(file),
- FILE__MAP, &ad);
+ rc = inode_has_perm(cred, file_inode(file), FILE__MAP, &ad);
if (rc)
return rc;
}
- return file_map_prot_check(file, prot,
- (flags & MAP_TYPE) == MAP_SHARED);
+ return file_map_prot_check(cred, file, prot, shared);
+}
+
+static int selinux_mmap_file(struct file *file,
+ unsigned long reqprot __always_unused,
+ unsigned long prot, unsigned long flags)
+{
+ return selinux_mmap_file_common(current_cred(), file, prot,
+ (flags & MAP_TYPE) == MAP_SHARED);
+}
+
+/**
+ * selinux_mmap_backing_file - Check mmap permissions on a backing file
+ * @vma: memory region
+ * @backing_file: stacked filesystem backing file
+ * @user_file: user visible file
+ *
+ * This is called after selinux_mmap_file() on stacked filesystems, and it
+ * is this function's responsibility to verify access to @backing_file and
+ * setup the SELinux state for possible later use in the mprotect() code path.
+ *
+ * By the time this function is called, mmap() access to @user_file has already
+ * been authorized and @vma->vm_file has been set to point to @backing_file.
+ *
+ * Return zero on success, negative values otherwise.
+ */
+static int selinux_mmap_backing_file(struct vm_area_struct *vma,
+ struct file *backing_file,
+ struct file *user_file __always_unused)
+{
+ unsigned long prot = 0;
+
+ /* translate vma->vm_flags perms into PROT perms */
+ if (vma->vm_flags & VM_READ)
+ prot |= PROT_READ;
+ if (vma->vm_flags & VM_WRITE)
+ prot |= PROT_WRITE;
+ if (vma->vm_flags & VM_EXEC)
+ prot |= PROT_EXEC;
+
+ return selinux_mmap_file_common(backing_file->f_cred, backing_file,
+ prot, vma->vm_flags & VM_SHARED);
}
static int selinux_file_mprotect(struct vm_area_struct *vma,
unsigned long reqprot __always_unused,
unsigned long prot)
{
+ int rc;
const struct cred *cred = current_cred();
u32 sid = cred_sid(cred);
+ const struct file *file = vma->vm_file;
+ bool backing_file;
+ bool shared = vma->vm_flags & VM_SHARED;
+
+ /* check if we need to trigger the "backing files are awful" mode */
+ backing_file = file && (file->f_mode & FMODE_BACKING);
if (default_noexec &&
(prot & PROT_EXEC) && !(vma->vm_flags & VM_EXEC)) {
- int rc = 0;
/*
* We don't use the vma_is_initial_heap() helper as it has
* a history of problems and is currently broken on systems
@@ -3849,11 +3940,15 @@ static int selinux_file_mprotect(struct vm_area_struct *vma,
vma->vm_end <= vma->vm_mm->brk) {
rc = avc_has_perm(sid, sid, SECCLASS_PROCESS,
PROCESS__EXECHEAP, NULL);
- } else if (!vma->vm_file && (vma_is_initial_stack(vma) ||
+ if (rc)
+ return rc;
+ } else if (!file && (vma_is_initial_stack(vma) ||
vma_is_stack_for_current(vma))) {
rc = avc_has_perm(sid, sid, SECCLASS_PROCESS,
PROCESS__EXECSTACK, NULL);
- } else if (vma->vm_file && vma->anon_vma) {
+ if (rc)
+ return rc;
+ } else if (file && vma->anon_vma) {
/*
* We are making executable a file mapping that has
* had some COW done. Since pages might have been
@@ -3861,13 +3956,29 @@ static int selinux_file_mprotect(struct vm_area_struct *vma,
* modified content. This typically should only
* occur for text relocations.
*/
- rc = file_has_perm(cred, vma->vm_file, FILE__EXECMOD);
+ rc = __file_has_perm(cred, file, FILE__EXECMOD,
+ backing_file);
+ if (rc)
+ return rc;
+ if (backing_file) {
+ rc = file_has_perm(file->f_cred, file,
+ FILE__EXECMOD);
+ if (rc)
+ return rc;
+ }
}
+ }
+
+ rc = __file_map_prot_check(cred, file, prot, shared, backing_file);
+ if (rc)
+ return rc;
+ if (backing_file) {
+ rc = file_map_prot_check(file->f_cred, file, prot, shared);
if (rc)
return rc;
}
- return file_map_prot_check(vma->vm_file, prot, vma->vm_flags&VM_SHARED);
+ return 0;
}
static int selinux_file_lock(struct file *file, unsigned int cmd)
@@ -6870,6 +6981,7 @@ static void selinux_bpf_prog_free(struct bpf_prog_aux *aux)
struct lsm_blob_sizes selinux_blob_sizes __ro_after_init = {
.lbs_cred = sizeof(struct task_security_struct),
.lbs_file = sizeof(struct file_security_struct),
+ .lbs_backing_file = sizeof(struct backing_file_security_struct),
.lbs_inode = sizeof(struct inode_security_struct),
.lbs_ipc = sizeof(struct ipc_security_struct),
.lbs_msg_msg = sizeof(struct msg_security_struct),
@@ -7074,9 +7186,11 @@ static struct security_hook_list selinux_hooks[] __ro_after_init = {
LSM_HOOK_INIT(file_permission, selinux_file_permission),
LSM_HOOK_INIT(file_alloc_security, selinux_file_alloc_security),
+ LSM_HOOK_INIT(backing_file_alloc, selinux_backing_file_alloc),
LSM_HOOK_INIT(file_ioctl, selinux_file_ioctl),
LSM_HOOK_INIT(file_ioctl_compat, selinux_file_ioctl_compat),
LSM_HOOK_INIT(mmap_file, selinux_mmap_file),
+ LSM_HOOK_INIT(mmap_backing_file, selinux_mmap_backing_file),
LSM_HOOK_INIT(mmap_addr, selinux_mmap_addr),
LSM_HOOK_INIT(file_mprotect, selinux_file_mprotect),
LSM_HOOK_INIT(file_lock, selinux_file_lock),
diff --git a/security/selinux/include/objsec.h b/security/selinux/include/objsec.h
index 8159fd53c3de..541933dd295c 100644
--- a/security/selinux/include/objsec.h
+++ b/security/selinux/include/objsec.h
@@ -60,6 +60,10 @@ struct file_security_struct {
u32 pseqno; /* Policy seqno at the time of file open */
};
+struct backing_file_security_struct {
+ u32 uf_sid; /* associated user file fsec->sid */
+};
+
struct superblock_security_struct {
u32 sid; /* SID of file system superblock */
u32 def_sid; /* default SID for labeling */
@@ -158,6 +162,13 @@ static inline struct file_security_struct *selinux_file(const struct file *file)
return file->f_security + selinux_blob_sizes.lbs_file;
}
+static inline struct backing_file_security_struct *
+selinux_backing_file(const struct file *backing_file)
+{
+ void *blob = backing_file_security(backing_file);
+ return blob + selinux_blob_sizes.lbs_backing_file;
+}
+
static inline struct inode_security_struct *selinux_inode(
const struct inode *inode)
{
--
2.18.0.huawei.25
^ permalink raw reply related
* [stable/linux-6.6.y 1/3] fs: prepare for adding LSM blob to backing_file
From: Cai Xinchen @ 2026-06-22 3:15 UTC (permalink / raw)
To: viro, brauner, jack, miklos, amir73il, paul, jmorris, serge,
stephen.smalley.work, omosnace, gregkh, bboscaccy
Cc: linux-fsdevel, linux-kernel, linux-unionfs, linux-security-module,
selinux, bpf, lujialin4
In-Reply-To: <20260622031556.2664089-1-caixinchen1@huawei.com>
From: Amir Goldstein <amir73il@gmail.com>
1. The commit def3ae83da02
("fs: store real path instead of fake path in backing file f_path")
is not merged, The 6.6 LTS version accordingly operates on
&ff->real_path instead of &ff->user_path.
2. Mainline's file_free() does both the backing_file cleanup and the
kmem_cache_free() synchronously. Linux 6.6.y defers the actual kfree()
to file_free_rcu() via call_rcu(), so only path_put() is done
synchronously in file_free().
original commit message:
In preparation to adding LSM blob to backing_file struct, factor out
helpers init_backing_file() and backing_file_free().
Cc: stable@vger.kernel.org
Cc: linux-fsdevel@vger.kernel.org
Cc: linux-unionfs@vger.kernel.org
Cc: linux-erofs@lists.ozlabs.org
Signed-off-by: Amir Goldstein <amir73il@gmail.com>
Reviewed-by: Serge Hallyn <serge@hallyn.com>
[PM: use the term "LSM blob", fix comment style to match file]
Signed-off-by: Paul Moore <paul@paul-moore.com>
Signed-off-by: Cai Xinchen <caixinchen1@huawei.com>
---
fs/file_table.c | 20 +++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/fs/file_table.c b/fs/file_table.c
index 234284ef72a9..b4c208a77153 100644
--- a/fs/file_table.c
+++ b/fs/file_table.c
@@ -72,11 +72,16 @@ static void file_free_rcu(struct rcu_head *head)
kmem_cache_free(filp_cachep, f);
}
+static inline void backing_file_free(struct backing_file *ff)
+{
+ path_put(&ff->real_path);
+}
+
static inline void file_free(struct file *f)
{
security_file_free(f);
if (unlikely(f->f_mode & FMODE_BACKING))
- path_put(backing_file_real_path(f));
+ backing_file_free(backing_file(f));
if (likely(!(f->f_mode & FMODE_NOACCOUNT)))
percpu_counter_dec(&nr_files);
call_rcu(&f->f_rcuhead, file_free_rcu);
@@ -252,6 +257,12 @@ struct file *alloc_empty_file_noaccount(int flags, const struct cred *cred)
return f;
}
+static int init_backing_file(struct backing_file *ff)
+{
+ memset(&ff->real_path, 0, sizeof(ff->real_path));
+ return 0;
+}
+
/*
* Variant of alloc_empty_file() that allocates a backing_file container
* and doesn't check and modify nr_files.
@@ -274,7 +285,14 @@ struct file *alloc_empty_backing_file(int flags, const struct cred *cred)
return ERR_PTR(error);
}
+ /* The f_mode flags must be set before fput(). */
ff->file.f_mode |= FMODE_BACKING | FMODE_NOACCOUNT;
+ error = init_backing_file(ff);
+ if (unlikely(error)) {
+ fput(&ff->file);
+ return ERR_PTR(error);
+ }
+
return &ff->file;
}
--
2.18.0.huawei.25
^ permalink raw reply related
* [stable/linux-6.6.y 0/3] Backport Fix incorrect overlayfs mmap() and mprotect() LSM access controls
From: Cai Xinchen @ 2026-06-22 3:15 UTC (permalink / raw)
To: viro, brauner, jack, miklos, amir73il, paul, jmorris, serge,
stephen.smalley.work, omosnace, gregkh, bboscaccy
Cc: linux-fsdevel, linux-kernel, linux-unionfs, linux-security-module,
selinux, bpf, lujialin4
Backport the patch series
"Fix incorrect overlayfs mmap() and mprotect() LSM access controls" [1]
to 6.6 lts
I test selinux-testsuite[2] overlay test, it pass 135 tests.
[1] https://lore.kernel.org/all/20260403030848.731867-5-paul@paul-moore.com/
[2] https://github.com/SELinuxProject/selinux-testsuite
Amir Goldstein (1):
fs: prepare for adding LSM blob to backing_file
Paul Moore (2):
lsm: add backing_file LSM hooks
selinux: fix overlayfs mmap() and mprotect() access checks
fs/file_table.c | 41 ++++-
fs/internal.h | 3 +-
fs/open.c | 7 +-
fs/overlayfs/file.c | 8 +-
include/linux/fs.h | 15 +-
include/linux/lsm_audit.h | 2 +-
include/linux/lsm_hook_defs.h | 5 +
include/linux/lsm_hooks.h | 1 +
include/linux/security.h | 22 +++
security/security.c | 110 ++++++++++++++
security/selinux/hooks.c | 242 ++++++++++++++++++++++--------
security/selinux/include/objsec.h | 11 ++
12 files changed, 394 insertions(+), 73 deletions(-)
--
2.18.0.huawei.25
^ permalink raw reply
* [stable/linux-6.12.y 2/2] selinux: fix overlayfs mmap() and mprotect() access checks
From: Cai Xinchen @ 2026-06-22 3:15 UTC (permalink / raw)
To: viro, brauner, jack, miklos, amir73il, paul, jmorris, serge,
stephen.smalley.work, omosnace, gregkh, bboscaccy
Cc: linux-fsdevel, linux-kernel, linux-unionfs, linux-security-module,
selinux, bpf, lujialin4
In-Reply-To: <20260622031509.2663919-1-caixinchen1@huawei.com>
From: Paul Moore <paul@paul-moore.com>
The existing SELinux security model for overlayfs is to allow access if
the current task is able to access the top level file (the "user" file)
and the mounter's credentials are sufficient to access the lower
level file (the "backing" file). Unfortunately, the current code does
not properly enforce these access controls for both mmap() and mprotect()
operations on overlayfs filesystems.
This patch makes use of the newly created security_mmap_backing_file()
LSM hook to provide the missing backing file enforcement for mmap()
operations, and leverages the backing file API and new LSM blob to
provide the necessary information to properly enforce the mprotect()
access controls.
Cc: stable@vger.kernel.org
Acked-by: Amir Goldstein <amir73il@gmail.com>
Signed-off-by: Paul Moore <paul@paul-moore.com>
Signed-off-by: Cai Xinchen <caixinchen1@huawei.com>
---
security/selinux/hooks.c | 242 ++++++++++++++++++++++--------
security/selinux/include/objsec.h | 11 ++
2 files changed, 189 insertions(+), 64 deletions(-)
diff --git a/security/selinux/hooks.c b/security/selinux/hooks.c
index 8e31d3b60fc6..1b89c8d5fa2f 100644
--- a/security/selinux/hooks.c
+++ b/security/selinux/hooks.c
@@ -1724,49 +1724,72 @@ static inline int file_path_has_perm(const struct cred *cred,
static int bpf_fd_pass(const struct file *file, u32 sid);
#endif
-/* Check whether a task can use an open file descriptor to
- access an inode in a given way. Check access to the
- descriptor itself, and then use dentry_has_perm to
- check a particular permission to the file.
- Access to the descriptor is implicitly granted if it
- has the same SID as the process. If av is zero, then
- access to the file is not checked, e.g. for cases
- where only the descriptor is affected like seek. */
-static int file_has_perm(const struct cred *cred,
- struct file *file,
- u32 av)
+static int __file_has_perm(const struct cred *cred, const struct file *file,
+ u32 av, bool bf_user_file)
+
{
- struct file_security_struct *fsec = selinux_file(file);
- struct inode *inode = file_inode(file);
struct common_audit_data ad;
- u32 sid = cred_sid(cred);
+ struct inode *inode;
+ u32 ssid = cred_sid(cred);
+ u32 tsid_fd;
int rc;
- ad.type = LSM_AUDIT_DATA_FILE;
- ad.u.file = file;
+ if (bf_user_file) {
+ struct backing_file_security_struct *bfsec;
+ const struct path *path;
- if (sid != fsec->sid) {
- rc = avc_has_perm(sid, fsec->sid,
- SECCLASS_FD,
- FD__USE,
- &ad);
+ if (WARN_ON(!(file->f_mode & FMODE_BACKING)))
+ return -EIO;
+
+ bfsec = selinux_backing_file(file);
+ path = backing_file_user_path(file);
+ tsid_fd = bfsec->uf_sid;
+ inode = d_inode(path->dentry);
+
+ ad.type = LSM_AUDIT_DATA_PATH;
+ ad.u.path = *path;
+ } else {
+ struct file_security_struct *fsec = selinux_file(file);
+
+ tsid_fd = fsec->sid;
+ inode = file_inode(file);
+
+ ad.type = LSM_AUDIT_DATA_FILE;
+ ad.u.file = file;
+ }
+
+ if (ssid != tsid_fd) {
+ rc = avc_has_perm(ssid, tsid_fd, SECCLASS_FD, FD__USE, &ad);
if (rc)
- goto out;
+ return rc;
}
#ifdef CONFIG_BPF_SYSCALL
- rc = bpf_fd_pass(file, cred_sid(cred));
+ /* regardless of backing vs user file, use the underlying file here */
+ rc = bpf_fd_pass(file, ssid);
if (rc)
return rc;
#endif
/* av is zero if only checking access to the descriptor. */
- rc = 0;
if (av)
- rc = inode_has_perm(cred, inode, av, &ad);
+ return inode_has_perm(cred, inode, av, &ad);
-out:
- return rc;
+ return 0;
+}
+
+/* Check whether a task can use an open file descriptor to
+ access an inode in a given way. Check access to the
+ descriptor itself, and then use dentry_has_perm to
+ check a particular permission to the file.
+ Access to the descriptor is implicitly granted if it
+ has the same SID as the process. If av is zero, then
+ access to the file is not checked, e.g. for cases
+ where only the descriptor is affected like seek. */
+static inline int file_has_perm(const struct cred *cred,
+ const struct file *file, u32 av)
+{
+ return __file_has_perm(cred, file, av, false);
}
/*
@@ -3653,6 +3676,17 @@ static int selinux_file_alloc_security(struct file *file)
return 0;
}
+static int selinux_backing_file_alloc(struct file *backing_file,
+ const struct file *user_file)
+{
+ struct backing_file_security_struct *bfsec;
+
+ bfsec = selinux_backing_file(backing_file);
+ bfsec->uf_sid = selinux_file(user_file)->sid;
+
+ return 0;
+}
+
/*
* Check whether a task has the ioctl permission and cmd
* operation to an inode.
@@ -3770,42 +3804,55 @@ static int selinux_file_ioctl_compat(struct file *file, unsigned int cmd,
static int default_noexec __ro_after_init;
-static int file_map_prot_check(struct file *file, unsigned long prot, int shared)
+static int __file_map_prot_check(const struct cred *cred,
+ const struct file *file, unsigned long prot,
+ bool shared, bool bf_user_file)
{
- const struct cred *cred = current_cred();
- u32 sid = cred_sid(cred);
- int rc = 0;
+ struct inode *inode = NULL;
+ bool prot_exec = prot & PROT_EXEC;
+ bool prot_write = prot & PROT_WRITE;
+
+ if (file) {
+ if (bf_user_file)
+ inode = d_inode(backing_file_user_path(file)->dentry);
+ else
+ inode = file_inode(file);
+ }
+
+ if (default_noexec && prot_exec &&
+ (!file || IS_PRIVATE(inode) || (!shared && prot_write))) {
+ int rc;
+ u32 sid = cred_sid(cred);
- if (default_noexec &&
- (prot & PROT_EXEC) && (!file || IS_PRIVATE(file_inode(file)) ||
- (!shared && (prot & PROT_WRITE)))) {
/*
- * We are making executable an anonymous mapping or a
- * private file mapping that will also be writable.
- * This has an additional check.
+ * We are making executable an anonymous mapping or a private
+ * file mapping that will also be writable.
*/
- rc = avc_has_perm(sid, sid, SECCLASS_PROCESS,
- PROCESS__EXECMEM, NULL);
+ rc = avc_has_perm(sid, sid, SECCLASS_PROCESS, PROCESS__EXECMEM,
+ NULL);
if (rc)
- goto error;
+ return rc;
}
if (file) {
- /* read access is always possible with a mapping */
+ /* "read" always possible, "write" only if shared */
u32 av = FILE__READ;
-
- /* write access only matters if the mapping is shared */
- if (shared && (prot & PROT_WRITE))
+ if (shared && prot_write)
av |= FILE__WRITE;
-
- if (prot & PROT_EXEC)
+ if (prot_exec)
av |= FILE__EXECUTE;
- return file_has_perm(cred, file, av);
+ return __file_has_perm(cred, file, av, bf_user_file);
}
-error:
- return rc;
+ return 0;
+}
+
+static inline int file_map_prot_check(const struct cred *cred,
+ const struct file *file,
+ unsigned long prot, bool shared)
+{
+ return __file_map_prot_check(cred, file, prot, shared, false);
}
static int selinux_mmap_addr(unsigned long addr)
@@ -3821,36 +3868,80 @@ static int selinux_mmap_addr(unsigned long addr)
return rc;
}
-static int selinux_mmap_file(struct file *file,
- unsigned long reqprot __always_unused,
- unsigned long prot, unsigned long flags)
+static int selinux_mmap_file_common(const struct cred *cred, struct file *file,
+ unsigned long prot, bool shared)
{
- struct common_audit_data ad;
- int rc;
-
if (file) {
+ int rc;
+ struct common_audit_data ad;
+
ad.type = LSM_AUDIT_DATA_FILE;
ad.u.file = file;
- rc = inode_has_perm(current_cred(), file_inode(file),
- FILE__MAP, &ad);
+ rc = inode_has_perm(cred, file_inode(file), FILE__MAP, &ad);
if (rc)
return rc;
}
- return file_map_prot_check(file, prot,
- (flags & MAP_TYPE) == MAP_SHARED);
+ return file_map_prot_check(cred, file, prot, shared);
+}
+
+static int selinux_mmap_file(struct file *file,
+ unsigned long reqprot __always_unused,
+ unsigned long prot, unsigned long flags)
+{
+ return selinux_mmap_file_common(current_cred(), file, prot,
+ (flags & MAP_TYPE) == MAP_SHARED);
+}
+
+/**
+ * selinux_mmap_backing_file - Check mmap permissions on a backing file
+ * @vma: memory region
+ * @backing_file: stacked filesystem backing file
+ * @user_file: user visible file
+ *
+ * This is called after selinux_mmap_file() on stacked filesystems, and it
+ * is this function's responsibility to verify access to @backing_file and
+ * setup the SELinux state for possible later use in the mprotect() code path.
+ *
+ * By the time this function is called, mmap() access to @user_file has already
+ * been authorized and @vma->vm_file has been set to point to @backing_file.
+ *
+ * Return zero on success, negative values otherwise.
+ */
+static int selinux_mmap_backing_file(struct vm_area_struct *vma,
+ struct file *backing_file,
+ struct file *user_file __always_unused)
+{
+ unsigned long prot = 0;
+
+ /* translate vma->vm_flags perms into PROT perms */
+ if (vma->vm_flags & VM_READ)
+ prot |= PROT_READ;
+ if (vma->vm_flags & VM_WRITE)
+ prot |= PROT_WRITE;
+ if (vma->vm_flags & VM_EXEC)
+ prot |= PROT_EXEC;
+
+ return selinux_mmap_file_common(backing_file->f_cred, backing_file,
+ prot, vma->vm_flags & VM_SHARED);
}
static int selinux_file_mprotect(struct vm_area_struct *vma,
unsigned long reqprot __always_unused,
unsigned long prot)
{
+ int rc;
const struct cred *cred = current_cred();
u32 sid = cred_sid(cred);
+ const struct file *file = vma->vm_file;
+ bool backing_file;
+ bool shared = vma->vm_flags & VM_SHARED;
+
+ /* check if we need to trigger the "backing files are awful" mode */
+ backing_file = file && (file->f_mode & FMODE_BACKING);
if (default_noexec &&
(prot & PROT_EXEC) && !(vma->vm_flags & VM_EXEC)) {
- int rc = 0;
/*
* We don't use the vma_is_initial_heap() helper as it has
* a history of problems and is currently broken on systems
@@ -3864,11 +3955,15 @@ static int selinux_file_mprotect(struct vm_area_struct *vma,
vma->vm_end <= vma->vm_mm->brk) {
rc = avc_has_perm(sid, sid, SECCLASS_PROCESS,
PROCESS__EXECHEAP, NULL);
- } else if (!vma->vm_file && (vma_is_initial_stack(vma) ||
+ if (rc)
+ return rc;
+ } else if (!file && (vma_is_initial_stack(vma) ||
vma_is_stack_for_current(vma))) {
rc = avc_has_perm(sid, sid, SECCLASS_PROCESS,
PROCESS__EXECSTACK, NULL);
- } else if (vma->vm_file && vma->anon_vma) {
+ if (rc)
+ return rc;
+ } else if (file && vma->anon_vma) {
/*
* We are making executable a file mapping that has
* had some COW done. Since pages might have been
@@ -3876,13 +3971,29 @@ static int selinux_file_mprotect(struct vm_area_struct *vma,
* modified content. This typically should only
* occur for text relocations.
*/
- rc = file_has_perm(cred, vma->vm_file, FILE__EXECMOD);
+ rc = __file_has_perm(cred, file, FILE__EXECMOD,
+ backing_file);
+ if (rc)
+ return rc;
+ if (backing_file) {
+ rc = file_has_perm(file->f_cred, file,
+ FILE__EXECMOD);
+ if (rc)
+ return rc;
+ }
}
+ }
+
+ rc = __file_map_prot_check(cred, file, prot, shared, backing_file);
+ if (rc)
+ return rc;
+ if (backing_file) {
+ rc = file_map_prot_check(file->f_cred, file, prot, shared);
if (rc)
return rc;
}
- return file_map_prot_check(vma->vm_file, prot, vma->vm_flags&VM_SHARED);
+ return 0;
}
static int selinux_file_lock(struct file *file, unsigned int cmd)
@@ -6960,6 +7071,7 @@ static void selinux_bpf_token_free(struct bpf_token *token)
struct lsm_blob_sizes selinux_blob_sizes __ro_after_init = {
.lbs_cred = sizeof(struct task_security_struct),
.lbs_file = sizeof(struct file_security_struct),
+ .lbs_backing_file = sizeof(struct backing_file_security_struct),
.lbs_inode = sizeof(struct inode_security_struct),
.lbs_ipc = sizeof(struct ipc_security_struct),
.lbs_key = sizeof(struct key_security_struct),
@@ -7165,9 +7277,11 @@ static struct security_hook_list selinux_hooks[] __ro_after_init = {
LSM_HOOK_INIT(file_permission, selinux_file_permission),
LSM_HOOK_INIT(file_alloc_security, selinux_file_alloc_security),
+ LSM_HOOK_INIT(backing_file_alloc, selinux_backing_file_alloc),
LSM_HOOK_INIT(file_ioctl, selinux_file_ioctl),
LSM_HOOK_INIT(file_ioctl_compat, selinux_file_ioctl_compat),
LSM_HOOK_INIT(mmap_file, selinux_mmap_file),
+ LSM_HOOK_INIT(mmap_backing_file, selinux_mmap_backing_file),
LSM_HOOK_INIT(mmap_addr, selinux_mmap_addr),
LSM_HOOK_INIT(file_mprotect, selinux_file_mprotect),
LSM_HOOK_INIT(file_lock, selinux_file_lock),
diff --git a/security/selinux/include/objsec.h b/security/selinux/include/objsec.h
index c88cae81ee4c..dc42282a2c05 100644
--- a/security/selinux/include/objsec.h
+++ b/security/selinux/include/objsec.h
@@ -61,6 +61,10 @@ struct file_security_struct {
u32 pseqno; /* Policy seqno at the time of file open */
};
+struct backing_file_security_struct {
+ u32 uf_sid; /* associated user file fsec->sid */
+};
+
struct superblock_security_struct {
u32 sid; /* SID of file system superblock */
u32 def_sid; /* default SID for labeling */
@@ -159,6 +163,13 @@ static inline struct file_security_struct *selinux_file(const struct file *file)
return file->f_security + selinux_blob_sizes.lbs_file;
}
+static inline struct backing_file_security_struct *
+selinux_backing_file(const struct file *backing_file)
+{
+ void *blob = backing_file_security(backing_file);
+ return blob + selinux_blob_sizes.lbs_backing_file;
+}
+
static inline struct inode_security_struct *
selinux_inode(const struct inode *inode)
{
--
2.18.0.huawei.25
^ permalink raw reply related
* [stable/linux-6.12.y 1/2] lsm: add backing_file LSM hooks
From: Cai Xinchen @ 2026-06-22 3:15 UTC (permalink / raw)
To: viro, brauner, jack, miklos, amir73il, paul, jmorris, serge,
stephen.smalley.work, omosnace, gregkh, bboscaccy
Cc: linux-fsdevel, linux-kernel, linux-unionfs, linux-security-module,
selinux, bpf, lujialin4
In-Reply-To: <20260622031509.2663919-1-caixinchen1@huawei.com>
From: Paul Moore <paul@paul-moore.com>
Mainline declares lsm_backing_file_cache in security/lsm.h. Linux 6.12.y
does not have security/lsm_init.c or security/lsm.h; the cache variable
is defined locally as static struct kmem_cache *lsm_backing_file_cache in
security/security.c.
Original commit message:
Stacked filesystems such as overlayfs do not currently provide the
necessary mechanisms for LSMs to properly enforce access controls on the
mmap() and mprotect() operations. In order to resolve this gap, a LSM
security blob is being added to the backing_file struct and the following
new LSM hooks are being created:
security_backing_file_alloc()
security_backing_file_free()
security_mmap_backing_file()
The first two hooks are to manage the lifecycle of the LSM security blob
in the backing_file struct, while the third provides a new mmap() access
control point for the underlying backing file. It is also expected that
LSMs will likely want to update their security_file_mprotect() callback
to address issues with their mprotect() controls, but that does not
require a change to the security_file_mprotect() LSM hook.
There are a three other small changes to support these new LSM hooks:
* Pass the user file associated with a backing file down to
alloc_empty_backing_file() so it can be included in the
security_backing_file_alloc() hook.
* Add getter and setter functions for the backing_file struct LSM blob
as the backing_file struct remains private to fs/file_table.c.
* Constify the file struct field in the LSM common_audit_data struct to
better support LSMs that need to pass a const file struct pointer into
the common LSM audit code.
Thanks to Arnd Bergmann for identifying the missing EXPORT_SYMBOL_GPL()
and supplying a fixup.
Cc: stable@vger.kernel.org
Cc: linux-fsdevel@vger.kernel.org
Cc: linux-unionfs@vger.kernel.org
Cc: linux-erofs@lists.ozlabs.org
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
Reviewed-by: Serge Hallyn <serge@hallyn.com>
Reviewed-by: Christian Brauner <brauner@kernel.org>
Signed-off-by: Paul Moore <paul@paul-moore.com>
Signed-off-by: Cai Xinchen <caixinchen1@huawei.com>
---
fs/backing-file.c | 18 ++++--
fs/file_table.c | 27 +++++++--
fs/fuse/passthrough.c | 2 +-
fs/internal.h | 3 +-
fs/overlayfs/dir.c | 2 +-
fs/overlayfs/file.c | 3 +-
include/linux/backing-file.h | 4 +-
include/linux/fs.h | 13 ++++
include/linux/lsm_audit.h | 2 +-
include/linux/lsm_hook_defs.h | 5 ++
include/linux/lsm_hooks.h | 1 +
include/linux/security.h | 22 +++++++
security/security.c | 109 ++++++++++++++++++++++++++++++++++
13 files changed, 195 insertions(+), 16 deletions(-)
diff --git a/fs/backing-file.c b/fs/backing-file.c
index 09a9be945d45..389cfe3ed509 100644
--- a/fs/backing-file.c
+++ b/fs/backing-file.c
@@ -12,6 +12,7 @@
#include <linux/backing-file.h>
#include <linux/splice.h>
#include <linux/mm.h>
+#include <linux/security.h>
#include "internal.h"
@@ -29,14 +30,15 @@
* returned file into a container structure that also stores the stacked
* file's path, which can be retrieved using backing_file_user_path().
*/
-struct file *backing_file_open(const struct path *user_path, int flags,
+struct file *backing_file_open(const struct file *user_file, int flags,
const struct path *real_path,
const struct cred *cred)
{
+ const struct path *user_path = &user_file->f_path;
struct file *f;
int error;
- f = alloc_empty_backing_file(flags, cred);
+ f = alloc_empty_backing_file(flags, cred, user_file);
if (IS_ERR(f))
return f;
@@ -52,15 +54,16 @@ struct file *backing_file_open(const struct path *user_path, int flags,
}
EXPORT_SYMBOL_GPL(backing_file_open);
-struct file *backing_tmpfile_open(const struct path *user_path, int flags,
+struct file *backing_tmpfile_open(const struct file *user_file, int flags,
const struct path *real_parentpath,
umode_t mode, const struct cred *cred)
{
struct mnt_idmap *real_idmap = mnt_idmap(real_parentpath->mnt);
+ const struct path *user_path = &user_file->f_path;
struct file *f;
int error;
- f = alloc_empty_backing_file(flags, cred);
+ f = alloc_empty_backing_file(flags, cred, user_file);
if (IS_ERR(f))
return f;
@@ -326,6 +329,7 @@ EXPORT_SYMBOL_GPL(backing_file_splice_write);
int backing_file_mmap(struct file *file, struct vm_area_struct *vma,
struct backing_file_ctx *ctx)
{
+ struct file *user_file = vma->vm_file;
const struct cred *old_cred;
int ret;
@@ -339,6 +343,12 @@ int backing_file_mmap(struct file *file, struct vm_area_struct *vma,
vma_set_file(vma, file);
old_cred = override_creds(ctx->cred);
+ ret = security_mmap_backing_file(vma, file, user_file);
+ if (ret) {
+ revert_creds(old_cred);
+ return ret;
+ }
+
ret = call_mmap(vma->vm_file, vma);
revert_creds(old_cred);
diff --git a/fs/file_table.c b/fs/file_table.c
index 2a08bc93b0b9..a771be30eb43 100644
--- a/fs/file_table.c
+++ b/fs/file_table.c
@@ -46,6 +46,9 @@ static struct percpu_counter nr_files __cacheline_aligned_in_smp;
struct backing_file {
struct file file;
struct path user_path;
+#ifdef CONFIG_SECURITY
+ void *security;
+#endif
};
static inline struct backing_file *backing_file(struct file *f)
@@ -59,8 +62,21 @@ struct path *backing_file_user_path(struct file *f)
}
EXPORT_SYMBOL_GPL(backing_file_user_path);
+#ifdef CONFIG_SECURITY
+void *backing_file_security(const struct file *f)
+{
+ return backing_file(f)->security;
+}
+
+void backing_file_set_security(struct file *f, void *security)
+{
+ backing_file(f)->security = security;
+}
+#endif /* CONFIG_SECURITY */
+
static inline void backing_file_free(struct backing_file *ff)
{
+ security_backing_file_free(&ff->file);
path_put(&ff->user_path);
kfree(ff);
}
@@ -259,10 +275,12 @@ struct file *alloc_empty_file_noaccount(int flags, const struct cred *cred)
return f;
}
-static int init_backing_file(struct backing_file *ff)
+static int init_backing_file(struct backing_file *ff,
+ const struct file *user_file)
{
memset(&ff->user_path, 0, sizeof(ff->user_path));
- return 0;
+ backing_file_set_security(&ff->file, NULL);
+ return security_backing_file_alloc(&ff->file, user_file);
}
/*
@@ -272,7 +290,8 @@ static int init_backing_file(struct backing_file *ff)
* This is only for kernel internal use, and the allocate file must not be
* installed into file tables or such.
*/
-struct file *alloc_empty_backing_file(int flags, const struct cred *cred)
+struct file *alloc_empty_backing_file(int flags, const struct cred *cred,
+ const struct file *user_file)
{
struct backing_file *ff;
int error;
@@ -289,7 +308,7 @@ struct file *alloc_empty_backing_file(int flags, const struct cred *cred)
/* The f_mode flags must be set before fput(). */
ff->file.f_mode |= FMODE_BACKING | FMODE_NOACCOUNT;
- error = init_backing_file(ff);
+ error = init_backing_file(ff, user_file);
if (unlikely(error)) {
fput(&ff->file);
return ERR_PTR(error);
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index 6bfd09dda9e3..140e150be0de 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -326,7 +326,7 @@ struct fuse_backing *fuse_passthrough_open(struct file *file,
goto out;
/* Allocate backing file per fuse file to store fuse path */
- backing_file = backing_file_open(&file->f_path, file->f_flags,
+ backing_file = backing_file_open(file, file->f_flags,
&fb->file->f_path, fb->cred);
err = PTR_ERR(backing_file);
if (IS_ERR(backing_file)) {
diff --git a/fs/internal.h b/fs/internal.h
index 8c1b7acbbe8f..89a15e0f2f1b 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -99,7 +99,8 @@ extern void chroot_fs_refs(const struct path *, const struct path *);
*/
struct file *alloc_empty_file(int flags, const struct cred *cred);
struct file *alloc_empty_file_noaccount(int flags, const struct cred *cred);
-struct file *alloc_empty_backing_file(int flags, const struct cred *cred);
+struct file *alloc_empty_backing_file(int flags, const struct cred *cred,
+ const struct file *user_file);
static inline void file_put_write_access(struct file *file)
{
diff --git a/fs/overlayfs/dir.c b/fs/overlayfs/dir.c
index ab65e98a1def..1c8009bf194b 100644
--- a/fs/overlayfs/dir.c
+++ b/fs/overlayfs/dir.c
@@ -1320,7 +1320,7 @@ static int ovl_create_tmpfile(struct file *file, struct dentry *dentry,
goto out_revert_creds;
ovl_path_upper(dentry->d_parent, &realparentpath);
- realfile = backing_tmpfile_open(&file->f_path, flags, &realparentpath,
+ realfile = backing_tmpfile_open(file, flags, &realparentpath,
mode, current_cred());
err = PTR_ERR_OR_ZERO(realfile);
pr_debug("tmpfile/open(%pd2, 0%o) = %i\n", realparentpath.dentry, mode, err);
diff --git a/fs/overlayfs/file.c b/fs/overlayfs/file.c
index 94095058da34..3765e1defa19 100644
--- a/fs/overlayfs/file.c
+++ b/fs/overlayfs/file.c
@@ -47,8 +47,7 @@ static struct file *ovl_open_realfile(const struct file *file,
} else {
if (!inode_owner_or_capable(real_idmap, realinode))
flags &= ~O_NOATIME;
-
- realfile = backing_file_open(file_user_path((struct file *) file),
+ realfile = backing_file_open(file,
flags, realpath, current_cred());
}
revert_creds(old_cred);
diff --git a/include/linux/backing-file.h b/include/linux/backing-file.h
index 2eed0ffb5e8f..cd18acd7ac5b 100644
--- a/include/linux/backing-file.h
+++ b/include/linux/backing-file.h
@@ -19,10 +19,10 @@ struct backing_file_ctx {
void (*end_write)(struct file *, loff_t, ssize_t);
};
-struct file *backing_file_open(const struct path *user_path, int flags,
+struct file *backing_file_open(const struct file *user_file, int flags,
const struct path *real_path,
const struct cred *cred);
-struct file *backing_tmpfile_open(const struct path *user_path, int flags,
+struct file *backing_tmpfile_open(const struct file *user_file, int flags,
const struct path *real_parentpath,
umode_t mode, const struct cred *cred);
ssize_t backing_file_read_iter(struct file *file, struct iov_iter *iter,
diff --git a/include/linux/fs.h b/include/linux/fs.h
index 87720e1b5419..d06c2d829877 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -2740,6 +2740,19 @@ struct file *dentry_create(const struct path *path, int flags, umode_t mode,
const struct cred *cred);
struct path *backing_file_user_path(struct file *f);
+#ifdef CONFIG_SECURITY
+void *backing_file_security(const struct file *f);
+void backing_file_set_security(struct file *f, void *security);
+#else
+static inline void *backing_file_security(const struct file *f)
+{
+ return NULL;
+}
+static inline void backing_file_set_security(struct file *f, void *security)
+{
+}
+#endif /* CONFIG_SECURITY */
+
/*
* When mmapping a file on a stackable filesystem (e.g., overlayfs), the file
* stored in ->vm_file is a backing file whose f_inode is on the underlying
diff --git a/include/linux/lsm_audit.h b/include/linux/lsm_audit.h
index 97a8b21eb033..c0a2839253fa 100644
--- a/include/linux/lsm_audit.h
+++ b/include/linux/lsm_audit.h
@@ -93,7 +93,7 @@ struct common_audit_data {
#endif
char *kmod_name;
struct lsm_ioctlop_audit *op;
- struct file *file;
+ const struct file *file;
struct lsm_ibpkey_audit *ibpkey;
struct lsm_ibendport_audit *ibendport;
int reason;
diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
index 9eca013aa5e1..addb34abffa1 100644
--- a/include/linux/lsm_hook_defs.h
+++ b/include/linux/lsm_hook_defs.h
@@ -188,6 +188,9 @@ LSM_HOOK(int, 0, file_permission, struct file *file, int mask)
LSM_HOOK(int, 0, file_alloc_security, struct file *file)
LSM_HOOK(void, LSM_RET_VOID, file_release, struct file *file)
LSM_HOOK(void, LSM_RET_VOID, file_free_security, struct file *file)
+LSM_HOOK(int, 0, backing_file_alloc, struct file *backing_file,
+ const struct file *user_file)
+LSM_HOOK(void, LSM_RET_VOID, backing_file_free, struct file *backing_file)
LSM_HOOK(int, 0, file_ioctl, struct file *file, unsigned int cmd,
unsigned long arg)
LSM_HOOK(int, 0, file_ioctl_compat, struct file *file, unsigned int cmd,
@@ -195,6 +198,8 @@ LSM_HOOK(int, 0, file_ioctl_compat, struct file *file, unsigned int cmd,
LSM_HOOK(int, 0, mmap_addr, unsigned long addr)
LSM_HOOK(int, 0, mmap_file, struct file *file, unsigned long reqprot,
unsigned long prot, unsigned long flags)
+LSM_HOOK(int, 0, mmap_backing_file, struct vm_area_struct *vma,
+ struct file *backing_file, struct file *user_file)
LSM_HOOK(int, 0, file_mprotect, struct vm_area_struct *vma,
unsigned long reqprot, unsigned long prot)
LSM_HOOK(int, 0, file_lock, struct file *file, unsigned int cmd)
diff --git a/include/linux/lsm_hooks.h b/include/linux/lsm_hooks.h
index 090d1d3e19fe..12b1576d5b02 100644
--- a/include/linux/lsm_hooks.h
+++ b/include/linux/lsm_hooks.h
@@ -104,6 +104,7 @@ struct security_hook_list {
struct lsm_blob_sizes {
int lbs_cred;
int lbs_file;
+ unsigned int lbs_backing_file;
int lbs_ib;
int lbs_inode;
int lbs_sock;
diff --git a/include/linux/security.h b/include/linux/security.h
index 2c6db949ad1a..e4300f2ff11b 100644
--- a/include/linux/security.h
+++ b/include/linux/security.h
@@ -421,11 +421,17 @@ int security_file_permission(struct file *file, int mask);
int security_file_alloc(struct file *file);
void security_file_release(struct file *file);
void security_file_free(struct file *file);
+int security_backing_file_alloc(struct file *backing_file,
+ const struct file *user_file);
+void security_backing_file_free(struct file *backing_file);
int security_file_ioctl(struct file *file, unsigned int cmd, unsigned long arg);
int security_file_ioctl_compat(struct file *file, unsigned int cmd,
unsigned long arg);
int security_mmap_file(struct file *file, unsigned long prot,
unsigned long flags);
+int security_mmap_backing_file(struct vm_area_struct *vma,
+ struct file *backing_file,
+ struct file *user_file);
int security_mmap_addr(unsigned long addr);
int security_file_mprotect(struct vm_area_struct *vma, unsigned long reqprot,
unsigned long prot);
@@ -1065,6 +1071,15 @@ static inline void security_file_release(struct file *file)
static inline void security_file_free(struct file *file)
{ }
+static inline int security_backing_file_alloc(struct file *backing_file,
+ const struct file *user_file)
+{
+ return 0;
+}
+
+static inline void security_backing_file_free(struct file *backing_file)
+{ }
+
static inline int security_file_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
@@ -1084,6 +1099,13 @@ static inline int security_mmap_file(struct file *file, unsigned long prot,
return 0;
}
+static inline int security_mmap_backing_file(struct vm_area_struct *vma,
+ struct file *backing_file,
+ struct file *user_file)
+{
+ return 0;
+}
+
static inline int security_mmap_addr(unsigned long addr)
{
return cap_mmap_addr(addr);
diff --git a/security/security.c b/security/security.c
index 6e4deac6ec07..aef71585d49e 100644
--- a/security/security.c
+++ b/security/security.c
@@ -95,6 +95,7 @@ const char *const lockdown_reasons[LOCKDOWN_CONFIDENTIALITY_MAX + 1] = {
static BLOCKING_NOTIFIER_HEAD(blocking_lsm_notifier_chain);
static struct kmem_cache *lsm_file_cache;
+struct kmem_cache *lsm_backing_file_cache;
static struct kmem_cache *lsm_inode_cache;
char *lsm_names;
@@ -266,6 +267,7 @@ static void __init lsm_set_blob_sizes(struct lsm_blob_sizes *needed)
lsm_set_blob_size(&needed->lbs_cred, &blob_sizes.lbs_cred);
lsm_set_blob_size(&needed->lbs_file, &blob_sizes.lbs_file);
+ lsm_set_blob_size(&needed->lbs_backing_file, &blob_sizes.lbs_backing_file);
lsm_set_blob_size(&needed->lbs_ib, &blob_sizes.lbs_ib);
/*
* The inode blob gets an rcu_head in addition to
@@ -468,6 +470,7 @@ static void __init ordered_lsm_init(void)
init_debug("cred blob size = %d\n", blob_sizes.lbs_cred);
init_debug("file blob size = %d\n", blob_sizes.lbs_file);
+ init_debug("lsm_backing_file_cache = %d\n", blob_sizes.lbs_backing_file);
init_debug("ib blob size = %d\n", blob_sizes.lbs_ib);
init_debug("inode blob size = %d\n", blob_sizes.lbs_inode);
init_debug("ipc blob size = %d\n", blob_sizes.lbs_ipc);
@@ -490,6 +493,11 @@ static void __init ordered_lsm_init(void)
lsm_file_cache = kmem_cache_create("lsm_file_cache",
blob_sizes.lbs_file, 0,
SLAB_PANIC, NULL);
+ if (blob_sizes.lbs_backing_file)
+ lsm_backing_file_cache = kmem_cache_create(
+ "lsm_backing_file_cache",
+ blob_sizes.lbs_backing_file,
+ 0, SLAB_PANIC, NULL);
if (blob_sizes.lbs_inode)
lsm_inode_cache = kmem_cache_create("lsm_inode_cache",
blob_sizes.lbs_inode, 0,
@@ -666,6 +674,30 @@ int unregister_blocking_lsm_notifier(struct notifier_block *nb)
}
EXPORT_SYMBOL(unregister_blocking_lsm_notifier);
+/**
+ * lsm_backing_file_alloc - allocate a composite backing file blob
+ * @backing_file: the backing file
+ *
+ * Allocate the backing file blob for all the modules.
+ *
+ * Returns 0, or -ENOMEM if memory can't be allocated.
+ */
+static int lsm_backing_file_alloc(struct file *backing_file)
+{
+ void *blob;
+
+ if (!lsm_backing_file_cache) {
+ backing_file_set_security(backing_file, NULL);
+ return 0;
+ }
+
+ blob = kmem_cache_zalloc(lsm_backing_file_cache, GFP_KERNEL);
+ backing_file_set_security(backing_file, blob);
+ if (!blob)
+ return -ENOMEM;
+ return 0;
+}
+
/**
* lsm_blob_alloc - allocate a composite blob
* @dest: the destination for the blob
@@ -2893,6 +2925,57 @@ void security_file_free(struct file *file)
}
}
+/**
+ * security_backing_file_alloc() - Allocate and setup a backing file blob
+ * @backing_file: the backing file
+ * @user_file: the associated user visible file
+ *
+ * Allocate a backing file LSM blob and perform any necessary initialization of
+ * the LSM blob. There will be some operations where the LSM will not have
+ * access to @user_file after this point, so any important state associated
+ * with @user_file that is important to the LSM should be captured in the
+ * backing file's LSM blob.
+ *
+ * LSM's should avoid taking a reference to @user_file in this hook as it will
+ * result in problems later when the system attempts to drop/put the file
+ * references due to a circular dependency.
+ *
+ * Return: Return 0 if the hook is successful, negative values otherwise.
+ */
+int security_backing_file_alloc(struct file *backing_file,
+ const struct file *user_file)
+{
+ int rc;
+
+ rc = lsm_backing_file_alloc(backing_file);
+ if (rc)
+ return rc;
+ rc = call_int_hook(backing_file_alloc, backing_file, user_file);
+ if (unlikely(rc))
+ security_backing_file_free(backing_file);
+
+ return rc;
+}
+
+/**
+ * security_backing_file_free() - Free a backing file blob
+ * @backing_file: the backing file
+ *
+ * Free any LSM state associate with a backing file's LSM blob, including the
+ * blob itself.
+ */
+void security_backing_file_free(struct file *backing_file)
+{
+ void *blob = backing_file_security(backing_file);
+
+ call_void_hook(backing_file_free, backing_file);
+
+ if (blob) {
+ backing_file_set_security(backing_file, NULL);
+ kmem_cache_free(lsm_backing_file_cache, blob);
+ }
+}
+
/**
* security_file_ioctl() - Check if an ioctl is allowed
* @file: associated file
@@ -2981,6 +3064,32 @@ int security_mmap_file(struct file *file, unsigned long prot,
flags);
}
+/**
+ * security_mmap_backing_file - Check if mmap'ing a backing file is allowed
+ * @vma: the vm_area_struct for the mmap'd region
+ * @backing_file: the backing file being mmap'd
+ * @user_file: the user file being mmap'd
+ *
+ * Check permissions for a mmap operation on a stacked filesystem. This hook
+ * is called after the security_mmap_file() and is responsible for authorizing
+ * the mmap on @backing_file. It is important to note that the mmap operation
+ * on @user_file has already been authorized and the @vma->vm_file has been
+ * set to @backing_file.
+ *
+ * Return: Returns 0 if permission is granted.
+ */
+int security_mmap_backing_file(struct vm_area_struct *vma,
+ struct file *backing_file,
+ struct file *user_file)
+{
+ /* recommended by the stackable filesystem devs */
+ if (WARN_ON_ONCE(!(backing_file->f_mode & FMODE_BACKING)))
+ return -EIO;
+
+ return call_int_hook(mmap_backing_file, vma, backing_file, user_file);
+}
+EXPORT_SYMBOL_GPL(security_mmap_backing_file);
+
/**
* security_mmap_addr() - Check if mmap'ing an address is allowed
* @addr: address
--
2.18.0.huawei.25
^ permalink raw reply related
* [stable/linux-6.12.y 0/2] Backport Fix incorrect overlayfs mmap() and mprotect() LSM access controls
From: Cai Xinchen @ 2026-06-22 3:15 UTC (permalink / raw)
To: viro, brauner, jack, miklos, amir73il, paul, jmorris, serge,
stephen.smalley.work, omosnace, gregkh, bboscaccy
Cc: linux-fsdevel, linux-kernel, linux-unionfs, linux-security-module,
selinux, bpf, lujialin4
Backport the patch series
"Fix incorrect overlayfs mmap() and mprotect() LSM access controls" [1]
to 6.12 lts
I test selinux-testsuite[2] overlay test, it pass 135 tests.
[1] https://lore.kernel.org/all/20260403030848.731867-5-paul@paul-moore.com/
[2] https://github.com/SELinuxProject/selinux-testsuite
Paul Moore (2):
lsm: add backing_file LSM hooks
selinux: fix overlayfs mmap() and mprotect() access checks
fs/backing-file.c | 18 ++-
fs/file_table.c | 27 +++-
fs/fuse/passthrough.c | 2 +-
fs/internal.h | 3 +-
fs/overlayfs/dir.c | 2 +-
fs/overlayfs/file.c | 3 +-
include/linux/backing-file.h | 4 +-
include/linux/fs.h | 13 ++
include/linux/lsm_audit.h | 2 +-
include/linux/lsm_hook_defs.h | 5 +
include/linux/lsm_hooks.h | 1 +
include/linux/security.h | 22 +++
security/security.c | 109 ++++++++++++++
security/selinux/hooks.c | 242 ++++++++++++++++++++++--------
security/selinux/include/objsec.h | 11 ++
15 files changed, 384 insertions(+), 80 deletions(-)
--
2.18.0.huawei.25
^ permalink raw reply
* [stable/linux-6.18.y 0/2] Backport Fix incorrect overlayfs mmap() and mprotect() LSM access controls
From: Cai Xinchen @ 2026-06-22 3:14 UTC (permalink / raw)
To: viro, brauner, jack, miklos, amir73il, paul, jmorris, serge,
stephen.smalley.work, omosnace, gregkh, bboscaccy
Cc: linux-fsdevel, linux-kernel, linux-unionfs, linux-security-module,
selinux, bpf, lujialin4
Backport the patch series
"Fix incorrect overlayfs mmap() and mprotect() LSM access controls" [1]
to 6.18 lts
I test selinux-testsuite[2] overlay test, it pass 135 tests.
[1] https://lore.kernel.org/all/20260403030848.731867-5-paul@paul-moore.com/
[2] https://github.com/SELinuxProject/selinux-testsuite
Paul Moore (2):
lsm: add backing_file LSM hooks
selinux: fix overlayfs mmap() and mprotect() access checks
fs/backing-file.c | 17 ++-
fs/file_table.c | 27 +++-
fs/fuse/passthrough.c | 2 +-
fs/internal.h | 3 +-
fs/overlayfs/dir.c | 2 +-
fs/overlayfs/file.c | 2 +-
include/linux/backing-file.h | 4 +-
include/linux/fs.h | 13 ++
include/linux/lsm_audit.h | 2 +-
include/linux/lsm_hook_defs.h | 5 +
include/linux/lsm_hooks.h | 1 +
include/linux/security.h | 22 +++
security/security.c | 109 ++++++++++++++
security/selinux/hooks.c | 242 ++++++++++++++++++++++--------
security/selinux/include/objsec.h | 11 ++
15 files changed, 383 insertions(+), 79 deletions(-)
--
2.18.0.huawei.25
^ permalink raw reply
* [stable/linux-6.18.y 1/2] lsm: add backing_file LSM hooks
From: Cai Xinchen @ 2026-06-22 3:14 UTC (permalink / raw)
To: viro, brauner, jack, miklos, amir73il, paul, jmorris, serge,
stephen.smalley.work, omosnace, gregkh, bboscaccy
Cc: linux-fsdevel, linux-kernel, linux-unionfs, linux-security-module,
selinux, bpf, lujialin4
In-Reply-To: <20260622031416.2663747-1-caixinchen1@huawei.com>
From: Paul Moore <paul@paul-moore.com>
Mainline declares lsm_backing_file_cache in security/lsm.h. Linux 6.18.y
does not have security/lsm_init.c or security/lsm.h; the cache variable
is defined locally as static struct kmem_cache *lsm_backing_file_cache in
security/security.c.
Original commit message:
Stacked filesystems such as overlayfs do not currently provide the
necessary mechanisms for LSMs to properly enforce access controls on the
mmap() and mprotect() operations. In order to resolve this gap, a LSM
security blob is being added to the backing_file struct and the following
new LSM hooks are being created:
security_backing_file_alloc()
security_backing_file_free()
security_mmap_backing_file()
The first two hooks are to manage the lifecycle of the LSM security blob
in the backing_file struct, while the third provides a new mmap() access
control point for the underlying backing file. It is also expected that
LSMs will likely want to update their security_file_mprotect() callback
to address issues with their mprotect() controls, but that does not
require a change to the security_file_mprotect() LSM hook.
There are a three other small changes to support these new LSM hooks:
* Pass the user file associated with a backing file down to
alloc_empty_backing_file() so it can be included in the
security_backing_file_alloc() hook.
* Add getter and setter functions for the backing_file struct LSM blob
as the backing_file struct remains private to fs/file_table.c.
* Constify the file struct field in the LSM common_audit_data struct to
better support LSMs that need to pass a const file struct pointer into
the common LSM audit code.
Thanks to Arnd Bergmann for identifying the missing EXPORT_SYMBOL_GPL()
and supplying a fixup.
Cc: stable@vger.kernel.org
Cc: linux-fsdevel@vger.kernel.org
Cc: linux-unionfs@vger.kernel.org
Cc: linux-erofs@lists.ozlabs.org
Reviewed-by: Amir Goldstein <amir73il@gmail.com>
Reviewed-by: Serge Hallyn <serge@hallyn.com>
Reviewed-by: Christian Brauner <brauner@kernel.org>
Signed-off-by: Paul Moore <paul@paul-moore.com>
Signed-off-by: Cai Xinchen <caixinchen1@huawei.com>
---
fs/backing-file.c | 17 ++++--
fs/file_table.c | 27 +++++++--
fs/fuse/passthrough.c | 2 +-
fs/internal.h | 3 +-
fs/overlayfs/dir.c | 2 +-
fs/overlayfs/file.c | 2 +-
include/linux/backing-file.h | 4 +-
include/linux/fs.h | 13 ++++
include/linux/lsm_audit.h | 2 +-
include/linux/lsm_hook_defs.h | 5 ++
include/linux/lsm_hooks.h | 1 +
include/linux/security.h | 22 +++++++
security/security.c | 109 ++++++++++++++++++++++++++++++++++
13 files changed, 194 insertions(+), 15 deletions(-)
diff --git a/fs/backing-file.c b/fs/backing-file.c
index 15a7f8031084..e049a627d78f 100644
--- a/fs/backing-file.c
+++ b/fs/backing-file.c
@@ -12,6 +12,7 @@
#include <linux/backing-file.h>
#include <linux/splice.h>
#include <linux/mm.h>
+#include <linux/security.h>
#include "internal.h"
@@ -29,14 +30,15 @@
* returned file into a container structure that also stores the stacked
* file's path, which can be retrieved using backing_file_user_path().
*/
-struct file *backing_file_open(const struct path *user_path, int flags,
+struct file *backing_file_open(const struct file *user_file, int flags,
const struct path *real_path,
const struct cred *cred)
{
+ const struct path *user_path = &user_file->f_path;
struct file *f;
int error;
- f = alloc_empty_backing_file(flags, cred);
+ f = alloc_empty_backing_file(flags, cred, user_file);
if (IS_ERR(f))
return f;
@@ -52,15 +54,16 @@ struct file *backing_file_open(const struct path *user_path, int flags,
}
EXPORT_SYMBOL_GPL(backing_file_open);
-struct file *backing_tmpfile_open(const struct path *user_path, int flags,
+struct file *backing_tmpfile_open(const struct file *user_file, int flags,
const struct path *real_parentpath,
umode_t mode, const struct cred *cred)
{
struct mnt_idmap *real_idmap = mnt_idmap(real_parentpath->mnt);
+ const struct path *user_path = &user_file->f_path;
struct file *f;
int error;
- f = alloc_empty_backing_file(flags, cred);
+ f = alloc_empty_backing_file(flags, cred, user_file);
if (IS_ERR(f))
return f;
@@ -339,6 +342,12 @@ int backing_file_mmap(struct file *file, struct vm_area_struct *vma,
vma_set_file(vma, file);
old_cred = override_creds(ctx->cred);
+ ret = security_mmap_backing_file(vma, file, user_file);
+ if (ret) {
+ revert_creds(old_cred);
+ return ret;
+ }
+
ret = vfs_mmap(vma->vm_file, vma);
revert_creds(old_cred);
diff --git a/fs/file_table.c b/fs/file_table.c
index 762f03dcbcd7..987e01da9938 100644
--- a/fs/file_table.c
+++ b/fs/file_table.c
@@ -50,6 +50,9 @@ struct backing_file {
struct path user_path;
freeptr_t bf_freeptr;
};
+#ifdef CONFIG_SECURITY
+ void *security;
+#endif
};
#define backing_file(f) container_of(f, struct backing_file, file)
@@ -66,8 +69,21 @@ void backing_file_set_user_path(struct file *f, const struct path *path)
}
EXPORT_SYMBOL_GPL(backing_file_set_user_path);
+#ifdef CONFIG_SECURITY
+void *backing_file_security(const struct file *f)
+{
+ return backing_file(f)->security;
+}
+
+void backing_file_set_security(struct file *f, void *security)
+{
+ backing_file(f)->security = security;
+}
+#endif /* CONFIG_SECURITY */
+
static inline void backing_file_free(struct backing_file *ff)
{
+ security_backing_file_free(&ff->file);
path_put(&ff->user_path);
kmem_cache_free(bfilp_cachep, ff);
}
@@ -288,10 +304,12 @@ struct file *alloc_empty_file_noaccount(int flags, const struct cred *cred)
return f;
}
-static int init_backing_file(struct backing_file *ff)
+static int init_backing_file(struct backing_file *ff,
+ const struct file *user_file)
{
memset(&ff->user_path, 0, sizeof(ff->user_path));
- return 0;
+ backing_file_set_security(&ff->file, NULL);
+ return security_backing_file_alloc(&ff->file, user_file);
}
/*
@@ -301,7 +319,8 @@ static int init_backing_file(struct backing_file *ff)
* This is only for kernel internal use, and the allocate file must not be
* installed into file tables or such.
*/
-struct file *alloc_empty_backing_file(int flags, const struct cred *cred)
+struct file *alloc_empty_backing_file(int flags, const struct cred *cred,
+ const struct file *user_file)
{
struct backing_file *ff;
int error;
@@ -318,7 +337,7 @@ struct file *alloc_empty_backing_file(int flags, const struct cred *cred)
/* The f_mode flags must be set before fput(). */
ff->file.f_mode |= FMODE_BACKING | FMODE_NOACCOUNT;
- error = init_backing_file(ff);
+ error = init_backing_file(ff, user_file);
if (unlikely(error)) {
fput(&ff->file);
return ERR_PTR(error);
diff --git a/fs/fuse/passthrough.c b/fs/fuse/passthrough.c
index 72de97c03d0e..f2d08ac2459b 100644
--- a/fs/fuse/passthrough.c
+++ b/fs/fuse/passthrough.c
@@ -167,7 +167,7 @@ struct fuse_backing *fuse_passthrough_open(struct file *file, int backing_id)
goto out;
/* Allocate backing file per fuse file to store fuse path */
- backing_file = backing_file_open(&file->f_path, file->f_flags,
+ backing_file = backing_file_open(file, file->f_flags,
&fb->file->f_path, fb->cred);
err = PTR_ERR(backing_file);
if (IS_ERR(backing_file)) {
diff --git a/fs/internal.h b/fs/internal.h
index 9b2b4d116880..51107fd51514 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -100,7 +100,8 @@ extern void chroot_fs_refs(const struct path *, const struct path *);
*/
struct file *alloc_empty_file(int flags, const struct cred *cred);
struct file *alloc_empty_file_noaccount(int flags, const struct cred *cred);
-struct file *alloc_empty_backing_file(int flags, const struct cred *cred);
+struct file *alloc_empty_backing_file(int flags, const struct cred *cred,
+ const struct file *user_file);
void backing_file_set_user_path(struct file *f, const struct path *path);
static inline void file_put_write_access(struct file *file)
diff --git a/fs/overlayfs/dir.c b/fs/overlayfs/dir.c
index a5e9ddf3023b..e924321b6402 100644
--- a/fs/overlayfs/dir.c
+++ b/fs/overlayfs/dir.c
@@ -1355,7 +1355,7 @@ static int ovl_create_tmpfile(struct file *file, struct dentry *dentry,
}
ovl_path_upper(dentry->d_parent, &realparentpath);
- realfile = backing_tmpfile_open(&file->f_path, flags, &realparentpath,
+ realfile = backing_tmpfile_open(file, flags, &realparentpath,
mode, current_cred());
err = PTR_ERR_OR_ZERO(realfile);
pr_debug("tmpfile/open(%pd2, 0%o) = %i\n", realparentpath.dentry, mode, err);
diff --git a/fs/overlayfs/file.c b/fs/overlayfs/file.c
index 7ab2c9daffd0..3fedfdddfa75 100644
--- a/fs/overlayfs/file.c
+++ b/fs/overlayfs/file.c
@@ -48,7 +48,7 @@ static struct file *ovl_open_realfile(const struct file *file,
if (!inode_owner_or_capable(real_idmap, realinode))
flags &= ~O_NOATIME;
- realfile = backing_file_open(file_user_path(file),
+ realfile = backing_file_open(file,
flags, realpath, current_cred());
}
ovl_revert_creds(old_cred);
diff --git a/include/linux/backing-file.h b/include/linux/backing-file.h
index 1476a6ed1bfd..c939cd222730 100644
--- a/include/linux/backing-file.h
+++ b/include/linux/backing-file.h
@@ -18,10 +18,10 @@ struct backing_file_ctx {
void (*end_write)(struct kiocb *iocb, ssize_t);
};
-struct file *backing_file_open(const struct path *user_path, int flags,
+struct file *backing_file_open(const struct file *user_file, int flags,
const struct path *real_path,
const struct cred *cred);
-struct file *backing_tmpfile_open(const struct path *user_path, int flags,
+struct file *backing_tmpfile_open(const struct file *user_file, int flags,
const struct path *real_parentpath,
umode_t mode, const struct cred *cred);
ssize_t backing_file_read_iter(struct file *file, struct iov_iter *iter,
diff --git a/include/linux/fs.h b/include/linux/fs.h
index 014cb04eefbe..f3e798184a58 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -2890,6 +2890,19 @@ struct file *dentry_create(const struct path *path, int flags, umode_t mode,
const struct cred *cred);
const struct path *backing_file_user_path(const struct file *f);
+#ifdef CONFIG_SECURITY
+void *backing_file_security(const struct file *f);
+void backing_file_set_security(struct file *f, void *security);
+#else
+static inline void *backing_file_security(const struct file *f)
+{
+ return NULL;
+}
+static inline void backing_file_set_security(struct file *f, void *security)
+{
+}
+#endif /* CONFIG_SECURITY */
+
/*
* When mmapping a file on a stackable filesystem (e.g., overlayfs), the file
* stored in ->vm_file is a backing file whose f_inode is on the underlying
diff --git a/include/linux/lsm_audit.h b/include/linux/lsm_audit.h
index 382c56a97bba..584db296e43b 100644
--- a/include/linux/lsm_audit.h
+++ b/include/linux/lsm_audit.h
@@ -94,7 +94,7 @@ struct common_audit_data {
#endif
char *kmod_name;
struct lsm_ioctlop_audit *op;
- struct file *file;
+ const struct file *file;
struct lsm_ibpkey_audit *ibpkey;
struct lsm_ibendport_audit *ibendport;
int reason;
diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
index 8c42b4bde09c..b4958167e381 100644
--- a/include/linux/lsm_hook_defs.h
+++ b/include/linux/lsm_hook_defs.h
@@ -191,6 +191,9 @@ LSM_HOOK(int, 0, file_permission, struct file *file, int mask)
LSM_HOOK(int, 0, file_alloc_security, struct file *file)
LSM_HOOK(void, LSM_RET_VOID, file_release, struct file *file)
LSM_HOOK(void, LSM_RET_VOID, file_free_security, struct file *file)
+LSM_HOOK(int, 0, backing_file_alloc, struct file *backing_file,
+ const struct file *user_file)
+LSM_HOOK(void, LSM_RET_VOID, backing_file_free, struct file *backing_file)
LSM_HOOK(int, 0, file_ioctl, struct file *file, unsigned int cmd,
unsigned long arg)
LSM_HOOK(int, 0, file_ioctl_compat, struct file *file, unsigned int cmd,
@@ -198,6 +201,8 @@ LSM_HOOK(int, 0, file_ioctl_compat, struct file *file, unsigned int cmd,
LSM_HOOK(int, 0, mmap_addr, unsigned long addr)
LSM_HOOK(int, 0, mmap_file, struct file *file, unsigned long reqprot,
unsigned long prot, unsigned long flags)
+LSM_HOOK(int, 0, mmap_backing_file, struct vm_area_struct *vma,
+ struct file *backing_file, struct file *user_file)
LSM_HOOK(int, 0, file_mprotect, struct vm_area_struct *vma,
unsigned long reqprot, unsigned long prot)
LSM_HOOK(int, 0, file_lock, struct file *file, unsigned int cmd)
diff --git a/include/linux/lsm_hooks.h b/include/linux/lsm_hooks.h
index 79ec5a2bdcca..e0707ec5a76d 100644
--- a/include/linux/lsm_hooks.h
+++ b/include/linux/lsm_hooks.h
@@ -104,6 +104,7 @@ struct security_hook_list {
struct lsm_blob_sizes {
int lbs_cred;
int lbs_file;
+ unsigned int lbs_backing_file;
int lbs_ib;
int lbs_inode;
int lbs_sock;
diff --git a/include/linux/security.h b/include/linux/security.h
index b64598e5d65d..e54025362426 100644
--- a/include/linux/security.h
+++ b/include/linux/security.h
@@ -473,11 +473,17 @@ int security_file_permission(struct file *file, int mask);
int security_file_alloc(struct file *file);
void security_file_release(struct file *file);
void security_file_free(struct file *file);
+int security_backing_file_alloc(struct file *backing_file,
+ const struct file *user_file);
+void security_backing_file_free(struct file *backing_file);
int security_file_ioctl(struct file *file, unsigned int cmd, unsigned long arg);
int security_file_ioctl_compat(struct file *file, unsigned int cmd,
unsigned long arg);
int security_mmap_file(struct file *file, unsigned long prot,
unsigned long flags);
+int security_mmap_backing_file(struct vm_area_struct *vma,
+ struct file *backing_file,
+ struct file *user_file);
int security_mmap_addr(unsigned long addr);
int security_file_mprotect(struct vm_area_struct *vma, unsigned long reqprot,
unsigned long prot);
@@ -1142,6 +1148,15 @@ static inline void security_file_release(struct file *file)
static inline void security_file_free(struct file *file)
{ }
+static inline int security_backing_file_alloc(struct file *backing_file,
+ const struct file *user_file)
+{
+ return 0;
+}
+
+static inline void security_backing_file_free(struct file *backing_file)
+{ }
+
static inline int security_file_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
@@ -1161,6 +1176,13 @@ static inline int security_mmap_file(struct file *file, unsigned long prot,
return 0;
}
+static inline int security_mmap_backing_file(struct vm_area_struct *vma,
+ struct file *backing_file,
+ struct file *user_file)
+{
+ return 0;
+}
+
static inline int security_mmap_addr(unsigned long addr)
{
return cap_mmap_addr(addr);
diff --git a/security/security.c b/security/security.c
index 603c3c6d5635..5d5ad55dbca7 100644
--- a/security/security.c
+++ b/security/security.c
@@ -94,6 +94,7 @@ const char *const lockdown_reasons[LOCKDOWN_CONFIDENTIALITY_MAX + 1] = {
static BLOCKING_NOTIFIER_HEAD(blocking_lsm_notifier_chain);
static struct kmem_cache *lsm_file_cache;
+struct kmem_cache *lsm_backing_file_cache;
static struct kmem_cache *lsm_inode_cache;
char *lsm_names;
@@ -265,6 +266,7 @@ static void __init lsm_set_blob_sizes(struct lsm_blob_sizes *needed)
lsm_set_blob_size(&needed->lbs_cred, &blob_sizes.lbs_cred);
lsm_set_blob_size(&needed->lbs_file, &blob_sizes.lbs_file);
+ lsm_set_blob_size(&needed->lbs_backing_file, &blob_sizes.lbs_backing_file);
lsm_set_blob_size(&needed->lbs_ib, &blob_sizes.lbs_ib);
/*
* The inode blob gets an rcu_head in addition to
@@ -470,6 +472,7 @@ static void __init ordered_lsm_init(void)
init_debug("cred blob size = %d\n", blob_sizes.lbs_cred);
init_debug("file blob size = %d\n", blob_sizes.lbs_file);
+ init_debug("lsm_backing_file_cache = %d\n", blob_sizes.lbs_backing_file);
init_debug("ib blob size = %d\n", blob_sizes.lbs_ib);
init_debug("inode blob size = %d\n", blob_sizes.lbs_inode);
init_debug("ipc blob size = %d\n", blob_sizes.lbs_ipc);
@@ -495,6 +498,11 @@ static void __init ordered_lsm_init(void)
lsm_file_cache = kmem_cache_create("lsm_file_cache",
blob_sizes.lbs_file, 0,
SLAB_PANIC, NULL);
+ if (blob_sizes.lbs_backing_file)
+ lsm_backing_file_cache = kmem_cache_create(
+ "lsm_backing_file_cache",
+ blob_sizes.lbs_backing_file,
+ 0, SLAB_PANIC, NULL);
if (blob_sizes.lbs_inode)
lsm_inode_cache = kmem_cache_create("lsm_inode_cache",
blob_sizes.lbs_inode, 0,
@@ -671,6 +679,30 @@ int unregister_blocking_lsm_notifier(struct notifier_block *nb)
}
EXPORT_SYMBOL(unregister_blocking_lsm_notifier);
+/**
+ * lsm_backing_file_alloc - allocate a composite backing file blob
+ * @backing_file: the backing file
+ *
+ * Allocate the backing file blob for all the modules.
+ *
+ * Returns 0, or -ENOMEM if memory can't be allocated.
+ */
+static int lsm_backing_file_alloc(struct file *backing_file)
+{
+ void *blob;
+
+ if (!lsm_backing_file_cache) {
+ backing_file_set_security(backing_file, NULL);
+ return 0;
+ }
+
+ blob = kmem_cache_zalloc(lsm_backing_file_cache, GFP_KERNEL);
+ backing_file_set_security(backing_file, blob);
+ if (!blob)
+ return -ENOMEM;
+ return 0;
+}
+
/**
* lsm_blob_alloc - allocate a composite blob
* @dest: the destination for the blob
@@ -2965,6 +2997,57 @@ void security_file_free(struct file *file)
}
}
+/**
+ * security_backing_file_alloc() - Allocate and setup a backing file blob
+ * @backing_file: the backing file
+ * @user_file: the associated user visible file
+ *
+ * Allocate a backing file LSM blob and perform any necessary initialization of
+ * the LSM blob. There will be some operations where the LSM will not have
+ * access to @user_file after this point, so any important state associated
+ * with @user_file that is important to the LSM should be captured in the
+ * backing file's LSM blob.
+ *
+ * LSM's should avoid taking a reference to @user_file in this hook as it will
+ * result in problems later when the system attempts to drop/put the file
+ * references due to a circular dependency.
+ *
+ * Return: Return 0 if the hook is successful, negative values otherwise.
+ */
+int security_backing_file_alloc(struct file *backing_file,
+ const struct file *user_file)
+{
+ int rc;
+
+ rc = lsm_backing_file_alloc(backing_file);
+ if (rc)
+ return rc;
+ rc = call_int_hook(backing_file_alloc, backing_file, user_file);
+ if (unlikely(rc))
+ security_backing_file_free(backing_file);
+
+ return rc;
+}
+
+/**
+ * security_backing_file_free() - Free a backing file blob
+ * @backing_file: the backing file
+ *
+ * Free any LSM state associate with a backing file's LSM blob, including the
+ * blob itself.
+ */
+void security_backing_file_free(struct file *backing_file)
+{
+ void *blob = backing_file_security(backing_file);
+
+ call_void_hook(backing_file_free, backing_file);
+
+ if (blob) {
+ backing_file_set_security(backing_file, NULL);
+ kmem_cache_free(lsm_backing_file_cache, blob);
+ }
+}
+
/**
* security_file_ioctl() - Check if an ioctl is allowed
* @file: associated file
@@ -3053,6 +3136,32 @@ int security_mmap_file(struct file *file, unsigned long prot,
flags);
}
+/**
+ * security_mmap_backing_file - Check if mmap'ing a backing file is allowed
+ * @vma: the vm_area_struct for the mmap'd region
+ * @backing_file: the backing file being mmap'd
+ * @user_file: the user file being mmap'd
+ *
+ * Check permissions for a mmap operation on a stacked filesystem. This hook
+ * is called after the security_mmap_file() and is responsible for authorizing
+ * the mmap on @backing_file. It is important to note that the mmap operation
+ * on @user_file has already been authorized and the @vma->vm_file has been
+ * set to @backing_file.
+ *
+ * Return: Returns 0 if permission is granted.
+ */
+int security_mmap_backing_file(struct vm_area_struct *vma,
+ struct file *backing_file,
+ struct file *user_file)
+{
+ /* recommended by the stackable filesystem devs */
+ if (WARN_ON_ONCE(!(backing_file->f_mode & FMODE_BACKING)))
+ return -EIO;
+
+ return call_int_hook(mmap_backing_file, vma, backing_file, user_file);
+}
+EXPORT_SYMBOL_GPL(security_mmap_backing_file);
+
/**
* security_mmap_addr() - Check if mmap'ing an address is allowed
* @addr: address
--
2.18.0.huawei.25
^ permalink raw reply related
* [stable/linux-6.18.y 2/2] selinux: fix overlayfs mmap() and mprotect() access checks
From: Cai Xinchen @ 2026-06-22 3:14 UTC (permalink / raw)
To: viro, brauner, jack, miklos, amir73il, paul, jmorris, serge,
stephen.smalley.work, omosnace, gregkh, bboscaccy
Cc: linux-fsdevel, linux-kernel, linux-unionfs, linux-security-module,
selinux, bpf, lujialin4
In-Reply-To: <20260622031416.2663747-1-caixinchen1@huawei.com>
From: Paul Moore <paul@paul-moore.com>
The existing SELinux security model for overlayfs is to allow access if
the current task is able to access the top level file (the "user" file)
and the mounter's credentials are sufficient to access the lower
level file (the "backing" file). Unfortunately, the current code does
not properly enforce these access controls for both mmap() and mprotect()
operations on overlayfs filesystems.
This patch makes use of the newly created security_mmap_backing_file()
LSM hook to provide the missing backing file enforcement for mmap()
operations, and leverages the backing file API and new LSM blob to
provide the necessary information to properly enforce the mprotect()
access controls.
Cc: stable@vger.kernel.org
Acked-by: Amir Goldstein <amir73il@gmail.com>
Signed-off-by: Paul Moore <paul@paul-moore.com>
Signed-off-by: Cai Xinchen <caixinchen1@huawei.com>
---
security/selinux/hooks.c | 242 ++++++++++++++++++++++--------
security/selinux/include/objsec.h | 11 ++
2 files changed, 189 insertions(+), 64 deletions(-)
diff --git a/security/selinux/hooks.c b/security/selinux/hooks.c
index 3da3017ad2ca..f96ee8f372e3 100644
--- a/security/selinux/hooks.c
+++ b/security/selinux/hooks.c
@@ -1739,49 +1739,72 @@ static inline int file_path_has_perm(const struct cred *cred,
static int bpf_fd_pass(const struct file *file, u32 sid);
#endif
-/* Check whether a task can use an open file descriptor to
- access an inode in a given way. Check access to the
- descriptor itself, and then use dentry_has_perm to
- check a particular permission to the file.
- Access to the descriptor is implicitly granted if it
- has the same SID as the process. If av is zero, then
- access to the file is not checked, e.g. for cases
- where only the descriptor is affected like seek. */
-static int file_has_perm(const struct cred *cred,
- struct file *file,
- u32 av)
+static int __file_has_perm(const struct cred *cred, const struct file *file,
+ u32 av, bool bf_user_file)
+
{
- struct file_security_struct *fsec = selinux_file(file);
- struct inode *inode = file_inode(file);
struct common_audit_data ad;
- u32 sid = cred_sid(cred);
+ struct inode *inode;
+ u32 ssid = cred_sid(cred);
+ u32 tsid_fd;
int rc;
- ad.type = LSM_AUDIT_DATA_FILE;
- ad.u.file = file;
+ if (bf_user_file) {
+ struct backing_file_security_struct *bfsec;
+ const struct path *path;
- if (sid != fsec->sid) {
- rc = avc_has_perm(sid, fsec->sid,
- SECCLASS_FD,
- FD__USE,
- &ad);
+ if (WARN_ON(!(file->f_mode & FMODE_BACKING)))
+ return -EIO;
+
+ bfsec = selinux_backing_file(file);
+ path = backing_file_user_path(file);
+ tsid_fd = bfsec->uf_sid;
+ inode = d_inode(path->dentry);
+
+ ad.type = LSM_AUDIT_DATA_PATH;
+ ad.u.path = *path;
+ } else {
+ struct file_security_struct *fsec = selinux_file(file);
+
+ tsid_fd = fsec->sid;
+ inode = file_inode(file);
+
+ ad.type = LSM_AUDIT_DATA_FILE;
+ ad.u.file = file;
+ }
+
+ if (ssid != tsid_fd) {
+ rc = avc_has_perm(ssid, tsid_fd, SECCLASS_FD, FD__USE, &ad);
if (rc)
- goto out;
+ return rc;
}
#ifdef CONFIG_BPF_SYSCALL
- rc = bpf_fd_pass(file, cred_sid(cred));
+ /* regardless of backing vs user file, use the underlying file here */
+ rc = bpf_fd_pass(file, ssid);
if (rc)
return rc;
#endif
/* av is zero if only checking access to the descriptor. */
- rc = 0;
if (av)
- rc = inode_has_perm(cred, inode, av, &ad);
+ return inode_has_perm(cred, inode, av, &ad);
-out:
- return rc;
+ return 0;
+}
+
+/* Check whether a task can use an open file descriptor to
+ access an inode in a given way. Check access to the
+ descriptor itself, and then use dentry_has_perm to
+ check a particular permission to the file.
+ Access to the descriptor is implicitly granted if it
+ has the same SID as the process. If av is zero, then
+ access to the file is not checked, e.g. for cases
+ where only the descriptor is affected like seek. */
+static inline int file_has_perm(const struct cred *cred,
+ const struct file *file, u32 av)
+{
+ return __file_has_perm(cred, file, av, false);
}
/*
@@ -3799,6 +3822,17 @@ static int selinux_file_alloc_security(struct file *file)
return 0;
}
+static int selinux_backing_file_alloc(struct file *backing_file,
+ const struct file *user_file)
+{
+ struct backing_file_security_struct *bfsec;
+
+ bfsec = selinux_backing_file(backing_file);
+ bfsec->uf_sid = selinux_file(user_file)->sid;
+
+ return 0;
+}
+
/*
* Check whether a task has the ioctl permission and cmd
* operation to an inode.
@@ -3916,42 +3950,55 @@ static int selinux_file_ioctl_compat(struct file *file, unsigned int cmd,
static int default_noexec __ro_after_init;
-static int file_map_prot_check(struct file *file, unsigned long prot, int shared)
+static int __file_map_prot_check(const struct cred *cred,
+ const struct file *file, unsigned long prot,
+ bool shared, bool bf_user_file)
{
- const struct cred *cred = current_cred();
- u32 sid = cred_sid(cred);
- int rc = 0;
+ struct inode *inode = NULL;
+ bool prot_exec = prot & PROT_EXEC;
+ bool prot_write = prot & PROT_WRITE;
+
+ if (file) {
+ if (bf_user_file)
+ inode = d_inode(backing_file_user_path(file)->dentry);
+ else
+ inode = file_inode(file);
+ }
+
+ if (default_noexec && prot_exec &&
+ (!file || IS_PRIVATE(inode) || (!shared && prot_write))) {
+ int rc;
+ u32 sid = cred_sid(cred);
- if (default_noexec &&
- (prot & PROT_EXEC) && (!file || IS_PRIVATE(file_inode(file)) ||
- (!shared && (prot & PROT_WRITE)))) {
/*
- * We are making executable an anonymous mapping or a
- * private file mapping that will also be writable.
- * This has an additional check.
+ * We are making executable an anonymous mapping or a private
+ * file mapping that will also be writable.
*/
- rc = avc_has_perm(sid, sid, SECCLASS_PROCESS,
- PROCESS__EXECMEM, NULL);
+ rc = avc_has_perm(sid, sid, SECCLASS_PROCESS, PROCESS__EXECMEM,
+ NULL);
if (rc)
- goto error;
+ return rc;
}
if (file) {
- /* read access is always possible with a mapping */
+ /* "read" always possible, "write" only if shared */
u32 av = FILE__READ;
-
- /* write access only matters if the mapping is shared */
- if (shared && (prot & PROT_WRITE))
+ if (shared && prot_write)
av |= FILE__WRITE;
-
- if (prot & PROT_EXEC)
+ if (prot_exec)
av |= FILE__EXECUTE;
- return file_has_perm(cred, file, av);
+ return __file_has_perm(cred, file, av, bf_user_file);
}
-error:
- return rc;
+ return 0;
+}
+
+static inline int file_map_prot_check(const struct cred *cred,
+ const struct file *file,
+ unsigned long prot, bool shared)
+{
+ return __file_map_prot_check(cred, file, prot, shared, false);
}
static int selinux_mmap_addr(unsigned long addr)
@@ -3967,36 +4014,80 @@ static int selinux_mmap_addr(unsigned long addr)
return rc;
}
-static int selinux_mmap_file(struct file *file,
- unsigned long reqprot __always_unused,
- unsigned long prot, unsigned long flags)
+static int selinux_mmap_file_common(const struct cred *cred, struct file *file,
+ unsigned long prot, bool shared)
{
- struct common_audit_data ad;
- int rc;
-
if (file) {
+ int rc;
+ struct common_audit_data ad;
+
ad.type = LSM_AUDIT_DATA_FILE;
ad.u.file = file;
- rc = inode_has_perm(current_cred(), file_inode(file),
- FILE__MAP, &ad);
+ rc = inode_has_perm(cred, file_inode(file), FILE__MAP, &ad);
if (rc)
return rc;
}
- return file_map_prot_check(file, prot,
- (flags & MAP_TYPE) == MAP_SHARED);
+ return file_map_prot_check(cred, file, prot, shared);
+}
+
+static int selinux_mmap_file(struct file *file,
+ unsigned long reqprot __always_unused,
+ unsigned long prot, unsigned long flags)
+{
+ return selinux_mmap_file_common(current_cred(), file, prot,
+ (flags & MAP_TYPE) == MAP_SHARED);
+}
+
+/**
+ * selinux_mmap_backing_file - Check mmap permissions on a backing file
+ * @vma: memory region
+ * @backing_file: stacked filesystem backing file
+ * @user_file: user visible file
+ *
+ * This is called after selinux_mmap_file() on stacked filesystems, and it
+ * is this function's responsibility to verify access to @backing_file and
+ * setup the SELinux state for possible later use in the mprotect() code path.
+ *
+ * By the time this function is called, mmap() access to @user_file has already
+ * been authorized and @vma->vm_file has been set to point to @backing_file.
+ *
+ * Return zero on success, negative values otherwise.
+ */
+static int selinux_mmap_backing_file(struct vm_area_struct *vma,
+ struct file *backing_file,
+ struct file *user_file __always_unused)
+{
+ unsigned long prot = 0;
+
+ /* translate vma->vm_flags perms into PROT perms */
+ if (vma->vm_flags & VM_READ)
+ prot |= PROT_READ;
+ if (vma->vm_flags & VM_WRITE)
+ prot |= PROT_WRITE;
+ if (vma->vm_flags & VM_EXEC)
+ prot |= PROT_EXEC;
+
+ return selinux_mmap_file_common(backing_file->f_cred, backing_file,
+ prot, vma->vm_flags & VM_SHARED);
}
static int selinux_file_mprotect(struct vm_area_struct *vma,
unsigned long reqprot __always_unused,
unsigned long prot)
{
+ int rc;
const struct cred *cred = current_cred();
u32 sid = cred_sid(cred);
+ const struct file *file = vma->vm_file;
+ bool backing_file;
+ bool shared = vma->vm_flags & VM_SHARED;
+
+ /* check if we need to trigger the "backing files are awful" mode */
+ backing_file = file && (file->f_mode & FMODE_BACKING);
if (default_noexec &&
(prot & PROT_EXEC) && !(vma->vm_flags & VM_EXEC)) {
- int rc = 0;
/*
* We don't use the vma_is_initial_heap() helper as it has
* a history of problems and is currently broken on systems
@@ -4010,11 +4101,15 @@ static int selinux_file_mprotect(struct vm_area_struct *vma,
vma->vm_end <= vma->vm_mm->brk) {
rc = avc_has_perm(sid, sid, SECCLASS_PROCESS,
PROCESS__EXECHEAP, NULL);
- } else if (!vma->vm_file && (vma_is_initial_stack(vma) ||
+ if (rc)
+ return rc;
+ } else if (!file && (vma_is_initial_stack(vma) ||
vma_is_stack_for_current(vma))) {
rc = avc_has_perm(sid, sid, SECCLASS_PROCESS,
PROCESS__EXECSTACK, NULL);
- } else if (vma->vm_file && vma->anon_vma) {
+ if (rc)
+ return rc;
+ } else if (file && vma->anon_vma) {
/*
* We are making executable a file mapping that has
* had some COW done. Since pages might have been
@@ -4022,13 +4117,29 @@ static int selinux_file_mprotect(struct vm_area_struct *vma,
* modified content. This typically should only
* occur for text relocations.
*/
- rc = file_has_perm(cred, vma->vm_file, FILE__EXECMOD);
+ rc = __file_has_perm(cred, file, FILE__EXECMOD,
+ backing_file);
+ if (rc)
+ return rc;
+ if (backing_file) {
+ rc = file_has_perm(file->f_cred, file,
+ FILE__EXECMOD);
+ if (rc)
+ return rc;
+ }
}
+ }
+
+ rc = __file_map_prot_check(cred, file, prot, shared, backing_file);
+ if (rc)
+ return rc;
+ if (backing_file) {
+ rc = file_map_prot_check(file->f_cred, file, prot, shared);
if (rc)
return rc;
}
- return file_map_prot_check(vma->vm_file, prot, vma->vm_flags&VM_SHARED);
+ return 0;
}
static int selinux_file_lock(struct file *file, unsigned int cmd)
@@ -7140,6 +7251,7 @@ struct lsm_blob_sizes selinux_blob_sizes __ro_after_init = {
.lbs_cred = sizeof(struct cred_security_struct),
.lbs_task = sizeof(struct task_security_struct),
.lbs_file = sizeof(struct file_security_struct),
+ .lbs_backing_file = sizeof(struct backing_file_security_struct),
.lbs_inode = sizeof(struct inode_security_struct),
.lbs_ipc = sizeof(struct ipc_security_struct),
.lbs_key = sizeof(struct key_security_struct),
@@ -7363,9 +7475,11 @@ static struct security_hook_list selinux_hooks[] __ro_after_init = {
LSM_HOOK_INIT(file_permission, selinux_file_permission),
LSM_HOOK_INIT(file_alloc_security, selinux_file_alloc_security),
+ LSM_HOOK_INIT(backing_file_alloc, selinux_backing_file_alloc),
LSM_HOOK_INIT(file_ioctl, selinux_file_ioctl),
LSM_HOOK_INIT(file_ioctl_compat, selinux_file_ioctl_compat),
LSM_HOOK_INIT(mmap_file, selinux_mmap_file),
+ LSM_HOOK_INIT(mmap_backing_file, selinux_mmap_backing_file),
LSM_HOOK_INIT(mmap_addr, selinux_mmap_addr),
LSM_HOOK_INIT(file_mprotect, selinux_file_mprotect),
LSM_HOOK_INIT(file_lock, selinux_file_lock),
diff --git a/security/selinux/include/objsec.h b/security/selinux/include/objsec.h
index 816fde5a5896..fcb46793898f 100644
--- a/security/selinux/include/objsec.h
+++ b/security/selinux/include/objsec.h
@@ -86,6 +86,10 @@ struct file_security_struct {
u32 pseqno; /* Policy seqno at the time of file open */
};
+struct backing_file_security_struct {
+ u32 uf_sid; /* associated user file fsec->sid */
+};
+
struct superblock_security_struct {
u32 sid; /* SID of file system superblock */
u32 def_sid; /* default SID for labeling */
@@ -190,6 +194,13 @@ static inline struct file_security_struct *selinux_file(const struct file *file)
return file->f_security + selinux_blob_sizes.lbs_file;
}
+static inline struct backing_file_security_struct *
+selinux_backing_file(const struct file *backing_file)
+{
+ void *blob = backing_file_security(backing_file);
+ return blob + selinux_blob_sizes.lbs_backing_file;
+}
+
static inline struct inode_security_struct *
selinux_inode(const struct inode *inode)
{
--
2.18.0.huawei.25
^ permalink raw reply related
* [PATCH v9 9/9] landlock: Add KUnit tests for LANDLOCK_ADD_RULE_NO_INHERIT
From: Justin Suess @ 2026-06-21 3:52 UTC (permalink / raw)
To: linux-security-module, mic; +Cc: m, gnoack, gnoack3000, matthieu, Justin Suess
In-Reply-To: <20260621035223.2651547-1-utilityemal77@gmail.com>
Add the landlock_ruleset KUnit suite with three tests for the
no_inherit handling in landlock_unmask_layers():
- test_unmask_no_inherit_propagates: a rule with no_inherit unmasks
access and sets the no_inherit bit on the layer mask.
- test_unmask_multilayer_no_inherit: no_inherit on one layer of a
multi-layer rule only affects that layer.
- test_unmask_no_inherit_sequential: applying a descendant rule
(no_inherit) followed by an ancestor rule causes the ancestor to be
skipped, modeling a path walk. This exercises the same skip branch
that a pre-set no_inherit mask would, via realistic rule application.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
Changes since v8:
- Reduced from five tests to three, dropping test_unmask_no_inherit_skip
and test_unmask_no_inherit_both_set (which set the per-layer no_inherit
bit synthetically). Kept test_unmask_no_inherit_propagates,
test_unmask_multilayer_no_inherit, and test_unmask_no_inherit_sequential,
which exercise the same skip branch through realistic rule application.
- Rebased onto mic/next.
security/landlock/ruleset.c | 137 ++++++++++++++++++++++++++++++++++++
1 file changed, 137 insertions(+)
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index ca7cfa45c90a..144b6fc19f79 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -6,6 +6,7 @@
* Copyright © 2018-2020 ANSSI
*/
+#include <kunit/test.h>
#include <linux/bits.h>
#include <linux/bug.h>
#include <linux/cleanup.h>
@@ -766,3 +767,139 @@ landlock_init_layer_masks(const struct landlock_ruleset *const domain,
return handled_accesses;
}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+/*
+ * Helper to allocate a rule with @num_layers layers and initialize
+ * its num_layers field. Caller must fill in individual layers.
+ */
+static struct landlock_rule *alloc_rule(struct kunit *test, u32 num_layers)
+{
+ struct landlock_rule *rule;
+
+ rule = kzalloc(struct_size(rule, layers, num_layers), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, rule);
+ rule->num_layers = num_layers;
+ return rule;
+}
+
+/*
+ * Build a layer_masks with the first @num_layers layers' access set to
+ * @val, and all no_inherit flags cleared. Layers beyond @num_layers stay
+ * zeroed, matching what landlock_init_layer_masks() produces for a domain
+ * with that many layers.
+ */
+static void fill_masks(struct layer_masks *masks, access_mask_t val,
+ size_t num_layers)
+{
+ memset(masks, 0, sizeof(*masks));
+ for (size_t i = 0; i < num_layers; i++)
+ masks->layers[i].access = val;
+}
+
+/* Verify that a rule with no_inherit unmasks access and propagates the flag. */
+static void test_unmask_no_inherit_propagates(struct kunit *const test)
+{
+ struct landlock_rule *rule = alloc_rule(test, 1);
+ struct layer_masks masks;
+ const access_mask_t req = BIT_ULL(0) | BIT_ULL(1);
+
+ rule->layers[0].level = 1;
+ rule->layers[0].access = BIT_ULL(0);
+ rule->layers[0].flags.no_inherit = true;
+
+ fill_masks(&masks, req, 1);
+ landlock_unmask_layers(rule, &masks);
+
+ /* access bit 0 should be cleared, bit 1 remains */
+ KUNIT_EXPECT_EQ(test, (access_mask_t)masks.layers[0].access,
+ BIT_ULL(1));
+ KUNIT_EXPECT_TRUE(test, masks.layers[0].no_inherit);
+ KUNIT_EXPECT_EQ(test, (access_mask_t)masks.layers[1].access, 0);
+ kfree(rule);
+}
+
+/*
+ * Verify that no_inherit on layer 1 of a multi-layer rule only affects
+ * layer 1; layer 2 still contributes normally.
+ */
+static void test_unmask_multilayer_no_inherit(struct kunit *const test)
+{
+ struct landlock_rule *rule = alloc_rule(test, 2);
+ struct layer_masks masks;
+ const access_mask_t req = BIT_ULL(0) | BIT_ULL(1);
+
+ rule->layers[0].level = 1;
+ rule->layers[0].access = BIT_ULL(0);
+ rule->layers[0].flags.no_inherit = true;
+
+ rule->layers[1].level = 2;
+ rule->layers[1].access = BIT_ULL(1);
+
+ fill_masks(&masks, req, 2);
+ landlock_unmask_layers(rule, &masks);
+
+ /* Layer 1: bit 0 cleared, no_inherit set */
+ KUNIT_EXPECT_EQ(test, (access_mask_t)masks.layers[0].access, BIT_ULL(1));
+ KUNIT_EXPECT_TRUE(test, masks.layers[0].no_inherit);
+
+ /* Layer 2: bit 1 cleared, no_inherit not set */
+ KUNIT_EXPECT_EQ(test, (access_mask_t)masks.layers[1].access, BIT_ULL(0));
+ KUNIT_EXPECT_FALSE(test, masks.layers[1].no_inherit);
+ kfree(rule);
+}
+
+/*
+ * Verify that when applying two rules sequentially (as happens during
+ * a path walk), no_inherit from the first rule prevents the second
+ * rule from contributing to that layer.
+ */
+static void test_unmask_no_inherit_sequential(struct kunit *const test)
+{
+ struct landlock_rule *rule1 = alloc_rule(test, 1);
+ struct landlock_rule *rule2 = alloc_rule(test, 1);
+ struct layer_masks masks;
+ const access_mask_t req = BIT_ULL(0) | BIT_ULL(1);
+
+ /* Rule 1: no_inherit on layer 1, grants access bit 0 */
+ rule1->layers[0].level = 1;
+ rule1->layers[0].access = BIT_ULL(0);
+ rule1->layers[0].flags.no_inherit = true;
+
+ /* Rule 2: also on layer 1, grants access bit 1 (ancestor rule) */
+ rule2->layers[0].level = 1;
+ rule2->layers[0].access = BIT_ULL(1);
+
+ /* Apply rule1 first (descendant), then rule2 (ancestor) */
+ fill_masks(&masks, req, 1);
+ landlock_unmask_layers(rule1, &masks);
+ landlock_unmask_layers(rule2, &masks);
+
+ /*
+ * Rule2 should be skipped because rule1 set no_inherit.
+ * bit 0 cleared by rule1, bit 1 remains because rule2 skipped.
+ */
+ KUNIT_EXPECT_EQ(test, (access_mask_t)masks.layers[0].access, BIT_ULL(1));
+ KUNIT_EXPECT_TRUE(test, masks.layers[0].no_inherit);
+ kfree(rule1);
+ kfree(rule2);
+}
+
+static struct kunit_case test_cases[] = {
+ /* clang-format off */
+ KUNIT_CASE(test_unmask_no_inherit_propagates),
+ KUNIT_CASE(test_unmask_multilayer_no_inherit),
+ KUNIT_CASE(test_unmask_no_inherit_sequential),
+ {}
+ /* clang-format on */
+};
+
+static struct kunit_suite test_suite = {
+ .name = "landlock_ruleset",
+ .test_cases = test_cases,
+};
+
+kunit_test_suite(test_suite);
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
--
2.54.0
^ permalink raw reply related
* [PATCH v9 8/9] selftests/landlock: Add selftests for LANDLOCK_ADD_RULE_NO_INHERIT
From: Justin Suess @ 2026-06-21 3:52 UTC (permalink / raw)
To: linux-security-module, mic; +Cc: m, gnoack, gnoack3000, matthieu, Justin Suess
In-Reply-To: <20260621035223.2651547-1-utilityemal77@gmail.com>
Add test coverage for the new flag:
- New layout1_no_inherit fixture with five variants covering NO_INHERIT
on leaf, middle, and root directories, RW-over-RO expansion, and a
regular file target. Three tests per variant exercise inheritance
blocking, topology sealing (reparenting and same-directory rename,
hard-link of a sealed file, and removal), and layered (multi-domain)
NO_INHERIT.
- A new layout4_disconnected_leafs variant exercising NO_INHERIT applied
through a bind mount, asserting that ancestors in both the bind and
source paths are sealed.
- A new audit_no_inherit fixture verifying that the flag interacts
correctly with the quiet flag: a quiet ancestor does not suppress
audit on a descendant that has crossed a NO_INHERIT boundary.
- Two rejection tests in base_test, modeled on the equivalent quiet
flag tests: useless_no_inherit_rule_fs checks that NO_INHERIT is
rejected on a ruleset that handles no filesystem access, and
no_inherit_rule_net checks that NO_INHERIT (a filesystem-only flag) is
rejected on a network rule.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
Changes since v8:
- Added two base_test rejection tests: useless_no_inherit_rule_fs
(NO_INHERIT on a ruleset handling no filesystem access -> EINVAL) and
no_inherit_rule_net (NO_INHERIT on a network rule -> EINVAL).
- blocks_inheritance now also asserts a same-directory rename and a hard
link of a sealed file are denied with EACCES, and unconditionally
expects unlink of the target to fail; added ni_samedir/ni_link fields
to the layout1_no_inherit variants.
- seals_topology (layered) now handles LANDLOCK_ACCESS_FS_REFER in both
rulesets via a shared handled mask.
- Dropped the redundant parent_is_logged variant from the
audit_no_inherit fixture.
- Rebased onto mic/next.
tools/testing/selftests/landlock/base_test.c | 58 +++
tools/testing/selftests/landlock/fs_test.c | 425 +++++++++++++++++++
2 files changed, 483 insertions(+)
diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
index b8b5fa1042ba..f4435d9a92a8 100644
--- a/tools/testing/selftests/landlock/base_test.c
+++ b/tools/testing/selftests/landlock/base_test.c
@@ -584,6 +584,64 @@ TEST(useless_quiet_rule_net)
ASSERT_EQ(0, close(ruleset_fd));
}
+TEST(useless_no_inherit_rule_fs)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+ };
+ struct landlock_path_beneath_attr path_beneath_attr = {
+ .allowed_access = 0,
+ };
+ int ruleset_fd, root_fd;
+
+ drop_caps(_metadata);
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ root_fd = open("/", O_PATH | O_CLOEXEC);
+ ASSERT_LE(0, root_fd);
+ path_beneath_attr.parent_fd = root_fd;
+
+ /*
+ * A seal is only ever consulted for a domain that handles some
+ * filesystem access, so a no-inherit rule on a ruleset with no handled
+ * filesystem access would be silently inert.
+ */
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath_attr,
+ LANDLOCK_ADD_RULE_NO_INHERIT));
+ ASSERT_EQ(EINVAL, errno);
+
+ ASSERT_EQ(0, close(root_fd));
+ ASSERT_EQ(0, close(ruleset_fd));
+}
+
+TEST(no_inherit_rule_net)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+ };
+ struct landlock_net_port_attr net_port_attr = {
+ .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
+ .port = 1024,
+ };
+ int ruleset_fd;
+
+ drop_caps(_metadata);
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ /* LANDLOCK_ADD_RULE_NO_INHERIT is a filesystem-only flag. */
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+ &net_port_attr,
+ LANDLOCK_ADD_RULE_NO_INHERIT));
+ ASSERT_EQ(EINVAL, errno);
+
+ ASSERT_EQ(0, close(ruleset_fd));
+}
+
TEST(invalid_quiet_bits_1)
{
const struct landlock_ruleset_attr ruleset_attr_fs = {
diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index 86e08aa6e0a7..e5d4fa6a169a 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -1429,6 +1429,238 @@ TEST_F_FORK(layout1, inherit_superset)
ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY));
}
+FIXTURE(layout1_no_inherit) {};
+
+FIXTURE_SETUP(layout1_no_inherit)
+{
+ prepare_layout(_metadata);
+ create_layout1(_metadata);
+}
+
+FIXTURE_TEARDOWN_PARENT(layout1_no_inherit)
+{
+ remove_layout1(_metadata);
+ cleanup_layout(_metadata);
+}
+
+FIXTURE_VARIANT(layout1_no_inherit)
+{
+ const char *ni_path;
+ const __u64 ni_access;
+ const char *ni_file;
+ const char *desc_file;
+ /* Sibling of ni_path within the same parent, for same-dir rename. */
+ const char *ni_samedir;
+ /* Hard-link target for a sealed file (NULL when ni_path is a dir). */
+ const char *ni_link;
+ const int expected_ni_write;
+ const int expected_ni_read;
+ const int expected_desc_write;
+ const int expected_desc_read;
+};
+
+/* NO_INHERIT on leaf directory: blocks parent's RW, grants only RO. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(layout1_no_inherit, rw_parent_ro_leaf) {
+ /* clang-format on */
+ .ni_path = TMP_DIR "/s1d1/s1d2/s1d3",
+ .ni_access = ACCESS_RO,
+ .ni_file = TMP_DIR "/s1d1/s1d2/s1d3/f1",
+ .desc_file = TMP_DIR "/s1d1/s1d2/s1d3/f2",
+ .ni_samedir = TMP_DIR "/s1d1/s1d2/s1d3_renamed",
+ .expected_ni_write = EACCES,
+ .expected_ni_read = 0,
+ .expected_desc_write = EACCES,
+ .expected_desc_read = 0,
+};
+
+/* NO_INHERIT on middle directory: blocks parent's RW for all descendants. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(layout1_no_inherit, rw_parent_ro_middle) {
+ /* clang-format on */
+ .ni_path = TMP_DIR "/s1d1/s1d2",
+ .ni_access = ACCESS_RO,
+ .ni_file = TMP_DIR "/s1d1/s1d2/f1",
+ .desc_file = TMP_DIR "/s1d1/s1d2/s1d3/f1",
+ .ni_samedir = TMP_DIR "/s1d1/s1d2_renamed",
+ .expected_ni_write = EACCES,
+ .expected_ni_read = 0,
+ .expected_desc_write = EACCES,
+ .expected_desc_read = 0,
+};
+
+/* NO_INHERIT on root directory: blocks parent's RW for entire subtree. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(layout1_no_inherit, rw_parent_ro_root) {
+ /* clang-format on */
+ .ni_path = TMP_DIR "/s1d1",
+ .ni_access = ACCESS_RO,
+ .ni_file = TMP_DIR "/s1d1/f1",
+ .desc_file = TMP_DIR "/s1d1/s1d2/s1d3/f1",
+ .ni_samedir = TMP_DIR "/s1d1_renamed",
+ .expected_ni_write = EACCES,
+ .expected_ni_read = 0,
+ .expected_desc_write = EACCES,
+ .expected_desc_read = 0,
+};
+
+/* NO_INHERIT with RW access expands parent's RO to RW. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(layout1_no_inherit, ro_parent_rw_middle) {
+ /* clang-format on */
+ .ni_path = TMP_DIR "/s1d1/s1d2",
+ .ni_access = ACCESS_RW,
+ .ni_file = TMP_DIR "/s1d1/s1d2/f1",
+ .desc_file = TMP_DIR "/s1d1/s1d2/s1d3/f1",
+ .ni_samedir = TMP_DIR "/s1d1/s1d2_renamed",
+ .expected_ni_write = 0,
+ .expected_ni_read = 0,
+ .expected_desc_write = 0,
+ .expected_desc_read = 0,
+};
+
+/* NO_INHERIT on a file: file gets only its explicit READ_FILE access. */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(layout1_no_inherit, rw_parent_read_file) {
+ /* clang-format on */
+ .ni_path = TMP_DIR "/s1d1/s1d2/f1",
+ .ni_access = LANDLOCK_ACCESS_FS_READ_FILE,
+ .ni_file = TMP_DIR "/s1d1/s1d2/f1",
+ .desc_file = TMP_DIR "/s1d1/s1d2/f2",
+ .ni_samedir = TMP_DIR "/s1d1/s1d2/f1_renamed",
+ .ni_link = TMP_DIR "/s1d1/s1d2/f1_link",
+ .expected_ni_write = EACCES,
+ .expected_ni_read = 0,
+ .expected_desc_write = 0,
+ .expected_desc_read = 0,
+};
+
+TEST_F_FORK(layout1_no_inherit, blocks_inheritance)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = ACCESS_RW,
+ };
+ int ruleset_fd;
+
+ /* RO variants: TMP_DIR gets RO instead of RW. */
+ if (variant->ni_access == ACCESS_RW)
+ ruleset_attr.handled_access_fs |= LANDLOCK_ACCESS_FS_READ_DIR;
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ if (variant->ni_access == ACCESS_RW)
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, TMP_DIR, 0);
+ else
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RW, TMP_DIR, 0);
+
+ add_path_beneath(_metadata, ruleset_fd, variant->ni_access,
+ variant->ni_path, LANDLOCK_ADD_RULE_NO_INHERIT);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ EXPECT_EQ(variant->expected_ni_write,
+ test_open(variant->ni_file, O_WRONLY));
+ EXPECT_EQ(variant->expected_ni_read,
+ test_open(variant->ni_file, O_RDONLY));
+
+ if (strcmp(variant->desc_file, variant->ni_file) != 0) {
+ EXPECT_EQ(variant->expected_desc_write,
+ test_open(variant->desc_file, O_WRONLY));
+ EXPECT_EQ(variant->expected_desc_read,
+ test_open(variant->desc_file, O_RDONLY));
+ }
+}
+
+TEST_F_FORK(layout1_no_inherit, seals_topology)
+{
+ int ruleset_fd;
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = ACCESS_RW | LANDLOCK_ACCESS_FS_REFER |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE |
+ LANDLOCK_ACCESS_FS_REMOVE_DIR,
+ };
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ add_path_beneath(_metadata, ruleset_fd,
+ ACCESS_RW | LANDLOCK_ACCESS_FS_REFER |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE |
+ LANDLOCK_ACCESS_FS_REMOVE_DIR,
+ TMP_DIR, 0);
+ add_path_beneath(_metadata, ruleset_fd, variant->ni_access,
+ variant->ni_path, LANDLOCK_ADD_RULE_NO_INHERIT);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ /* The directory bearing NO_INHERIT cannot be renamed or removed. */
+ ASSERT_EQ(-1, rename(variant->ni_path, TMP_DIR "/ni_renamed"));
+ ASSERT_EQ(EACCES, errno);
+
+ /* A same-directory rename (no REFER needed) is also denied. */
+ ASSERT_EQ(-1, rename(variant->ni_path, variant->ni_samedir));
+ ASSERT_EQ(EACCES, errno);
+
+ /* Hard-linking a sealed file is denied (dirs can't be linked). */
+ if (variant->ni_link) {
+ ASSERT_EQ(-1, link(variant->ni_path, variant->ni_link));
+ ASSERT_EQ(EACCES, errno);
+ }
+
+ /*
+ * Removal is not granted by any variant's NO_INHERIT access, so
+ * unlinking content inside the sealed directory is denied.
+ */
+ ASSERT_EQ(-1, unlink(variant->ni_file));
+ ASSERT_EQ(EACCES, errno);
+
+ /* Unrelated operations outside the sealed branch still work. */
+ ASSERT_EQ(0, unlink(file1_s2d1));
+ ASSERT_EQ(0, mknod(file1_s2d1, S_IFREG | 0700, 0));
+}
+
+TEST_F_FORK(layout1_no_inherit, layered_no_inherit)
+{
+ const __u64 handled = ACCESS_RW | LANDLOCK_ACCESS_FS_REMOVE_FILE |
+ LANDLOCK_ACCESS_FS_REFER;
+ const struct rule layer_rules[] = {
+ {
+ .path = TMP_DIR,
+ .access = handled,
+ },
+ {},
+ };
+ int ruleset_fd;
+
+ /* Layer 1: RW + REFER on TMP_DIR. */
+ ruleset_fd = create_ruleset(_metadata, handled, layer_rules);
+ ASSERT_LE(0, ruleset_fd);
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ /* Layer 2: NO_INHERIT on the target. */
+ ruleset_fd = create_ruleset(_metadata, handled, layer_rules);
+ ASSERT_LE(0, ruleset_fd);
+ add_path_beneath(_metadata, ruleset_fd, variant->ni_access,
+ variant->ni_path, LANDLOCK_ADD_RULE_NO_INHERIT);
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ ASSERT_EQ(-1, rename(variant->ni_path, TMP_DIR "/ni_renamed_layered"));
+ ASSERT_EQ(EACCES, errno);
+
+ /* Content at NI path respects the NO_INHERIT access from layer 2. */
+ EXPECT_EQ(variant->expected_ni_write,
+ test_open(variant->ni_file, O_WRONLY));
+ EXPECT_EQ(variant->expected_ni_read,
+ test_open(variant->ni_file, O_RDONLY));
+}
+
TEST_F_FORK(layout0, max_layers)
{
int i, err;
@@ -5571,6 +5803,25 @@ FIXTURE_VARIANT(layout4_disconnected_leafs)
const int expected_exchange_result;
/* Expected result of the call to renameat([fd:s1d42]/f4, [fd:s1d42]/f5). */
const int expected_same_dir_rename_result;
+
+ /*
+ * If true, a NO_INHERIT rule is set on s1d41 (via the bind mount
+ * at s2d2). Used by the no_inherit_mount test.
+ */
+ bool no_inherit_on_s1d41;
+ /*
+ * Access rights used for the optional NO_INHERIT rule on s1d41.
+ */
+ const __u64 no_inherit_access;
+ /*
+ * Expected result of renaming s1d31 (parent of s1d41 within the
+ * mount) when no_inherit_on_s1d41 is set.
+ */
+ const int expected_parent_rename;
+ /*
+ * Expected result of rmdir on s1d31, when no_inherit_on_s1d41 is set.
+ */
+ const int expected_parent_rmdir;
};
/* clang-format off */
@@ -5823,6 +6074,26 @@ FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, f1_f2_f3) {
.expected_exchange_result = EACCES,
};
+/*
+ * NO_INHERIT variant: s1d41 is protected with ACCESS_RO via the bind mount.
+ * Parents within the mount are sealed against topology changes.
+ */
+/* clang-format off */
+FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, no_inherit_mount) {
+ /* clang-format on */
+ .allowed_f1 = LANDLOCK_ACCESS_FS_READ_FILE,
+ .allowed_f2 = LANDLOCK_ACCESS_FS_READ_FILE,
+ .allowed_f3 = LANDLOCK_ACCESS_FS_READ_FILE,
+ .expected_read_result = 0,
+ .expected_rename_result = EACCES,
+ .expected_exchange_result = EACCES,
+ .expected_same_dir_rename_result = EACCES,
+ .no_inherit_on_s1d41 = true,
+ .no_inherit_access = ACCESS_RO,
+ .expected_parent_rename = EACCES,
+ .expected_parent_rmdir = EACCES,
+};
+
TEST_F_FORK(layout4_disconnected_leafs, read_rename_exchange)
{
const __u64 handled_access =
@@ -5931,6 +6202,70 @@ TEST_F_FORK(layout4_disconnected_leafs, read_rename_exchange)
test_renameat(s1d42_bind_fd, "f4", s1d42_bind_fd, "f5"));
}
+/*
+ * When s1d41 (accessed via the bind mount at s2d2) is protected with
+ * NO_INHERIT, its parent directories within the mount are sealed from
+ * topology changes. Other variants do not exercise NO_INHERIT and skip
+ * this test.
+ */
+TEST_F_FORK(layout4_disconnected_leafs, no_inherit_seals_mount)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = ACCESS_RW | LANDLOCK_ACCESS_FS_REFER |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE |
+ LANDLOCK_ACCESS_FS_REMOVE_DIR,
+ };
+ int ruleset_fd, s1d41_bind_fd;
+
+ if (!variant->no_inherit_on_s1d41)
+ SKIP(return, "variant does not set NO_INHERIT on s1d41");
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ add_path_beneath(_metadata, ruleset_fd,
+ ACCESS_RW | LANDLOCK_ACCESS_FS_REFER |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE |
+ LANDLOCK_ACCESS_FS_REMOVE_DIR,
+ TMP_DIR, 0);
+
+ s1d41_bind_fd = open(TMP_DIR "/s2d1/s2d2/s1d31/s1d41",
+ O_DIRECTORY | O_PATH | O_CLOEXEC);
+ ASSERT_LE(0, s1d41_bind_fd);
+
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &(struct landlock_path_beneath_attr){
+ .parent_fd = s1d41_bind_fd,
+ .allowed_access =
+ variant->no_inherit_access,
+ },
+ LANDLOCK_ADD_RULE_NO_INHERIT));
+ EXPECT_EQ(0, close(s1d41_bind_fd));
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ /* Parent of s1d41 within the mount is sealed. */
+ ASSERT_EQ(-1, rmdir(TMP_DIR "/s2d1/s2d2/s1d31"));
+ ASSERT_EQ(variant->expected_parent_rmdir, errno);
+
+ ASSERT_EQ(-1, rename(TMP_DIR "/s2d1/s2d2/s1d31",
+ TMP_DIR "/s2d1/s2d2/s1d31_renamed"));
+ ASSERT_EQ(variant->expected_parent_rename, errno);
+
+ /* Sibling directories outside the sealed chain are free. */
+ ASSERT_EQ(0, rename(TMP_DIR "/s2d1/s2d2/s1d32",
+ TMP_DIR "/s2d1/s2d2/s1d32_renamed"));
+ ASSERT_EQ(0, rename(TMP_DIR "/s2d1/s2d2/s1d32_renamed",
+ TMP_DIR "/s2d1/s2d2/s1d32"));
+
+ /* The mount source parent hierarchy is also sealed. */
+ ASSERT_EQ(-1, rename(TMP_DIR "/s1d1/s1d2/s1d31",
+ TMP_DIR "/s1d1/s1d2/s1d31_renamed"));
+ ASSERT_EQ(variant->expected_parent_rename, errno);
+}
+
/*
* layout5_disconnected_branch before rename:
*
@@ -7358,6 +7693,96 @@ TEST_F(audit_layout1, write_file)
EXPECT_EQ(1, records.domain);
}
+FIXTURE(audit_no_inherit)
+{
+ struct audit_filter audit_filter;
+ int audit_fd;
+};
+
+FIXTURE_SETUP(audit_no_inherit)
+{
+ prepare_layout(_metadata);
+ create_layout1(_metadata);
+
+ set_cap(_metadata, CAP_AUDIT_CONTROL);
+ self->audit_fd = audit_init_with_exe_filter(&self->audit_filter);
+ EXPECT_LE(0, self->audit_fd);
+ clear_cap(_metadata, CAP_AUDIT_CONTROL);
+}
+
+FIXTURE_TEARDOWN_PARENT(audit_no_inherit)
+{
+ remove_layout1(_metadata);
+ cleanup_layout(_metadata);
+
+ EXPECT_EQ(0, audit_cleanup(-1, NULL));
+}
+
+FIXTURE_VARIANT(audit_no_inherit)
+{
+ bool parent_quiet;
+ const char *test_path;
+ bool expect_audit_log;
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(audit_no_inherit, blocks_quiet_inheritance) {
+ /* clang-format on */
+ .parent_quiet = true,
+ .test_path = TMP_DIR "/s1d1/s1d2/s1d3/f1",
+ .expect_audit_log = true,
+};
+
+/* clang-format off */
+FIXTURE_VARIANT_ADD(audit_no_inherit, quiet_parent) {
+ /* clang-format on */
+ .parent_quiet = true,
+ .test_path = TMP_DIR "/s1d1/f1",
+ .expect_audit_log = false,
+};
+
+TEST_F(audit_no_inherit, no_inherit_audit)
+{
+ struct audit_records records;
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = ACCESS_RW,
+ .quiet_access_fs = variant->parent_quiet ? ACCESS_RW : 0,
+ };
+ int ruleset_fd;
+
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ if (variant->parent_quiet)
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d1,
+ LANDLOCK_ADD_RULE_QUIET);
+ else
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d1, 0);
+
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_RO, dir_s1d3,
+ LANDLOCK_ADD_RULE_NO_INHERIT);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+
+ EXPECT_EQ(EACCES, test_open(variant->test_path, O_WRONLY));
+ if (variant->expect_audit_log) {
+ EXPECT_EQ(0, matches_log_fs(_metadata, self->audit_fd,
+ "fs\\.write_file",
+ variant->test_path));
+ } else {
+ EXPECT_NE(0, matches_log_fs(_metadata, self->audit_fd,
+ "fs\\.write_file",
+ variant->test_path));
+ }
+
+ EXPECT_EQ(0, audit_count_records(self->audit_fd, &records));
+ EXPECT_EQ(0, records.access);
+ EXPECT_EQ(variant->expect_audit_log ? 1 : 0, records.domain);
+
+ EXPECT_EQ(0, close(ruleset_fd));
+}
+
TEST_F(audit_layout1, read_file)
{
struct audit_records records;
--
2.54.0
^ permalink raw reply related
* [PATCH v9 7/9] samples/landlock: Add LANDLOCK_ADD_RULE_NO_INHERIT to landlock-sandboxer
From: Justin Suess @ 2026-06-21 3:52 UTC (permalink / raw)
To: linux-security-module, mic; +Cc: m, gnoack, gnoack3000, matthieu, Justin Suess
In-Reply-To: <20260621035223.2651547-1-utilityemal77@gmail.com>
Add a new LL_FS_NO_INHERIT environment variable to the sandboxer.
Paths listed in it are added with the LANDLOCK_ADD_RULE_NO_INHERIT
flag, demonstrating how to set up a parent directory with broader
access than its children.
The flag is silently skipped on kernels older than ABI 11.
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
Changes since v8:
- Added an explicit ABI 10 case to the sandboxer's downgrade switch so
the NO_INHERIT flag is stripped on kernels with ABI < 11.
- Updated for the merged quiet-flag env-var renames, LANDLOCK_ABI_LAST,
and help text (ABI >= 11).
- Rebased onto mic/next.
samples/landlock/sandboxer.c | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
index ac71019e6212..80c2120d4171 100644
--- a/samples/landlock/sandboxer.c
+++ b/samples/landlock/sandboxer.c
@@ -59,6 +59,7 @@ static inline int landlock_restrict_self(const int ruleset_fd,
#define ENV_FS_RO_NAME "LL_FS_RO"
#define ENV_FS_RW_NAME "LL_FS_RW"
#define ENV_FS_QUIET_NAME "LL_FS_QUIET"
+#define ENV_FS_NO_INHERIT_NAME "LL_FS_NO_INHERIT"
#define ENV_TCP_BIND_NAME "LL_TCP_BIND"
#define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT"
#define ENV_NET_QUIET_NAME "LL_NET_QUIET"
@@ -369,7 +370,7 @@ static int add_quiet_access(const char *const env_var,
return 0;
}
-#define LANDLOCK_ABI_LAST 10
+#define LANDLOCK_ABI_LAST 11
#define XSTR(s) #s
#define STR(s) XSTR(s)
@@ -405,6 +406,7 @@ static const char help[] =
"but to test audit we can set " ENV_FORCE_LOG_NAME "=1\n"
ENV_FS_QUIET_NAME " and " ENV_NET_QUIET_NAME ", both optional, can then be used "
"to make access to some denied paths or network ports not trigger audit logging.\n"
+ ENV_FS_NO_INHERIT_NAME " can be used to suppress access right propagation (ABI >= 11).\n"
ENV_QUIET_ACCESS_NAME " can be used to specify which accesses should be quieted "
"(required when " ENV_FS_QUIET_NAME " or " ENV_NET_QUIET_NAME " is set):\n"
" - \"all\" to quiet all of the accesses below\n"
@@ -453,6 +455,7 @@ int main(const int argc, char *const argv[], char *const *const envp)
.quiet_scoped = 0,
};
bool quiet_supported = true;
+ bool no_inherit_supported = true;
int supported_restrict_flags = LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON;
int set_restrict_flags = 0;
@@ -545,6 +548,10 @@ int main(const int argc, char *const argv[], char *const *const envp)
LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
/* Removes quiet flags for ABI < 10 later on. */
quiet_supported = false;
+ __attribute__((fallthrough));
+ case 10:
+ /* Removes no_inherit flag for ABI < 11 later on. */
+ no_inherit_supported = false;
/* Must be printed for any ABI < LANDLOCK_ABI_LAST. */
fprintf(stderr,
@@ -649,6 +656,13 @@ int main(const int argc, char *const argv[], char *const *const envp)
goto err_close_ruleset;
}
+ /* Don't require this env to be present. */
+ if (no_inherit_supported && getenv(ENV_FS_NO_INHERIT_NAME)) {
+ if (populate_ruleset_fs(ENV_FS_NO_INHERIT_NAME, ruleset_fd, 0,
+ LANDLOCK_ADD_RULE_NO_INHERIT))
+ goto err_close_ruleset;
+ }
+
if (populate_ruleset_net(ENV_TCP_BIND_NAME, ruleset_fd,
LANDLOCK_ACCESS_NET_BIND_TCP, 0)) {
goto err_close_ruleset;
--
2.54.0
^ permalink raw reply related
* [PATCH v9 6/9] landlock: Add documentation for LANDLOCK_ADD_RULE_NO_INHERIT
From: Justin Suess @ 2026-06-21 3:52 UTC (permalink / raw)
To: linux-security-module, mic; +Cc: m, gnoack, gnoack3000, matthieu, Justin Suess
In-Reply-To: <20260621035223.2651547-1-utilityemal77@gmail.com>
Add documentation of the flag to the userspace API, describing the
functionality of the flag and parent directory protections.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
Changes since v8:
- Expanded the userspace-api documentation: the conservative seal
(same-directory renames and hard links denied; rules keyed by inode),
the best-effort ancestor walk, guidance to discard a partially applied
policy on error, and the threat-model paragraph.
- Updated the ABI references to version 11.
- Rebased onto mic/next.
Documentation/userspace-api/landlock.rst | 44 ++++++++++++++++++++++++
1 file changed, 44 insertions(+)
diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
index 5a63d4476c1c..01623e0ab95d 100644
--- a/Documentation/userspace-api/landlock.rst
+++ b/Documentation/userspace-api/landlock.rst
@@ -789,6 +789,50 @@ when at least one sys_landlock_add_rule() call is made for it with the
``LANDLOCK_ADD_RULE_QUIET`` flag, additional add-rule calls for the same
object without this flag do not clear it.
+Filesystem inheritance suppression (ABI < 11)
+---------------------------------------------
+
+Starting with the Landlock ABI version 11, it is possible to prevent a
+directory or file from inheriting its parent's access grants by using the
+``LANDLOCK_ADD_RULE_NO_INHERIT`` flag passed to sys_landlock_add_rule().
+This is useful for policies where a parent directory needs broader access
+than its children.
+
+To mitigate sandbox-restart attacks, the tagged inode and all of its
+ancestors up to the VFS root cannot be removed, renamed, reparented, or
+linked into or out of other directories.
+
+This seal is intentionally conservative: every rename, removal or link
+operation that targets a sealed inode is denied, including same-directory
+renames and hard links that do not change the inode's parent. Landlock rules
+are keyed by inode, so such operations could not by themselves bypass a seal,
+but denying them as well keeps enforcement simple and leaves no edge cases
+that could weaken the guarantee.
+
+Inheritance of access grants from descendants of an inode tagged with
+``LANDLOCK_ADD_RULE_NO_INHERIT`` is unaffected: such descendants continue
+to inherit from the tagged inode normally, unless they also carry this
+flag.
+
+Because sealing an inode also seals all of its ancestors, the kernel walks
+the path up to the VFS root while adding such a rule, sealing each ancestor in
+turn. This walk is best effort: it is not serialized against concurrent
+renames, so a rename that reparents one of the ancestors while the walk is in
+progress may leave the seal incomplete.
+
+Similarly, if sys_landlock_add_rule() returns an error while adding a
+``LANDLOCK_ADD_RULE_NO_INHERIT`` rule (for example because of memory
+pressure), the ruleset may have been left with the rule's object and only some
+of its ancestors sealed. Such a ruleset should be discarded rather than
+enforced.
+
+This is not a security concern. Changes to the filesystem hierarchy between
+the time a ruleset is built and the time it is enforced are outside of
+Landlock's threat model: a ruleset only describes the restrictions that take
+effect once it is enforced, and what happens to the hierarchy beforehand is
+not controlled by Landlock. Once enforced, the seals that were established
+deny the topology changes they cover.
+
.. _kernel_support:
Kernel support
--
2.54.0
^ permalink raw reply related
* [PATCH v9 5/9] landlock: Implement LANDLOCK_ADD_RULE_NO_INHERIT
From: Justin Suess @ 2026-06-21 3:52 UTC (permalink / raw)
To: linux-security-module, mic; +Cc: m, gnoack, gnoack3000, matthieu, Justin Suess
In-Reply-To: <20260621035223.2651547-1-utilityemal77@gmail.com>
Make %LANDLOCK_ADD_RULE_NO_INHERIT actually enforce its semantics:
- Tag the new rule's layer with @no_inherit and
@has_no_inherit_descendant so landlock_unmask_layers() stops walking
up the hierarchy once it has been seen, and so the rule's own object
is sealed against topology changes.
- Walk from the rule's path up to the VFS root in
landlock_append_fs_rule(), inserting a zero-access rule on each
ancestor with @has_no_inherit_descendant set, so topology changes
(rename, rmdir, link, ...) on any ancestor are denied too.
- Add deny_no_inherit_topology_change(), called from
current_check_refer_path(), hook_path_unlink() and hook_path_rmdir()
to enforce the seal at the LSM hook layer.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
Changes since v8:
- Extracted the ancestor-sealing walk out of landlock_append_fs_rule()
into a new seal_ancestors() helper, removing the out_unlock goto.
- The seal loop now handles LANDLOCK_WALK_INTERNAL explicitly (skips
disconnected internal-mount roots and keeps walking), matching the
updated landlock_walk_path_up() behavior.
- Broadened current_check_refer_path() enforcement:
deny_no_inherit_topology_change() is now applied to old_dentry and
(when present) new_dentry unconditionally, covering hard links (link)
in addition to rename/exchange.
- Factored the unlink/rmdir hooks into a new current_check_remove()
helper instead of duplicating the subject lookup and deny call.
- Simplified deny_no_inherit_topology_change() to short-circuit and log
on the first sealed layer, dropping the sealed_layers accumulator and
the redundant no_inherit test.
- Bumped the Landlock ABI to version 11 and updated the base_test
abi_version expectation.
- Rebased onto mic/next.
security/landlock/access.h | 6 +
security/landlock/fs.c | 156 ++++++++++++++++++-
security/landlock/ruleset.c | 30 +++-
security/landlock/ruleset.h | 13 ++
security/landlock/syscalls.c | 2 +-
tools/testing/selftests/landlock/base_test.c | 2 +-
6 files changed, 202 insertions(+), 7 deletions(-)
diff --git a/security/landlock/access.h b/security/landlock/access.h
index d926078bf0a5..9df6c6de71e2 100644
--- a/security/landlock/access.h
+++ b/security/landlock/access.h
@@ -81,6 +81,12 @@ struct layer_mask {
*/
access_mask_t quiet : 1;
#endif /* CONFIG_AUDIT */
+ /**
+ * @no_inherit: Whether we have encountered a rule with the no-inherit
+ * flag for this layer, so that ancestor rules do not grant additional
+ * access rights.
+ */
+ access_mask_t no_inherit : 1;
} __packed __aligned(sizeof(access_mask_t));
/*
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 34d1c245af92..44ccf6cede85 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -362,6 +362,70 @@ static enum landlock_walk_result landlock_walk_path_up(struct path *const path)
return LANDLOCK_WALK_CONTINUE;
}
+/**
+ * seal_ancestors - Seal every ancestor of @walker up to the VFS root
+ *
+ * @ruleset: Ruleset (must be locked by the caller) to insert the seal rules
+ * into.
+ * @walker: Path to the inode whose ancestors must be sealed. It is walked up
+ * to the real VFS root and is modified during the walk.
+ *
+ * Inserts a no-access rule tagged @has_no_inherit_descendant for each ancestor
+ * so that topology-changing operations (rename, rmdir, link, ...) on them are
+ * denied.
+ *
+ * On failure the walk stops early, so the ruleset may be left with the leaf
+ * rule and only some of its ancestors sealed. The caller must therefore
+ * discard the ruleset on error rather than enforce it, since an enforced but
+ * partially sealed ruleset would provide a weaker guarantee than intended.
+ *
+ * Return: 0 on success or a negative error code on failure.
+ */
+static int seal_ancestors(struct landlock_ruleset *const ruleset,
+ struct path *const walker)
+{
+ int err = 0;
+
+ path_get(walker);
+ for (;;) {
+ struct landlock_rule *ancestor_rule;
+ struct landlock_id ancestor_id = {
+ .type = LANDLOCK_KEY_INODE,
+ };
+ const enum landlock_walk_result res =
+ landlock_walk_path_up(walker);
+
+ /* Done once the real VFS root has been reached. */
+ if (res == LANDLOCK_WALK_STOP_REAL_ROOT)
+ break;
+ /*
+ * @walker advanced past the disconnected root of an internal
+ * mount, which does not name a sealable ancestor; keep walking
+ * up so that every ancestor up to the real root is still
+ * sealed.
+ */
+ if (res == LANDLOCK_WALK_INTERNAL)
+ continue;
+
+ /* LANDLOCK_WALK_CONTINUE: seal this ancestor. */
+ ancestor_id.key.object =
+ get_inode_object(d_backing_inode(walker->dentry));
+ if (IS_ERR(ancestor_id.key.object)) {
+ err = PTR_ERR(ancestor_id.key.object);
+ break;
+ }
+ ancestor_rule = landlock_insert_rule(ruleset, ancestor_id, 0, 0);
+ landlock_put_object(ancestor_id.key.object);
+ if (IS_ERR(ancestor_rule)) {
+ err = PTR_ERR(ancestor_rule);
+ break;
+ }
+ ancestor_rule->layers[0].flags.has_no_inherit_descendant = true;
+ }
+ path_put(walker);
+ return err;
+}
+
/*
* @path: Should have been checked by get_path_from_fd().
*/
@@ -391,6 +455,20 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
mutex_lock(&ruleset->lock);
rule = landlock_insert_rule(ruleset, id, access_rights, flags);
err = PTR_ERR_OR_ZERO(rule);
+ /*
+ * Sealing an inode also seals all of its ancestors against topology
+ * changes, so walk the path up to the VFS root and seal each ancestor
+ * too. This is best effort: a concurrent rename during the walk may
+ * leave the seal incomplete. Such changes to the hierarchy between
+ * ruleset construction and enforcement are outside of Landlock's
+ * threat model. See the "Filesystem inheritance suppression" section
+ * in Documentation/userspace-api/landlock.rst for the limitations.
+ */
+ if (!err && (flags & LANDLOCK_ADD_RULE_NO_INHERIT)) {
+ struct path walker = *path;
+
+ err = seal_ancestors(ruleset, &walker);
+ }
mutex_unlock(&ruleset->lock);
/*
* No need to check for an error because landlock_insert_rule()
@@ -1129,6 +1207,47 @@ log_fs_change_topology_dentry(const struct landlock_cred_security *const subject
});
}
+/**
+ * deny_no_inherit_topology_change - Deny topology changes on sealed paths
+ * @subject: Subject performing the operation.
+ * @dentry: Target of the topology modification.
+ *
+ * Return: -EACCES (and emits an audit record) if any of the subject's
+ * domain layers seal @dentry against topology changes: either @dentry
+ * itself has a %LANDLOCK_ADD_RULE_NO_INHERIT rule, or one of its
+ * descendants does (recorded via @has_no_inherit_descendant on the
+ * dentry's rule). Returns 0 otherwise.
+ */
+static int
+deny_no_inherit_topology_change(const struct landlock_cred_security *subject,
+ struct dentry *const dentry)
+{
+ const struct landlock_rule *rule;
+
+ if (WARN_ON_ONCE(!subject || !dentry || d_is_negative(dentry)))
+ return 0;
+
+ rule = find_rule(subject->domain, dentry);
+ if (!rule)
+ return 0;
+
+ for (size_t i = 0; i < rule->num_layers; i++) {
+ const struct landlock_layer *const layer = &rule->layers[i];
+
+ /*
+ * @has_no_inherit_descendant is a superset of @no_inherit: it is
+ * set on the rule's own object when the no-inherit rule is
+ * created, and on every ancestor. Testing it alone seals both.
+ */
+ if (layer->flags.has_no_inherit_descendant) {
+ log_fs_change_topology_dentry(subject, layer->level - 1,
+ dentry);
+ return -EACCES;
+ }
+ }
+ return 0;
+}
+
/**
* current_check_refer_path - Check if a rename or link action is allowed
*
@@ -1194,6 +1313,7 @@ static int current_check_refer_path(struct dentry *const old_dentry,
struct path old_parent_path;
struct layer_masks layer_masks_parent1 = {}, layer_masks_parent2 = {};
struct landlock_request request1 = {}, request2 = {};
+ int err;
if (!subject)
return 0;
@@ -1210,6 +1330,22 @@ static int current_check_refer_path(struct dentry *const old_dentry,
}
access_request_parent2 =
get_mode_access(d_backing_inode(old_dentry)->i_mode);
+ /*
+ * A no-inherit seal forbids any topology change of the sealed inode or
+ * its ancestors. Deny renaming or linking the source out of its
+ * hierarchy, as well as removing, overwriting or exchanging a sealed
+ * destination. This applies to both rename (@removable) and link
+ * (!@removable) operations, so it is checked unconditionally.
+ */
+ err = deny_no_inherit_topology_change(subject, old_dentry);
+ if (err)
+ return err;
+ if (!d_is_negative(new_dentry)) {
+ err = deny_no_inherit_topology_change(subject, new_dentry);
+ if (err)
+ return err;
+ }
+
if (removable) {
access_request_parent1 |= maybe_remove(old_dentry);
access_request_parent2 |= maybe_remove(new_dentry);
@@ -1586,16 +1722,32 @@ static int hook_path_symlink(const struct path *const dir,
return current_check_access_path(dir, LANDLOCK_ACCESS_FS_MAKE_SYM);
}
+static int current_check_remove(const struct path *const dir,
+ struct dentry *const dentry,
+ const access_mask_t access_request)
+{
+ const struct landlock_cred_security *const subject =
+ landlock_get_applicable_subject(current_cred(), any_fs, NULL);
+
+ if (subject) {
+ int err = deny_no_inherit_topology_change(subject, dentry);
+
+ if (err)
+ return err;
+ }
+ return current_check_access_path(dir, access_request);
+}
+
static int hook_path_unlink(const struct path *const dir,
struct dentry *const dentry)
{
- return current_check_access_path(dir, LANDLOCK_ACCESS_FS_REMOVE_FILE);
+ return current_check_remove(dir, dentry, LANDLOCK_ACCESS_FS_REMOVE_FILE);
}
static int hook_path_rmdir(const struct path *const dir,
struct dentry *const dentry)
{
- return current_check_access_path(dir, LANDLOCK_ACCESS_FS_REMOVE_DIR);
+ return current_check_remove(dir, dentry, LANDLOCK_ACCESS_FS_REMOVE_DIR);
}
static int hook_path_truncate(const struct path *const path)
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index b8a35675bcbf..ca7cfa45c90a 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -258,6 +258,10 @@ insert_rule(struct landlock_ruleset *const ruleset,
return ERR_PTR(-EINVAL);
this->layers[0].access |= (*layers)[0].access;
this->layers[0].flags.quiet |= (*layers)[0].flags.quiet;
+ this->layers[0].flags.no_inherit |=
+ (*layers)[0].flags.no_inherit;
+ this->layers[0].flags.has_no_inherit_descendant |=
+ (*layers)[0].flags.has_no_inherit_descendant;
return this;
}
@@ -311,12 +315,20 @@ landlock_insert_rule(struct landlock_ruleset *const ruleset,
const struct landlock_id id,
const access_mask_t access, const u32 flags)
{
+ const bool no_inherit = !!(flags & LANDLOCK_ADD_RULE_NO_INHERIT);
struct landlock_layer layers[] = { {
.access = access,
/* When @level is zero, insert_rule() extends @ruleset. */
.level = 0,
.flags = {
.quiet = !!(flags & LANDLOCK_ADD_RULE_QUIET),
+ .no_inherit = no_inherit,
+ /*
+ * The rule's own object is also sealed against
+ * topology changes, so mark it as if it had a
+ * no-inherit descendant.
+ */
+ .has_no_inherit_descendant = no_inherit,
},
} };
@@ -657,15 +669,25 @@ bool landlock_unmask_layers(const struct landlock_rule *const rule,
*/
for (size_t i = 0; i < rule->num_layers; i++) {
const struct landlock_layer *const layer = &rule->layers[i];
+ struct layer_mask *const layer_mask =
+ &masks->layers[layer->level - 1];
+
+ /*
+ * Skip layers that already have no_inherit set: these layers
+ * should not inherit access rights from ancestor directories.
+ */
+ if (layer_mask->no_inherit)
+ continue;
/* Clear the bits where the layer in the rule grants access. */
- masks->layers[layer->level - 1].access &= ~layer->access;
+ layer_mask->access &= ~layer->access;
#ifdef CONFIG_AUDIT
- /* Collect rule flags for each layer. */
if (layer->flags.quiet)
- masks->layers[layer->level - 1].quiet = true;
+ layer_mask->quiet = true;
#endif /* CONFIG_AUDIT */
+ if (layer->flags.no_inherit)
+ layer_mask->no_inherit = true;
}
for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) {
@@ -731,6 +753,7 @@ landlock_init_layer_masks(const struct landlock_ruleset *const domain,
#ifdef CONFIG_AUDIT
masks->layers[i].quiet = false;
#endif /* CONFIG_AUDIT */
+ masks->layers[i].no_inherit = false;
}
for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->layers);
i++) {
@@ -738,6 +761,7 @@ landlock_init_layer_masks(const struct landlock_ruleset *const domain,
#ifdef CONFIG_AUDIT
masks->layers[i].quiet = false;
#endif /* CONFIG_AUDIT */
+ masks->layers[i].no_inherit = false;
}
return handled_accesses;
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index c927bcb82fa3..8d3dd551bf77 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -40,6 +40,19 @@ struct landlock_layer {
* down the file hierarchy.
*/
u8 quiet : 1;
+ /**
+ * @no_inherit: Prevents this rule from inheriting access rights
+ * from ancestor inodes. Only used for filesystem rules; set
+ * via %LANDLOCK_ADD_RULE_NO_INHERIT.
+ */
+ u8 no_inherit : 1;
+ /**
+ * @has_no_inherit_descendant: Marker used to deny topology
+ * changes on the rule's object: either the object itself has
+ * a no-inherit rule, or a descendant does. Only used for
+ * filesystem rules; set by Landlock, never by user space.
+ */
+ u8 has_no_inherit_descendant : 1;
} flags;
/**
* @access: Bitfield of allowed actions on the kernel object. They are
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index b847b0be1cf7..3fea5492df0d 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -169,7 +169,7 @@ static const struct file_operations ruleset_fops = {
* If the change involves a fix that requires userspace awareness, also update
* the errata documentation in Documentation/userspace-api/landlock.rst .
*/
-const int landlock_abi_version = 10;
+const int landlock_abi_version = 11;
/**
* sys_landlock_create_ruleset - Create a new ruleset
diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
index cbd3c1669951..b8b5fa1042ba 100644
--- a/tools/testing/selftests/landlock/base_test.c
+++ b/tools/testing/selftests/landlock/base_test.c
@@ -76,7 +76,7 @@ TEST(abi_version)
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
};
- ASSERT_EQ(10, landlock_create_ruleset(NULL, 0,
+ ASSERT_EQ(11, landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION));
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
--
2.54.0
^ permalink raw reply related
* [PATCH v9 4/9] landlock: Move log_fs_change_topology_dentry() above current_check_refer_path()
From: Justin Suess @ 2026-06-21 3:52 UTC (permalink / raw)
To: linux-security-module, mic; +Cc: m, gnoack, gnoack3000, matthieu, Justin Suess
In-Reply-To: <20260621035223.2651547-1-utilityemal77@gmail.com>
In preparation for a new caller (the no-inherit topology-change check)
that sits earlier in fs.c, move log_fs_change_topology_dentry() above
current_check_refer_path() so that caller does not need a forward
declaration. Reflow its signature to match log_fs_change_topology_path()
while moving it.
No functional change intended.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
New patch in v9.
Splits the code motion out of the implementation patch: moves
log_fs_change_topology_dentry() above current_check_refer_path() so the
new no-inherit topology-change check does not need a forward
declaration. No functional change.
security/landlock/fs.c | 28 ++++++++++++++--------------
1 file changed, 14 insertions(+), 14 deletions(-)
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index fd829e06835d..34d1c245af92 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -1115,6 +1115,20 @@ collect_domain_accesses(const struct landlock_ruleset *const domain,
return ret;
}
+static void
+log_fs_change_topology_dentry(const struct landlock_cred_security *const subject,
+ size_t handle_layer, struct dentry *const dentry)
+{
+ landlock_log_denial(subject, &(struct landlock_request) {
+ .type = LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY,
+ .audit = {
+ .type = LSM_AUDIT_DATA_DENTRY,
+ .u.dentry = dentry,
+ },
+ .layer_plus_one = handle_layer + 1,
+ });
+}
+
/**
* current_check_refer_path - Check if a rename or link action is allowed
*
@@ -1427,20 +1441,6 @@ log_fs_change_topology_path(const struct landlock_cred_security *const subject,
});
}
-static void log_fs_change_topology_dentry(
- const struct landlock_cred_security *const subject, size_t handle_layer,
- struct dentry *const dentry)
-{
- landlock_log_denial(subject, &(struct landlock_request) {
- .type = LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY,
- .audit = {
- .type = LSM_AUDIT_DATA_DENTRY,
- .u.dentry = dentry,
- },
- .layer_plus_one = handle_layer + 1,
- });
-}
-
/*
* Because a Landlock security policy is defined according to the filesystem
* topology (i.e. the mount namespace), changing it may grant access to files
--
2.54.0
^ permalink raw reply related
* [PATCH v9 3/9] landlock: Return inserted rule from landlock_insert_rule()
From: Justin Suess @ 2026-06-21 3:52 UTC (permalink / raw)
To: linux-security-module, mic; +Cc: m, gnoack, gnoack3000, matthieu, Justin Suess
In-Reply-To: <20260621035223.2651547-1-utilityemal77@gmail.com>
Change insert_rule() and landlock_insert_rule() to return the inserted
(or updated) struct landlock_rule pointer instead of an int errno.
Errors are propagated via ERR_PTR().
This gives callers a handle on the resulting rule so a subsequent change
can mutate per-layer flags on it (e.g. to mark ancestor rules created
for no-inherit topology sealing).
No functional change intended.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Notes:
Changes since v8:
- Simplified error propagation with PTR_ERR_OR_ZERO() in
landlock_append_fs_rule() and landlock_append_net_rule(), replacing
the open-coded IS_ERR()/PTR_ERR() handling.
- Rebased onto mic/next (the flags parameter is now u32).
security/landlock/fs.c | 6 ++--
security/landlock/net.c | 6 ++--
security/landlock/ruleset.c | 68 ++++++++++++++++++-------------------
security/landlock/ruleset.h | 7 ++--
4 files changed, 45 insertions(+), 42 deletions(-)
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 5b9cc450d614..fd829e06835d 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -369,7 +369,8 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
const struct path *const path,
access_mask_t access_rights, const u32 flags)
{
- int err;
+ int err = 0;
+ struct landlock_rule *rule;
struct landlock_id id = {
.type = LANDLOCK_KEY_INODE,
};
@@ -388,7 +389,8 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
if (IS_ERR(id.key.object))
return PTR_ERR(id.key.object);
mutex_lock(&ruleset->lock);
- err = landlock_insert_rule(ruleset, id, access_rights, flags);
+ rule = landlock_insert_rule(ruleset, id, access_rights, flags);
+ err = PTR_ERR_OR_ZERO(rule);
mutex_unlock(&ruleset->lock);
/*
* No need to check for an error because landlock_insert_rule()
diff --git a/security/landlock/net.c b/security/landlock/net.c
index cbff59ec3aba..88b9ffcd11fb 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -23,11 +23,11 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
const u16 port, access_mask_t access_rights,
const u32 flags)
{
- int err;
const struct landlock_id id = {
.key.data = (__force uintptr_t)htons(port),
.type = LANDLOCK_KEY_NET_PORT,
};
+ struct landlock_rule *rule;
BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data));
@@ -36,10 +36,10 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
~landlock_get_net_access_mask(ruleset, 0);
mutex_lock(&ruleset->lock);
- err = landlock_insert_rule(ruleset, id, access_rights, flags);
+ rule = landlock_insert_rule(ruleset, id, access_rights, flags);
mutex_unlock(&ruleset->lock);
- return err;
+ return PTR_ERR_OR_ZERO(rule);
}
static int current_check_access_socket(struct socket *const sock,
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 4dd09ea22c84..b8a35675bcbf 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -203,12 +203,13 @@ static void build_check_ruleset(void)
* added to @ruleset as new constraints, similarly to a boolean AND between
* access rights.
*
- * Return: 0 on success, -errno on failure.
+ * Return: A pointer to the inserted or updated rule, or an ERR_PTR on failure.
*/
-static int insert_rule(struct landlock_ruleset *const ruleset,
- const struct landlock_id id,
- const struct landlock_layer (*layers)[],
- const size_t num_layers)
+static struct landlock_rule *
+insert_rule(struct landlock_ruleset *const ruleset,
+ const struct landlock_id id,
+ const struct landlock_layer (*layers)[],
+ const size_t num_layers)
{
struct rb_node **walker_node;
struct rb_node *parent_node = NULL;
@@ -218,14 +219,14 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
might_sleep();
lockdep_assert_held(&ruleset->lock);
if (WARN_ON_ONCE(!layers))
- return -ENOENT;
+ return ERR_PTR(-ENOENT);
if (is_object_pointer(id.type) && WARN_ON_ONCE(!id.key.object))
- return -ENOENT;
+ return ERR_PTR(-ENOENT);
root = get_root(ruleset, id.type);
if (IS_ERR(root))
- return PTR_ERR(root);
+ return ERR_CAST(root);
walker_node = &root->rb_node;
while (*walker_node) {
@@ -243,7 +244,7 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
/* Only a single-level layer should match an existing rule. */
if (WARN_ON_ONCE(num_layers != 1))
- return -EINVAL;
+ return ERR_PTR(-EINVAL);
/* If there is a matching rule, updates it. */
if ((*layers)[0].level == 0) {
@@ -252,16 +253,16 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
* landlock_add_rule(2), i.e. @ruleset is not a domain.
*/
if (WARN_ON_ONCE(this->num_layers != 1))
- return -EINVAL;
+ return ERR_PTR(-EINVAL);
if (WARN_ON_ONCE(this->layers[0].level != 0))
- return -EINVAL;
+ return ERR_PTR(-EINVAL);
this->layers[0].access |= (*layers)[0].access;
this->layers[0].flags.quiet |= (*layers)[0].flags.quiet;
- return 0;
+ return this;
}
if (WARN_ON_ONCE(this->layers[0].level == 0))
- return -EINVAL;
+ return ERR_PTR(-EINVAL);
/*
* Intersects access rights when it is a merge between a
@@ -270,23 +271,23 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
new_rule = create_rule(id, &this->layers, this->num_layers,
&(*layers)[0]);
if (IS_ERR(new_rule))
- return PTR_ERR(new_rule);
+ return ERR_CAST(new_rule);
rb_replace_node(&this->node, &new_rule->node, root);
free_rule(this, id.type);
- return 0;
+ return new_rule;
}
/* There is no match for @id. */
build_check_ruleset();
if (ruleset->num_rules >= LANDLOCK_MAX_NUM_RULES)
- return -E2BIG;
+ return ERR_PTR(-E2BIG);
new_rule = create_rule(id, layers, num_layers, NULL);
if (IS_ERR(new_rule))
- return PTR_ERR(new_rule);
+ return ERR_CAST(new_rule);
rb_link_node(&new_rule->node, parent_node, walker_node);
rb_insert_color(&new_rule->node, root);
ruleset->num_rules++;
- return 0;
+ return new_rule;
}
static void build_check_layer(void)
@@ -305,9 +306,10 @@ static void build_check_layer(void)
}
/* @ruleset must be locked by the caller. */
-int landlock_insert_rule(struct landlock_ruleset *const ruleset,
- const struct landlock_id id,
- const access_mask_t access, const u32 flags)
+struct landlock_rule *
+landlock_insert_rule(struct landlock_ruleset *const ruleset,
+ const struct landlock_id id,
+ const access_mask_t access, const u32 flags)
{
struct landlock_layer layers[] = { {
.access = access,
@@ -326,9 +328,8 @@ static int merge_tree(struct landlock_ruleset *const dst,
struct landlock_ruleset *const src,
const enum landlock_key_type key_type)
{
- struct landlock_rule *walker_rule, *next_rule;
+ struct landlock_rule *walker_rule, *next_rule, *rule;
struct rb_root *src_root;
- int err = 0;
might_sleep();
lockdep_assert_held(&dst->lock);
@@ -358,11 +359,11 @@ static int merge_tree(struct landlock_ruleset *const dst,
layers[0].access = walker_rule->layers[0].access;
layers[0].flags = walker_rule->layers[0].flags;
- err = insert_rule(dst, id, &layers, ARRAY_SIZE(layers));
- if (err)
- return err;
+ rule = insert_rule(dst, id, &layers, ARRAY_SIZE(layers));
+ if (IS_ERR(rule))
+ return PTR_ERR(rule);
}
- return err;
+ return 0;
}
static int merge_ruleset(struct landlock_ruleset *const dst,
@@ -412,9 +413,8 @@ static int inherit_tree(struct landlock_ruleset *const parent,
struct landlock_ruleset *const child,
const enum landlock_key_type key_type)
{
- struct landlock_rule *walker_rule, *next_rule;
+ struct landlock_rule *walker_rule, *next_rule, *rule;
struct rb_root *parent_root;
- int err = 0;
might_sleep();
lockdep_assert_held(&parent->lock);
@@ -432,12 +432,12 @@ static int inherit_tree(struct landlock_ruleset *const parent,
.type = key_type,
};
- err = insert_rule(child, id, &walker_rule->layers,
- walker_rule->num_layers);
- if (err)
- return err;
+ rule = insert_rule(child, id, &walker_rule->layers,
+ walker_rule->num_layers);
+ if (IS_ERR(rule))
+ return PTR_ERR(rule);
}
- return err;
+ return 0;
}
static int inherit_ruleset(struct landlock_ruleset *const parent,
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index 61f3c253d5c9..c927bcb82fa3 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -217,9 +217,10 @@ void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset);
DEFINE_FREE(landlock_put_ruleset, struct landlock_ruleset *,
if (!IS_ERR_OR_NULL(_T)) landlock_put_ruleset(_T))
-int landlock_insert_rule(struct landlock_ruleset *const ruleset,
- const struct landlock_id id,
- const access_mask_t access, const u32 flags);
+struct landlock_rule *
+landlock_insert_rule(struct landlock_ruleset *const ruleset,
+ const struct landlock_id id,
+ const access_mask_t access, const u32 flags);
struct landlock_ruleset *
landlock_merge_ruleset(struct landlock_ruleset *const parent,
--
2.54.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox