From: Daniel Borkmann <daniel@iogearbox.net>
To: alexei.starovoitov@gmail.com
Cc: brauner@kernel.org, dwindsor@gmail.com, john.fastabend@gmail.com,
memxor@gmail.com, kpsingh@kernel.org, matt@bobrowski.net,
bpf@vger.kernel.org
Subject: [PATCH bpf-next 4/8] bpf, lsm: Let BPF LSM provide xattrs at inode creation
Date: Tue, 15 Sep 2026 17:07:35 +0200 [thread overview]
Message-ID: <20260915150739.284189-5-daniel@iogearbox.net> (raw)
In-Reply-To: <20260915150739.284189-1-daniel@iogearbox.net>
From: David Windsor <dwindsor@gmail.com>
Many in-kernel LSMs (SELinux, Smack, IMA) store security labels in extended
attributes. For these LSMs, atomic labeling during inode creation is
critical: if the inode becomes accessible before its xattr is set, it is
briefly unlabeled, which can disrupt LSMs making policy decisions based
on file labels. Existing LSMs solve this by setting xattrs in the
inode_init_security hook, which runs before the inode becomes accessible.
BPF LSM programs currently lack this capability because the hook uses an
output parameter (xattr_count) that BPF programs cannot write to, and
existing kfuncs like bpf_set_dentry_xattr() require a dentry that isn't
available until after the inode is accessible.
Add a bpf_inode_init_xattr() kfunc that takes the hook's own xattrs and
xattr_count arguments, passed through from the program's context, and
claims a slot via lsm_get_xattr_slot() on the program's behalf. The
xattr_count output argument is exposed to inode_init_security programs
as trusted read-only memory, so programs can pass it to the kfunc but
cannot modify the count themselves.
Reserve BPF_LSM_INODE_INIT_XATTRS slots in bpf_lsm_blob_sizes the way
every other xattr-providing LSM does, for the life of the kernel. The
framework keys the collection off the reserved slot count, so a kernel
built with CONFIG_BPF_LSM=y now allocates the xattr array on every inode
creation, whether or not a program sits on the hook.
Give the hook a strong prototype in bpf_lsm_proto.c so that its qstr and
xattrs arguments are marked __nullable. Both can be NULL for some callers.
Without the annotation the verifier otherwise hands the program a trusted
non-NULL pointer which it dereferences. Also, keep the hook out of the
sleepable set. inode_init_security runs inside the transaction creating
the inode, with a journal handle held on ext4 and btrfs and the parent's
i_rwsem down, which is why everything on the path allocates GFP_NOFS.
Signed-off-by: David Windsor <dwindsor@gmail.com>
Co-developed-by: Daniel Borkmann <daniel@iogearbox.net>
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
fs/bpf_fs_kfuncs.c | 99 ++++++++++++++++++++++++++++++++++++++
include/linux/bpf_lsm.h | 11 ++++-
kernel/bpf/bpf_lsm.c | 22 ++++++++-
kernel/bpf/bpf_lsm_proto.c | 15 ++++++
security/bpf/hooks.c | 1 +
5 files changed, 145 insertions(+), 3 deletions(-)
diff --git a/fs/bpf_fs_kfuncs.c b/fs/bpf_fs_kfuncs.c
index 6cb877267978..c51c3ae8063b 100644
--- a/fs/bpf_fs_kfuncs.c
+++ b/fs/bpf_fs_kfuncs.c
@@ -11,6 +11,7 @@
#include <linux/fsnotify.h>
#include <linux/file.h>
#include <linux/kernfs.h>
+#include <linux/lsm_hooks.h>
#include <linux/mm.h>
#include <linux/net.h>
#include <linux/xattr.h>
@@ -328,6 +329,89 @@ __bpf_kfunc int bpf_remove_dentry_xattr(struct dentry *dentry, const char *name_
return ret;
}
+static int bpf_inode_init_xattrs_claimed(const struct xattr *xattrs, int xattr_count)
+{
+ const size_t suffix_len = sizeof(XATTR_BPF_LSM_SUFFIX) - 1;
+ int i, claimed = 0;
+
+ for (i = 0; i < xattr_count; i++) {
+ const char *name = xattrs[i].name;
+
+ if (name && !strncmp(name, XATTR_BPF_LSM_SUFFIX, suffix_len))
+ claimed++;
+ }
+ return claimed;
+}
+
+/**
+ * bpf_inode_init_xattr - attach a xattr to an inode that is being created
+ * @xattrs: xattr array the inode_init_security hook was handed
+ * @xattr_count__ctx_out: xattr count the inode_init_security hook was handed
+ * @name__str: name of the xattr
+ * @value_p: xattr value
+ *
+ * Claim one of the slots the BPF LSM reserved in the xattr array, so that
+ * the xattr is written out as part of the transaction creating the inode.
+ *
+ * For security reasons, only *name__str* with prefix "security.bpf." is
+ * allowed. The slot stores the name without that "security." prefix, which
+ * the filesystem's initxattrs() callback puts back.
+ *
+ * At most BPF_LSM_INODE_INIT_XATTRS xattrs can be attached to one inode,
+ * matching the number of slots the BPF LSM reserves.
+ *
+ * Return: 0 on success, a negative value on error.
+ */
+__bpf_kfunc int bpf_inode_init_xattr(struct xattr *xattrs,
+ int *xattr_count__ctx_out,
+ const char *name__str,
+ const struct bpf_dynptr *value_p)
+{
+ const struct bpf_dynptr_kern *value_ptr = (struct bpf_dynptr_kern *)value_p;
+ int *xattr_count = xattr_count__ctx_out;
+ const char *suffix;
+ struct xattr *slot;
+ const void *value;
+ size_t name_len;
+ u32 value_len;
+ char *buf;
+
+ if (!match_security_bpf_prefix(name__str))
+ return -EPERM;
+ if (bpf_inode_init_xattrs_claimed(xattrs, *xattr_count) >=
+ BPF_LSM_INODE_INIT_XATTRS)
+ return -ENOSPC;
+
+ suffix = name__str + XATTR_SECURITY_PREFIX_LEN;
+ name_len = strlen(suffix);
+ if (name_len <= sizeof(XATTR_BPF_LSM_SUFFIX) - 1 ||
+ name_len > XATTR_NAME_MAX - XATTR_SECURITY_PREFIX_LEN)
+ return -EINVAL;
+
+ value_len = __bpf_dynptr_size(value_ptr);
+ value = __bpf_dynptr_data(value_ptr, value_len);
+ if (!value)
+ return -EINVAL;
+ if (value_len > XATTR_SIZE_MAX)
+ return -E2BIG;
+
+ buf = kmalloc(value_len + name_len + 1, GFP_NOWAIT | __GFP_NOWARN);
+ if (!buf)
+ return -ENOMEM;
+ memcpy(buf, value, value_len);
+ memcpy(buf + value_len, suffix, name_len + 1);
+
+ slot = lsm_get_xattr_slot(xattrs, xattr_count);
+ if (!slot) {
+ kfree(buf);
+ return -ENOSPC;
+ }
+ slot->value = buf;
+ slot->value_len = value_len;
+ slot->name = buf + value_len;
+ return 0;
+}
+
#ifdef CONFIG_CGROUPS
/**
* bpf_cgroup_read_xattr - read xattr of a cgroup's node in cgroupfs
@@ -425,6 +509,7 @@ 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_data_inode, KF_SLEEPABLE | KF_RET_NULL)
+BTF_ID_FLAGS(func, bpf_inode_init_xattr)
#ifdef CONFIG_NET
BTF_ID_FLAGS(func, bpf_sock_read_xattr, KF_RCU)
#endif
@@ -436,10 +521,24 @@ BTF_ID(func, bpf_set_dentry_xattr)
BTF_ID(func, bpf_remove_dentry_xattr)
BTF_SET_END(bpf_fs_kfunc_lsm_only_ids)
+BTF_ID_LIST_SINGLE(bpf_inode_init_xattr_ids, func, bpf_inode_init_xattr)
+
+BTF_SET_START(bpf_inode_init_xattr_hooks)
+BTF_ID(func, bpf_lsm_inode_init_security)
+BTF_SET_END(bpf_inode_init_xattr_hooks)
+
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))
return 0;
+ if (kfunc_id == bpf_inode_init_xattr_ids[0]) {
+ if (prog->type != BPF_PROG_TYPE_LSM ||
+ prog->expected_attach_type != BPF_LSM_MAC ||
+ !btf_id_set_contains(&bpf_inode_init_xattr_hooks,
+ prog->aux->attach_btf_id))
+ return -EACCES;
+ return 0;
+ }
if (prog->type == BPF_PROG_TYPE_LSM)
return 0;
if (prog->type != BPF_PROG_TYPE_STRUCT_OPS)
diff --git a/include/linux/bpf_lsm.h b/include/linux/bpf_lsm.h
index dda272d78f01..eb4a38eef87e 100644
--- a/include/linux/bpf_lsm.h
+++ b/include/linux/bpf_lsm.h
@@ -21,6 +21,13 @@ extern bool bpf_lsm_initialized __ro_after_init;
#include <linux/lsm_hook_defs.h>
#undef LSM_HOOK
+/*
+ * Number of xattr slots the BPF LSM reserves in the array handed to
+ * security_inode_init_security(), i.e. the maximum number of labels
+ * a policy may attach to an inode while it is being created.
+ */
+#define BPF_LSM_INODE_INIT_XATTRS 2
+
struct bpf_storage_blob {
struct bpf_local_storage __rcu *storage;
};
@@ -28,7 +35,7 @@ struct bpf_storage_blob {
extern struct lsm_blob_sizes bpf_lsm_blob_sizes;
int bpf_lsm_verify_prog(struct bpf_verifier_log *vlog,
- const struct bpf_prog *prog);
+ struct bpf_prog *prog);
bool bpf_lsm_is_sleepable_hook(u32 btf_id);
bool bpf_lsm_is_trusted(const struct bpf_prog *prog);
@@ -71,7 +78,7 @@ static inline bool bpf_lsm_is_trusted(const struct bpf_prog *prog)
}
static inline int bpf_lsm_verify_prog(struct bpf_verifier_log *vlog,
- const struct bpf_prog *prog)
+ struct bpf_prog *prog)
{
return -EOPNOTSUPP;
}
diff --git a/kernel/bpf/bpf_lsm.c b/kernel/bpf/bpf_lsm.c
index 2660a89fc8d7..f58dce888ff4 100644
--- a/kernel/bpf/bpf_lsm.c
+++ b/kernel/bpf/bpf_lsm.c
@@ -116,8 +116,11 @@ void bpf_lsm_find_cgroup_shim(const struct bpf_prog *prog,
}
#endif
+BTF_ID_LIST_SINGLE(bpf_lsm_inode_init_security_btf_id, func,
+ bpf_lsm_inode_init_security)
+
int bpf_lsm_verify_prog(struct bpf_verifier_log *vlog,
- const struct bpf_prog *prog)
+ struct bpf_prog *prog)
{
u32 btf_id = prog->aux->attach_btf_id;
const char *func_name = prog->aux->attach_func_name;
@@ -140,6 +143,23 @@ int bpf_lsm_verify_prog(struct bpf_verifier_log *vlog,
return -EINVAL;
}
+ if (btf_id == bpf_lsm_inode_init_security_btf_id[0]) {
+ /*
+ * inode, dir, qstr, xattrs, xattr_count
+ *
+ * The trusted pointer this hands the program is only as
+ * trustworthy as the context it is loaded from, which
+ * lsm_verifier_ops keeps read-only.
+ */
+ static const struct bpf_ctx_arg_aux xattr_count_arg_info = {
+ .offset = 4 * sizeof(u64),
+ .reg_type = PTR_TO_MEM | MEM_RDONLY | PTR_TRUSTED,
+ .mem_size = sizeof(int),
+ };
+
+ return bpf_prog_ctx_arg_info_init(prog, &xattr_count_arg_info, 1);
+ }
+
return 0;
}
diff --git a/kernel/bpf/bpf_lsm_proto.c b/kernel/bpf/bpf_lsm_proto.c
index 44a54fd8045e..4a776df76cbb 100644
--- a/kernel/bpf/bpf_lsm_proto.c
+++ b/kernel/bpf/bpf_lsm_proto.c
@@ -4,6 +4,7 @@
*/
#include <linux/fs.h>
+#include <linux/xattr.h>
#include <linux/bpf_lsm.h>
/*
@@ -17,3 +18,17 @@ int bpf_lsm_mmap_file(struct file *file__nullable, unsigned long reqprot,
{
return 0;
}
+
+/*
+ * Strong definition of the inode_init_security() BPF LSM hook. Both the
+ * qstr and the xattr array are NULL for some callers, so the __nullable
+ * suffix marks it as PTR_MAYBE_NULL. BPF LSM programs have to check before
+ * dereferencing them or handing them to bpf_inode_init_xattr().
+ */
+int bpf_lsm_inode_init_security(struct inode *inode, struct inode *dir,
+ const struct qstr *qstr__nullable,
+ struct xattr *xattrs__nullable,
+ int *xattr_count)
+{
+ return -EOPNOTSUPP;
+}
diff --git a/security/bpf/hooks.c b/security/bpf/hooks.c
index 7b98f5d1e2be..8f8c3de3035f 100644
--- a/security/bpf/hooks.c
+++ b/security/bpf/hooks.c
@@ -33,6 +33,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) = {
--
2.43.0
next prev parent reply other threads:[~2026-09-15 15:07 UTC|newest]
Thread overview: 31+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-15 15:07 [PATCH bpf-next 0/8] BPF LSM xattrs at inode creation support Daniel Borkmann
2026-09-15 15:07 ` [PATCH bpf-next 1/8] ocfs2: Copy the xattr name in ocfs2_initxattrs Daniel Borkmann
2026-09-15 15:16 ` sashiko-bot
2026-09-15 16:26 ` bot+bpf-ci
2026-09-16 2:47 ` Heming Zhao
2026-09-16 7:07 ` Daniel Borkmann
2026-09-16 7:28 ` Heming Zhao
2026-09-16 7:28 ` Joseph Qi
2026-09-16 7:37 ` Daniel Borkmann
2026-09-16 7:49 ` Joseph Qi
2026-09-16 7:29 ` Heming Zhao
2026-09-15 15:07 ` [PATCH bpf-next 2/8] bpf, lsm: Reject writes into the BPF LSM program context Daniel Borkmann
2026-09-15 15:07 ` [PATCH bpf-next 3/8] bpf: Support passing context output arguments to kfuncs Daniel Borkmann
2026-09-15 15:07 ` Daniel Borkmann [this message]
2026-09-23 16:57 ` [PATCH bpf-next 4/8] bpf, lsm: Let BPF LSM provide xattrs at inode creation Paul Moore
2026-09-23 19:11 ` Daniel Borkmann
2026-09-23 20:51 ` Paul Moore
2026-09-23 19:14 ` David Windsor
2026-09-23 20:56 ` Paul Moore
2026-09-23 21:07 ` Paul Moore
2026-09-24 16:14 ` Justin Suess
2026-09-24 16:23 ` Paul Moore
2026-09-24 18:37 ` Justin Suess
2026-09-24 19:33 ` Paul Moore
2026-09-24 19:34 ` Paul Moore
2026-09-15 15:07 ` [PATCH bpf-next 5/8] bpf, lsm: Mark the BPF LSM hook overrides noinline Daniel Borkmann
2026-09-15 15:07 ` [PATCH bpf-next 6/8] selftests/bpf: Test that the BPF LSM context is read-only Daniel Borkmann
2026-09-15 15:07 ` [PATCH bpf-next 7/8] selftests/bpf: Add verifier tests for the __ctx_out plumbing Daniel Borkmann
2026-09-15 16:26 ` bot+bpf-ci
2026-09-15 15:07 ` [PATCH bpf-next 8/8] selftests/bpf: Add tests for BPF LSM inode init labelling Daniel Borkmann
2026-09-19 19:10 ` [PATCH bpf-next 0/8] BPF LSM xattrs at inode creation support patchwork-bot+netdevbpf
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260915150739.284189-5-daniel@iogearbox.net \
--to=daniel@iogearbox.net \
--cc=alexei.starovoitov@gmail.com \
--cc=bpf@vger.kernel.org \
--cc=brauner@kernel.org \
--cc=dwindsor@gmail.com \
--cc=john.fastabend@gmail.com \
--cc=kpsingh@kernel.org \
--cc=matt@bobrowski.net \
--cc=memxor@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox