* [PATCH v6 bpf-next 0/4] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling
@ 2026-07-30 23:45 David Windsor
2026-07-30 23:45 ` [PATCH v6 bpf-next 1/4] security: introduce struct lsm_xattrs David Windsor
` (3 more replies)
0 siblings, 4 replies; 12+ messages in thread
From: David Windsor @ 2026-07-30 23:45 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Martin KaFai Lau, Eduard Zingerman, Song Liu, Yonghong Song,
John Fastabend, KP Singh, Jiri Olsa, Kumar Kartikeya Dwivedi,
Emil Tsalapatis, Matt Bobrowski, Paul Moore, James Morris,
Serge E . Hallyn, Casey Schaufler, Stephen Smalley,
Ondrej Mosnacek, Mimi Zohar, Roberto Sassu, Dmitry Kasatkin,
Eric Snowberg, Alexander Viro, Christian Brauner, Jan Kara,
Shuah Khan
Cc: bpf, linux-security-module, linux-fsdevel, linux-integrity,
selinux, linux-kselftest, linux-kernel, David Windsor
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 directly 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.
This series introduces the bpf_init_inode_xattr() kfunc, which takes
the combined inode_init_security xattr context argument and claims a
slot in it via the new security_lsmxattr_add() LSM helper.
v6:
- split struct lsm_xattrs / security_lsmxattr_add() into 2 patches
(Paul)
- drop the CONFIG_BPF_LSM bracketing in security_lsmxattr_add() (Paul)
- have security_lsmxattr_add() take only extra name components (Paul)
- return 0 from security_lsmxattr_add() when the filesystem does not
provide an initxattrs callback (Paul)
- selftests: label files in /dev/shm and skip when the hook does not
run or the filesystem does not support xattrs at inode creation,
fixing BPF CI failures on the 9p rootfs
v5:
- make the kfunc non-sleepable to avoid an ext4 deadlock (sashiko)
- move xattr creation logic to security_lsmxattr_add() (Paul)
- use distinct xattr names in init_inode_xattr_slot_limit
v4:
- introduce struct lsm_xattrs in separate patch (Alexei, Paul)
- rename struct xattr_ctx to struct lsm_xattrs (Paul)
- make lsm_xattrs.xattr_count unsigned int (Paul)
- drop new_xattrs/xattr_count locals in
security_inode_init_security() (Paul)
- fold __bpf_init_inode_xattr() into bpf_init_inode_xattr() (Paul)
- drop bpf_fs_kfuncs_filter() attach-point check; rely on verifier
type enforcement (Alexei)
- drop attach-time cap; enforce slot budget in the kfunc (Alexei)
- allocate the combined xattr with GFP_NOFS (sashiko-bot)
- replace init_inode_xattr_attach_cap selftest with runtime
init_inode_xattr_slot_limit
v3:
- rename struct lsm_xattr_ctx to struct xattr_ctx (Paul)
- increase BPF_LSM_INODE_INIT_XATTRS to 4 (Song)
- enforce per-hook attachment cap at attach time to prevent
runtime rejection (Paul)
- add init_inode_xattr_attach_cap selftest
v2:
- pass the xattr state as a combined context object and drop the
verifier fixup path (Kumar)
- restrict bpf_init_inode_xattr labels to bpf.* namespace (Matt)
- cap bpf_init_inode_xattr() at BPF_LSM_INODE_INIT_XATTRS slots per
invocation (AI)
v5: https://lore.kernel.org/bpf/20260708000956.46138-1-dwindsor@gmail.com/
David Windsor (4):
security: introduce struct lsm_xattrs
security: add security_lsmxattr_add()
bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling
selftests/bpf: add tests for bpf_init_inode_xattr kfunc
fs/bpf_fs_kfuncs.c | 41 +++++
include/linux/bpf_lsm.h | 3 +
include/linux/evm.h | 9 +-
include/linux/lsm_hook_defs.h | 4 +-
include/linux/lsm_hooks.h | 16 +-
include/linux/security.h | 15 ++
security/bpf/hooks.c | 1 +
security/integrity/evm/evm_main.c | 8 +-
security/security.c | 120 ++++++++++++--
security/selinux/hooks.c | 4 +-
security/smack/smack_lsm.c | 27 ++-
tools/testing/selftests/bpf/bpf_kfuncs.h | 5 +
.../selftests/bpf/prog_tests/fs_kfuncs.c | 156 ++++++++++++++++++
.../bpf/progs/test_init_inode_xattr.c | 33 ++++
14 files changed, 395 insertions(+), 47 deletions(-)
create mode 100644 tools/testing/selftests/bpf/progs/test_init_inode_xattr.c
base-commit: 2659f94ed3be147942acbea96405efc562afb34c
--
2.53.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v6 bpf-next 1/4] security: introduce struct lsm_xattrs
2026-07-30 23:45 [PATCH v6 bpf-next 0/4] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling David Windsor
@ 2026-07-30 23:45 ` David Windsor
2026-07-30 23:53 ` sashiko-bot
2026-07-30 23:45 ` [PATCH v6 bpf-next 2/4] security: add security_lsmxattr_add() David Windsor
` (2 subsequent siblings)
3 siblings, 1 reply; 12+ messages in thread
From: David Windsor @ 2026-07-30 23:45 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Martin KaFai Lau, Eduard Zingerman, Song Liu, Yonghong Song,
John Fastabend, KP Singh, Jiri Olsa, Kumar Kartikeya Dwivedi,
Emil Tsalapatis, Matt Bobrowski, Paul Moore, James Morris,
Serge E . Hallyn, Casey Schaufler, Stephen Smalley,
Ondrej Mosnacek, Mimi Zohar, Roberto Sassu, Dmitry Kasatkin,
Eric Snowberg, Alexander Viro, Christian Brauner, Jan Kara,
Shuah Khan
Cc: bpf, linux-security-module, linux-fsdevel, linux-integrity,
selinux, linux-kselftest, linux-kernel, David Windsor
In preparation for bpf_init_inode_xattr(), a kfunc that lets bpf LSM
programs atomically label new inodes, rework how inode_init_security
xattrs are managed.
inode_init_security receives the LSM xattr array and its count as
separate parameters. For better compatibility with the bpf verifier,
update inode_init_security and its callers to consolidate these
parameters into a single context object: struct lsm_xattrs.
Suggested-by: Paul Moore <paul@paul-moore.com>
Signed-off-by: David Windsor <dwindsor@gmail.com>
---
include/linux/evm.h | 9 +++++----
include/linux/lsm_hook_defs.h | 4 ++--
include/linux/lsm_hooks.h | 16 +++++++---------
include/linux/security.h | 5 +++++
security/integrity/evm/evm_main.c | 8 +++++---
security/security.c | 24 ++++++++++++------------
security/selinux/hooks.c | 4 ++--
security/smack/smack_lsm.c | 27 ++++++++++++---------------
8 files changed, 50 insertions(+), 47 deletions(-)
diff --git a/include/linux/evm.h b/include/linux/evm.h
index 913f4573b203..528f360f3308 100644
--- a/include/linux/evm.h
+++ b/include/linux/evm.h
@@ -12,6 +12,8 @@
#include <linux/integrity.h>
#include <linux/xattr.h>
+struct lsm_xattrs;
+
#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 lsm_xattrs *xattrs);
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 lsm_xattrs *xattrs)
{
return 0;
}
diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h
index 65c9609ec207..5b2de7865ce8 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 lsm_xattrs *xattrs)
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..7afe06a8d4c6 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 lsm_xattrs *ctx)
{
- if (unlikely(!xattrs))
+ if (unlikely(!ctx || !ctx->xattrs))
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..0be590c40689 100644
--- a/include/linux/security.h
+++ b/include/linux/security.h
@@ -68,6 +68,11 @@ struct watch;
struct watch_notification;
struct lsm_ctx;
+struct lsm_xattrs {
+ struct xattr *xattrs;
+ unsigned 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/security/integrity/evm/evm_main.c b/security/integrity/evm/evm_main.c
index b59e3f121b8a..b7158fc63543 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 lsm_xattrs *lsm_xattrs)
{
struct evm_xattr *xattr_data;
struct xattr *xattr, *evm_xattr;
+ struct xattr *xattrs;
bool evm_protected_xattrs = false;
int rc;
+ xattrs = lsm_xattrs ? lsm_xattrs->xattrs : NULL;
if (!(evm_initialized & EVM_INIT_HMAC) || !xattrs)
return 0;
@@ -1087,7 +1089,7 @@ int evm_inode_init_security(struct inode *inode, struct inode *dir,
if (!evm_protected_xattrs)
return 0;
- evm_xattr = lsm_get_xattr_slot(xattrs, xattr_count);
+ evm_xattr = lsm_get_xattr_slot(lsm_xattrs);
/*
* Array terminator (xattr name = NULL) must be the first non-filled
* xattr slot.
diff --git a/security/security.c b/security/security.c
index 71aea8fdf014..2ad7f09c1a61 100644
--- a/security/security.c
+++ b/security/security.c
@@ -1333,8 +1333,8 @@ int security_inode_init_security(struct inode *inode, struct inode *dir,
const initxattrs initxattrs, void *fs_data)
{
struct lsm_static_call *scall;
- struct xattr *new_xattrs = NULL;
- int ret = -EOPNOTSUPP, xattr_count = 0;
+ struct lsm_xattrs xattrs = {};
+ int ret = -EOPNOTSUPP;
if (unlikely(IS_PRIVATE(inode)))
return 0;
@@ -1344,15 +1344,15 @@ int security_inode_init_security(struct inode *inode, struct inode *dir,
if (initxattrs) {
/* Allocate +1 as terminator. */
- new_xattrs = kcalloc(blob_sizes.lbs_xattr_count + 1,
- sizeof(*new_xattrs), GFP_NOFS);
- if (!new_xattrs)
+ xattrs.xattrs = kcalloc(blob_sizes.lbs_xattr_count + 1,
+ sizeof(*xattrs.xattrs), GFP_NOFS);
+ if (!xattrs.xattrs)
return -ENOMEM;
}
lsm_for_each_hook(scall, inode_init_security) {
- ret = scall->hl->hook.inode_init_security(inode, dir, qstr, new_xattrs,
- &xattr_count);
+ ret = scall->hl->hook.inode_init_security(inode, dir, qstr,
+ &xattrs);
if (ret && ret != -EOPNOTSUPP)
goto out;
/*
@@ -1364,14 +1364,14 @@ int security_inode_init_security(struct inode *inode, struct inode *dir,
}
/* If initxattrs() is NULL, xattr_count is zero, skip the call. */
- if (!xattr_count)
+ if (!xattrs.xattr_count)
goto out;
- ret = initxattrs(inode, new_xattrs, fs_data);
+ ret = initxattrs(inode, xattrs.xattrs, fs_data);
out:
- for (; xattr_count > 0; xattr_count--)
- kfree(new_xattrs[xattr_count - 1].value);
- kfree(new_xattrs);
+ for (; xattrs.xattr_count > 0; xattrs.xattr_count--)
+ kfree(xattrs.xattrs[xattrs.xattr_count - 1].value);
+ kfree(xattrs.xattrs);
return (ret == -EOPNOTSUPP) ? 0 : ret;
}
EXPORT_SYMBOL(security_inode_init_security);
diff --git a/security/selinux/hooks.c b/security/selinux/hooks.c
index 8d6945edae7a..21544c775c17 100644
--- a/security/selinux/hooks.c
+++ b/security/selinux/hooks.c
@@ -2962,7 +2962,7 @@ static int selinux_dentry_create_files_as(struct dentry *dentry, int mode,
static int selinux_inode_init_security(struct inode *inode, struct inode *dir,
const struct qstr *qstr,
- struct xattr *xattrs, int *xattr_count)
+ struct lsm_xattrs *xattrs)
{
const struct cred_security_struct *crsec = selinux_cred(current_cred());
struct superblock_security_struct *sbsec;
@@ -2992,7 +2992,7 @@ static int selinux_inode_init_security(struct inode *inode, struct inode *dir,
!(sbsec->flags & SBLABEL_MNT))
return -EOPNOTSUPP;
- xattr = lsm_get_xattr_slot(xattrs, xattr_count);
+ xattr = lsm_get_xattr_slot(xattrs);
if (xattr) {
rc = security_sid_to_context_force(newsid,
&context, &clen);
diff --git a/security/smack/smack_lsm.c b/security/smack/smack_lsm.c
index ff115068c5c0..4501078430ca 100644
--- a/security/smack/smack_lsm.c
+++ b/security/smack/smack_lsm.c
@@ -981,10 +981,10 @@ smk_rule_transmutes(struct smack_known *subject,
}
static int
-xattr_dupval(struct xattr *xattrs, int *xattr_count,
+xattr_dupval(struct lsm_xattrs *xattrs,
const char *name, const void *value, unsigned int vallen)
{
- struct xattr * const xattr = lsm_get_xattr_slot(xattrs, xattr_count);
+ struct xattr * const xattr = lsm_get_xattr_slot(xattrs);
if (!xattr)
return 0;
@@ -1003,14 +1003,13 @@ xattr_dupval(struct xattr *xattrs, int *xattr_count,
* @inode: the newly created inode
* @dir: containing directory object
* @qstr: unused
- * @xattrs: where to put the attributes
- * @xattr_count: current number of LSM-provided xattrs (updated)
+ * @xattrs: where to put attributes and update count
*
* Returns 0 if it all works out, -ENOMEM if there's no memory
*/
static int smack_inode_init_security(struct inode *inode, struct inode *dir,
const struct qstr *qstr,
- struct xattr *xattrs, int *xattr_count)
+ struct lsm_xattrs *xattrs)
{
struct task_smack *tsp = smack_cred(current_cred());
struct inode_smack * const issp = smack_inode(inode);
@@ -1057,21 +1056,19 @@ static int smack_inode_init_security(struct inode *inode, struct inode *dir,
if (S_ISDIR(inode->i_mode)) {
transflag = SMK_INODE_TRANSMUTE;
- if (xattr_dupval(xattrs, xattr_count,
- XATTR_SMACK_TRANSMUTE,
- TRANS_TRUE,
- TRANS_TRUE_SIZE
- ))
+ if (xattr_dupval(xattrs,
+ XATTR_SMACK_TRANSMUTE,
+ TRANS_TRUE,
+ TRANS_TRUE_SIZE))
rc = -ENOMEM;
}
}
if (rc == 0)
- if (xattr_dupval(xattrs, xattr_count,
- XATTR_SMACK_SUFFIX,
- issp->smk_inode->smk_known,
- strlen(issp->smk_inode->smk_known)
- ))
+ if (xattr_dupval(xattrs,
+ XATTR_SMACK_SUFFIX,
+ issp->smk_inode->smk_known,
+ strlen(issp->smk_inode->smk_known)))
rc = -ENOMEM;
instant_inode:
issp->smk_flags |= (SMK_INODE_INSTANT | transflag);
--
2.53.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v6 bpf-next 2/4] security: add security_lsmxattr_add()
2026-07-30 23:45 [PATCH v6 bpf-next 0/4] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling David Windsor
2026-07-30 23:45 ` [PATCH v6 bpf-next 1/4] security: introduce struct lsm_xattrs David Windsor
@ 2026-07-30 23:45 ` David Windsor
2026-07-31 0:00 ` sashiko-bot
2026-07-30 23:45 ` [PATCH v6 bpf-next 3/4] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling David Windsor
2026-07-30 23:45 ` [PATCH v6 bpf-next 4/4] selftests/bpf: add tests for bpf_init_inode_xattr kfunc David Windsor
3 siblings, 1 reply; 12+ messages in thread
From: David Windsor @ 2026-07-30 23:45 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Martin KaFai Lau, Eduard Zingerman, Song Liu, Yonghong Song,
John Fastabend, KP Singh, Jiri Olsa, Kumar Kartikeya Dwivedi,
Emil Tsalapatis, Matt Bobrowski, Paul Moore, James Morris,
Serge E . Hallyn, Casey Schaufler, Stephen Smalley,
Ondrej Mosnacek, Mimi Zohar, Roberto Sassu, Dmitry Kasatkin,
Eric Snowberg, Alexander Viro, Christian Brauner, Jan Kara,
Shuah Khan
Cc: bpf, linux-security-module, linux-fsdevel, linux-integrity,
selinux, linux-kselftest, linux-kernel, David Windsor
Add security_lsmxattr_add(), which claims a slot in the
inode_init_security xattr array on behalf of the calling LSM and
fills it with a copy of the given name and value.
Callers pass only the name components beyond their LSM's standard
xattr suffix; security_lsmxattr_add() builds the full xattr name from
the suffix associated with the given lsm_id.
Suggested-by: Paul Moore <paul@paul-moore.com>
Signed-off-by: David Windsor <dwindsor@gmail.com>
---
include/linux/bpf_lsm.h | 3 ++
include/linux/security.h | 10 +++++
security/bpf/hooks.c | 1 +
security/security.c | 96 ++++++++++++++++++++++++++++++++++++++++
4 files changed, 110 insertions(+)
diff --git a/include/linux/bpf_lsm.h b/include/linux/bpf_lsm.h
index dda272d78f01..4bf350ef02f4 100644
--- a/include/linux/bpf_lsm.h
+++ b/include/linux/bpf_lsm.h
@@ -12,6 +12,9 @@
#include <linux/bpf_verifier.h>
#include <linux/lsm_hooks.h>
+/* max bpf xattrs per inode */
+#define BPF_LSM_INODE_INIT_XATTRS 4
+
#ifdef CONFIG_BPF_LSM
extern bool bpf_lsm_initialized __ro_after_init;
diff --git a/include/linux/security.h b/include/linux/security.h
index 0be590c40689..d35fde7aa11f 100644
--- a/include/linux/security.h
+++ b/include/linux/security.h
@@ -406,6 +406,9 @@ void security_inode_free(struct inode *inode);
int security_inode_init_security(struct inode *inode, struct inode *dir,
const struct qstr *qstr,
initxattrs initxattrs, void *fs_data);
+int security_lsmxattr_add(struct lsm_xattrs *xattrs, u64 lsm_id,
+ const char *name_extra, const void *value,
+ size_t value_len);
int security_inode_init_security_anon(struct inode *inode,
const struct qstr *name,
const struct inode *context_inode);
@@ -900,6 +903,13 @@ static inline int security_inode_init_security(struct inode *inode,
return 0;
}
+static inline int security_lsmxattr_add(struct lsm_xattrs *xattrs, u64 lsm_id,
+ const char *name_extra,
+ const void *value, size_t value_len)
+{
+ return -EOPNOTSUPP;
+}
+
static inline int security_inode_init_security_anon(struct inode *inode,
const struct qstr *name,
const struct inode *context_inode)
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) = {
diff --git a/security/security.c b/security/security.c
index 2ad7f09c1a61..ae72102cd29b 100644
--- a/security/security.c
+++ b/security/security.c
@@ -12,6 +12,7 @@
#define pr_fmt(fmt) "LSM: " fmt
#include <linux/bpf.h>
+#include <linux/bpf_lsm.h>
#include <linux/capability.h>
#include <linux/dcache.h>
#include <linux/export.h>
@@ -1376,6 +1377,101 @@ int security_inode_init_security(struct inode *inode, struct inode *dir,
}
EXPORT_SYMBOL(security_inode_init_security);
+static unsigned int lsm_xattrs_used(const struct lsm_xattrs *xattrs,
+ const char *prefix)
+{
+ size_t prefix_len = strlen(prefix);
+ unsigned int i, n = 0;
+
+ for (i = 0; i < xattrs->xattr_count; i++) {
+ const char *name = xattrs->xattrs[i].name;
+
+ if (name && !strncmp(name, prefix, prefix_len))
+ n++;
+ }
+ return n;
+}
+
+/**
+ * security_lsmxattr_add() - Add an xattr during inode_init_security
+ * @xattrs: xattr state shared by inode_init_security hooks
+ * @lsm_id: LSM_ID_* value identifying the calling LSM
+ * @name_extra: xattr name components beyond the calling LSM's standard
+ * xattr suffix, NULL if the standard suffix is the full name
+ * @value: xattr value
+ * @value_len: length of @value
+ *
+ * Claim an xattr slot in @xattrs on behalf of the LSM identified by
+ * @lsm_id and fill it with a copy of @value. The xattr name is built from
+ * the standard xattr suffix of the calling LSM, followed by @name_extra.
+ * Callers can invoke this function from non-sleepable context.
+ *
+ * Return: Returns 0 on success or if the filesystem does not accept xattrs
+ * at inode creation, -ENOSPC if the calling LSM's slot budget is
+ * exhausted, negative values on other errors.
+ */
+int security_lsmxattr_add(struct lsm_xattrs *xattrs, u64 lsm_id,
+ const char *name_extra, const void *value,
+ size_t value_len)
+{
+ struct xattr *xattr;
+ void *xattr_value;
+ const char *suffix;
+ size_t suffix_len, extra_len, name_len;
+
+ if (!xattrs || !value)
+ return -EINVAL;
+
+ /* The filesystem did not provide an initxattrs callback. */
+ if (!xattrs->xattrs)
+ return 0;
+
+ switch (lsm_id) {
+ case LSM_ID_BPF:
+ if (!name_extra || !name_extra[0])
+ return -EINVAL;
+ suffix = XATTR_BPF_LSM_SUFFIX;
+ if (lsm_xattrs_used(xattrs, XATTR_BPF_LSM_SUFFIX) >=
+ BPF_LSM_INODE_INIT_XATTRS)
+ return -ENOSPC;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ suffix_len = strlen(suffix);
+ extra_len = name_extra ? strlen(name_extra) : 0;
+ name_len = suffix_len + extra_len;
+ if (name_len > XATTR_NAME_MAX)
+ return -EINVAL;
+ if (value_len == 0 || value_len > XATTR_SIZE_MAX)
+ return -EINVAL;
+
+ /* Combine xattr value + name into one allocation. */
+ xattr_value = kmalloc(value_len + name_len + 1, GFP_NOWAIT);
+ if (!xattr_value)
+ return -ENOMEM;
+
+ memcpy(xattr_value, value, value_len);
+ memcpy(xattr_value + value_len, suffix, suffix_len);
+ if (extra_len)
+ memcpy(xattr_value + value_len + suffix_len, name_extra,
+ extra_len);
+ ((char *)xattr_value)[value_len + name_len] = '\0';
+
+ xattr = lsm_get_xattr_slot(xattrs);
+ 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;
+}
+
/**
* security_inode_init_security_anon() - Initialize an anonymous inode
* @inode: the inode
--
2.53.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v6 bpf-next 3/4] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling
2026-07-30 23:45 [PATCH v6 bpf-next 0/4] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling David Windsor
2026-07-30 23:45 ` [PATCH v6 bpf-next 1/4] security: introduce struct lsm_xattrs David Windsor
2026-07-30 23:45 ` [PATCH v6 bpf-next 2/4] security: add security_lsmxattr_add() David Windsor
@ 2026-07-30 23:45 ` David Windsor
2026-07-30 23:53 ` Paul Moore
2026-07-31 0:04 ` sashiko-bot
2026-07-30 23:45 ` [PATCH v6 bpf-next 4/4] selftests/bpf: add tests for bpf_init_inode_xattr kfunc David Windsor
3 siblings, 2 replies; 12+ messages in thread
From: David Windsor @ 2026-07-30 23:45 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Martin KaFai Lau, Eduard Zingerman, Song Liu, Yonghong Song,
John Fastabend, KP Singh, Jiri Olsa, Kumar Kartikeya Dwivedi,
Emil Tsalapatis, Matt Bobrowski, Paul Moore, James Morris,
Serge E . Hallyn, Casey Schaufler, Stephen Smalley,
Ondrej Mosnacek, Mimi Zohar, Roberto Sassu, Dmitry Kasatkin,
Eric Snowberg, Alexander Viro, Christian Brauner, Jan Kara,
Shuah Khan
Cc: bpf, linux-security-module, linux-fsdevel, linux-integrity,
selinux, linux-kselftest, linux-kernel, David Windsor
Add bpf_init_inode_xattr() kfunc for BPF LSM programs to atomically set
xattrs via the inode_init_security hook using security_lsmxattr_add().
The hook now passes its xattr state as a single struct lsm_xattrs
object, which the kfunc takes directly.
This kfunc is only callable from inode_init_security; the verifier
rejects attempts to call it elsewhere.
A previous attempt [1] required a kmalloc string output protocol for
the xattr name. Since commit 6bcdfd2cac55 ("security: Allow all LSMs to
provide xattrs for inode_init_security hook") [2], the xattr name is no
longer allocated; it is a static constant.
Link: https://kernsec.org/pipermail/linux-security-module-archive/2022-October/034878.html [1]
Link: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=6bcdfd2cac55 [2]
Suggested-by: Song Liu <song@kernel.org>
Signed-off-by: David Windsor <dwindsor@gmail.com>
---
fs/bpf_fs_kfuncs.c | 41 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 41 insertions(+)
diff --git a/fs/bpf_fs_kfuncs.c b/fs/bpf_fs_kfuncs.c
index f1863a891db6..7b55073934e0 100644
--- a/fs/bpf_fs_kfuncs.c
+++ b/fs/bpf_fs_kfuncs.c
@@ -11,7 +11,9 @@
#include <linux/file.h>
#include <linux/kernfs.h>
#include <linux/mm.h>
+#include <linux/security.h>
#include <linux/xattr.h>
+#include <uapi/linux/lsm.h>
__bpf_kfunc_start_defs();
@@ -379,6 +381,44 @@ __bpf_kfunc struct inode *bpf_real_data_inode(struct file *file)
return d_real_inode(file_dentry(file));
}
+/**
+ * bpf_init_inode_xattr - set an xattr on a new inode from inode_init_security
+ * @xattrs: 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, -EOPNOTSUPP if the filesystem does not accept
+ * xattrs at inode creation, negative error on other failures.
+ */
+__bpf_kfunc int bpf_init_inode_xattr(struct lsm_xattrs *xattrs,
+ const char *name__str,
+ const struct bpf_dynptr *value_p)
+{
+ struct bpf_dynptr_kern *value_ptr = (struct bpf_dynptr_kern *)value_p;
+ const void *value;
+ u32 value_len;
+
+ if (!name__str)
+ return -EINVAL;
+ if (strncmp(name__str, XATTR_BPF_LSM_SUFFIX,
+ sizeof(XATTR_BPF_LSM_SUFFIX) - 1))
+ return -EPERM;
+
+ if (!xattrs->xattrs)
+ return -EOPNOTSUPP;
+
+ value_len = __bpf_dynptr_size(value_ptr);
+ value = __bpf_dynptr_data(value_ptr, value_len);
+ if (!value)
+ return -EINVAL;
+
+ return security_lsmxattr_add(xattrs, LSM_ID_BPF,
+ name__str + sizeof(XATTR_BPF_LSM_SUFFIX) - 1,
+ value, value_len);
+}
+
__bpf_kfunc_end_defs();
BTF_KFUNCS_START(bpf_fs_kfunc_set_ids)
@@ -390,6 +430,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_init_inode_xattr)
BTF_KFUNCS_END(bpf_fs_kfunc_set_ids)
static int bpf_fs_kfuncs_filter(const struct bpf_prog *prog, u32 kfunc_id)
--
2.53.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v6 bpf-next 4/4] selftests/bpf: add tests for bpf_init_inode_xattr kfunc
2026-07-30 23:45 [PATCH v6 bpf-next 0/4] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling David Windsor
` (2 preceding siblings ...)
2026-07-30 23:45 ` [PATCH v6 bpf-next 3/4] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling David Windsor
@ 2026-07-30 23:45 ` David Windsor
2026-07-30 23:55 ` sashiko-bot
3 siblings, 1 reply; 12+ messages in thread
From: David Windsor @ 2026-07-30 23:45 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Martin KaFai Lau, Eduard Zingerman, Song Liu, Yonghong Song,
John Fastabend, KP Singh, Jiri Olsa, Kumar Kartikeya Dwivedi,
Emil Tsalapatis, Matt Bobrowski, Paul Moore, James Morris,
Serge E . Hallyn, Casey Schaufler, Stephen Smalley,
Ondrej Mosnacek, Mimi Zohar, Roberto Sassu, Dmitry Kasatkin,
Eric Snowberg, Alexander Viro, Christian Brauner, Jan Kara,
Shuah Khan
Cc: bpf, linux-security-module, linux-fsdevel, linux-integrity,
selinux, linux-kselftest, linux-kernel, David Windsor
Test bpf atomic inode xattr labeling in inode_init_security.
Signed-off-by: David Windsor <dwindsor@gmail.com>
---
tools/testing/selftests/bpf/bpf_kfuncs.h | 5 +
.../selftests/bpf/prog_tests/fs_kfuncs.c | 156 ++++++++++++++++++
.../bpf/progs/test_init_inode_xattr.c | 33 ++++
3 files changed, 194 insertions(+)
create mode 100644 tools/testing/selftests/bpf/progs/test_init_inode_xattr.c
diff --git a/tools/testing/selftests/bpf/bpf_kfuncs.h b/tools/testing/selftests/bpf/bpf_kfuncs.h
index ae71e9b69051..2639f9f94195 100644
--- a/tools/testing/selftests/bpf/bpf_kfuncs.h
+++ b/tools/testing/selftests/bpf/bpf_kfuncs.h
@@ -92,4 +92,9 @@ extern int bpf_set_dentry_xattr(struct dentry *dentry, const char *name__str,
const struct bpf_dynptr *value_p, int flags) __ksym __weak;
extern int bpf_remove_dentry_xattr(struct dentry *dentry, const char *name__str) __ksym __weak;
+struct lsm_xattrs;
+extern int bpf_init_inode_xattr(struct lsm_xattrs *xattrs,
+ const char *name__str,
+ const struct bpf_dynptr *value_p) __ksym __weak;
+
#endif
diff --git a/tools/testing/selftests/bpf/prog_tests/fs_kfuncs.c b/tools/testing/selftests/bpf/prog_tests/fs_kfuncs.c
index 43a26ec69a8e..b208522dbd33 100644
--- a/tools/testing/selftests/bpf/prog_tests/fs_kfuncs.c
+++ b/tools/testing/selftests/bpf/prog_tests/fs_kfuncs.c
@@ -10,6 +10,7 @@
#include "test_get_xattr.skel.h"
#include "test_set_remove_xattr.skel.h"
#include "test_fsverity.skel.h"
+#include "test_init_inode_xattr.skel.h"
static const char testfile[] = "/tmp/test_progs_fs_kfuncs";
@@ -268,6 +269,155 @@ static void test_fsverity(void)
remove(testfile);
}
+static void test_init_inode_xattr(void)
+{
+ struct test_init_inode_xattr *skel = NULL;
+ int fd = -1, err;
+ char value_out[64];
+
+ /* This test must be run from a fs that calls
+ * security_inode_init_security().
+ */
+ const char *testfile_new = "/dev/shm/test_progs_fs_kfuncs_new";
+
+ skel = test_init_inode_xattr__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "test_init_inode_xattr__open_and_load"))
+ return;
+
+ skel->bss->monitored_pid = getpid();
+ err = test_init_inode_xattr__attach(skel);
+ if (!ASSERT_OK(err, "test_init_inode_xattr__attach"))
+ goto out;
+
+ /* Trigger inode_init_security */
+ fd = open(testfile_new, O_CREAT | O_RDWR, 0644);
+ if (!ASSERT_GE(fd, 0, "create_file"))
+ goto out;
+
+ /*
+ * Probably should not be needed as we will be labeling a file
+ * in /dev/shm, but just in case we check if the hook was actually
+ * called.
+ */
+ if (!skel->bss->hook_ran) {
+ printf("%s:SKIP:inode_init_security hook was not invoked\n",
+ __func__);
+ test__skip();
+ goto out;
+ }
+
+ /* The filesystem does not accept xattrs at inode creation. */
+ if (skel->data->init_result == -EOPNOTSUPP) {
+ printf("%s:SKIP:filesystem does not support LSM init xattrs\n",
+ __func__);
+ test__skip();
+ goto out;
+ }
+
+ ASSERT_EQ(skel->data->init_result, 0, "init_result");
+
+ /* initxattrs prepends "security." to the name. */
+ err = getxattr(testfile_new, "security.bpf.test_label", value_out,
+ sizeof(value_out));
+ if (err < 0 && errno == ENODATA) {
+ printf("%s:SKIP:filesystem did not apply LSM xattrs\n",
+ __func__);
+ test__skip();
+ goto out;
+ }
+ if (!ASSERT_GE(err, 0, "getxattr"))
+ goto out;
+
+ ASSERT_EQ(err, (int)sizeof(skel->data->xattr_value), "xattr_size");
+ ASSERT_EQ(strncmp(value_out, "unconfined_u:object_r:user_home_t:s0",
+ sizeof("unconfined_u:object_r:user_home_t:s0")), 0,
+ "xattr_value");
+
+out:
+ close(fd);
+ test_init_inode_xattr__destroy(skel);
+ remove(testfile_new);
+}
+
+/* Keep in sync with BPF_LSM_INODE_INIT_XATTRS in include/linux/bpf_lsm.h. */
+#define INIT_INODE_XATTR_MAX 4
+
+/*
+ * Programs may attach to inode_init_security without an attach-time limit, but
+ * the kfunc only lets BPF claim INIT_INODE_XATTR_MAX xattr slots per inode.
+ * Calls beyond that budget are rejected at runtime with -ENOSPC.
+ */
+static void test_init_inode_xattr_slot_limit(void)
+{
+ struct test_init_inode_xattr *skel[INIT_INODE_XATTR_MAX + 1] = {};
+ struct bpf_link *link[INIT_INODE_XATTR_MAX + 1] = {};
+ const char *testfile_slot = "/dev/shm/test_progs_fs_kfuncs_slot";
+ int ok = 0, nospc = 0, notrun = 0, other = 0;
+ int i, fd = -1;
+
+ /* All programs attach successfully; there is no attach-time cap. */
+ for (i = 0; i <= INIT_INODE_XATTR_MAX; i++) {
+ skel[i] = test_init_inode_xattr__open();
+ if (!ASSERT_OK_PTR(skel[i], "open"))
+ goto out;
+
+ snprintf(skel[i]->rodata->xattr_name,
+ sizeof(skel[i]->rodata->xattr_name), "bpf.label_%d",
+ i);
+
+ if (!ASSERT_OK(test_init_inode_xattr__load(skel[i]), "load"))
+ goto out;
+
+ skel[i]->bss->monitored_pid = getpid();
+
+ link[i] = bpf_program__attach_lsm(skel[i]->progs.test_init_inode_xattr);
+ if (!ASSERT_OK_PTR(link[i], "attach"))
+ goto out;
+ }
+
+ /* Trigger inode_init_security once with all programs attached. */
+ fd = open(testfile_slot, O_CREAT | O_RDWR, 0644);
+ if (!ASSERT_GE(fd, 0, "create_file"))
+ goto out;
+
+ /*
+ * Exactly INIT_INODE_XATTR_MAX programs claim a slot; test
+ * xattr budget accounting with -ENOSPC.
+ */
+ for (i = 0; i <= INIT_INODE_XATTR_MAX; i++) {
+ int res = skel[i]->data->init_result;
+
+ if (!skel[i]->bss->hook_ran || res == -EOPNOTSUPP)
+ notrun++;
+ else if (res == 0)
+ ok++;
+ else if (res == -ENOSPC)
+ nospc++;
+ else
+ other++;
+ }
+
+ if (notrun == INIT_INODE_XATTR_MAX + 1) {
+ printf("%s:SKIP:hook not invoked or fs lacks LSM init xattrs\n",
+ __func__);
+ test__skip();
+ goto out;
+ }
+
+ ASSERT_EQ(ok, INIT_INODE_XATTR_MAX, "slots_within_budget");
+ ASSERT_EQ(nospc, 1, "slot_over_budget");
+ ASSERT_EQ(other, 0, "unexpected_result");
+
+out:
+ if (fd >= 0)
+ close(fd);
+ for (i = 0; i <= INIT_INODE_XATTR_MAX; i++) {
+ bpf_link__destroy(link[i]);
+ test_init_inode_xattr__destroy(skel[i]);
+ }
+ remove(testfile_slot);
+}
+
void test_fs_kfuncs(void)
{
/* Matches xattr_names in progs/test_get_xattr.c */
@@ -288,4 +438,10 @@ void test_fs_kfuncs(void)
if (test__start_subtest("fsverity"))
test_fsverity();
+
+ if (test__start_subtest("init_inode_xattr"))
+ test_init_inode_xattr();
+
+ if (test__start_subtest("init_inode_xattr_slot_limit"))
+ test_init_inode_xattr_slot_limit();
}
diff --git a/tools/testing/selftests/bpf/progs/test_init_inode_xattr.c b/tools/testing/selftests/bpf/progs/test_init_inode_xattr.c
new file mode 100644
index 000000000000..ce3d8d39de9c
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/test_init_inode_xattr.c
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Cisco Systems, Inc. */
+
+#include "vmlinux.h"
+#include <bpf/bpf_tracing.h>
+#include "bpf_kfuncs.h"
+
+char _license[] SEC("license") = "GPL";
+
+__u32 monitored_pid;
+int hook_ran;
+int init_result = -1;
+
+const char xattr_name[16] = "bpf.test_label";
+char xattr_value[] = "unconfined_u:object_r:user_home_t:s0";
+
+SEC("lsm/inode_init_security")
+int BPF_PROG(test_init_inode_xattr, struct inode *inode, struct inode *dir,
+ const struct qstr *qstr, struct lsm_xattrs *xattrs)
+{
+ struct bpf_dynptr value_ptr;
+ __u32 pid;
+
+ pid = bpf_get_current_pid_tgid() >> 32;
+ if (pid != monitored_pid)
+ return 0;
+
+ hook_ran = 1;
+ bpf_dynptr_from_mem(xattr_value, sizeof(xattr_value), 0, &value_ptr);
+ init_result = bpf_init_inode_xattr(xattrs, xattr_name, &value_ptr);
+
+ return 0;
+}
--
2.53.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH v6 bpf-next 1/4] security: introduce struct lsm_xattrs
2026-07-30 23:45 ` [PATCH v6 bpf-next 1/4] security: introduce struct lsm_xattrs David Windsor
@ 2026-07-30 23:53 ` sashiko-bot
0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-07-30 23:53 UTC (permalink / raw)
To: David Windsor; +Cc: selinux, bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Latent buffer overflow: `struct lsm_xattrs` lacks a capacity field, and `lsm_get_xattr_slot()` increments `xattr_count` without bounds checking.
--
--- Patch [1]: [PATCH v6 bpf-next 1/4] security: introduce struct lsm_xattrs ---
commit 4d965e040bf3aa7aac8ca18905c2c3f09a037f96
Author: David Windsor <dwindsor@gmail.com>
security: introduce struct lsm_xattrs
This patch introduces struct lsm_xattrs to consolidate the LSM xattr array
and its count into a single context object. This preparation allows better
compatibility with the BPF verifier for a planned kfunc that lets BPF LSM
programs atomically label new inodes.
> diff --git a/include/linux/security.h b/include/linux/security.h
> --- a/include/linux/security.h
> +++ b/include/linux/security.h
[ ... ]
> @@ -68,6 +68,11 @@ struct watch;
> struct watch_notification;
> struct lsm_ctx;
>
> +struct lsm_xattrs {
> + struct xattr *xattrs;
> + unsigned int xattr_count;
> +};
> +
[Severity: Medium]
Should this struct also track the maximum capacity of the array?
Since this object is being introduced to allow dynamically loaded BPF LSM
programs to label new inodes, a BPF program calling the planned kfunc
multiple times could potentially exceed the allocated capacity. Tracking
the max capacity would allow for bounds checking.
> /* Default (no) options for the capable function */
> #define CAP_OPT_NONE 0x0
[ ... ]
> diff --git a/include/linux/lsm_hooks.h b/include/linux/lsm_hooks.h
> --- a/include/linux/lsm_hooks.h
> +++ b/include/linux/lsm_hooks.h
[ ... ]
> -static inline struct xattr *lsm_get_xattr_slot(struct xattr *xattrs,
> - int *xattr_count)
> +static inline struct xattr *lsm_get_xattr_slot(struct lsm_xattrs *ctx)
> {
> - if (unlikely(!xattrs))
> + if (unlikely(!ctx || !ctx->xattrs))
> return NULL;
> - return &xattrs[(*xattr_count)++];
> + return &ctx->xattrs[ctx->xattr_count++];
> }
[Severity: Medium]
Does this increment need a bounds check against the array capacity?
If a dynamic BPF LSM program requests more slots than were allocated in
security_inode_init_security() (via blob_sizes.lbs_xattr_count + 1), this
would result in an out-of-bounds write to the array.
This could subsequently lead to memory corruption during the cleanup loop
that calls kfree() on the array elements.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260730234533.1912709-1-dwindsor@gmail.com?part=1
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v6 bpf-next 3/4] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling
2026-07-30 23:45 ` [PATCH v6 bpf-next 3/4] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling David Windsor
@ 2026-07-30 23:53 ` Paul Moore
2026-07-31 0:01 ` David Windsor
2026-07-31 0:04 ` sashiko-bot
1 sibling, 1 reply; 12+ messages in thread
From: Paul Moore @ 2026-07-30 23:53 UTC (permalink / raw)
To: David Windsor
Cc: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Martin KaFai Lau, Eduard Zingerman, Song Liu, Yonghong Song,
John Fastabend, KP Singh, Jiri Olsa, Kumar Kartikeya Dwivedi,
Emil Tsalapatis, Matt Bobrowski, James Morris, Serge E . Hallyn,
Casey Schaufler, Stephen Smalley, Ondrej Mosnacek, Mimi Zohar,
Roberto Sassu, Dmitry Kasatkin, Eric Snowberg, Alexander Viro,
Christian Brauner, Jan Kara, Shuah Khan, bpf,
linux-security-module, linux-fsdevel, linux-integrity, selinux,
linux-kselftest, linux-kernel
On Thu, Jul 30, 2026 at 7:45 PM David Windsor <dwindsor@gmail.com> wrote:
>
> Add bpf_init_inode_xattr() kfunc for BPF LSM programs to atomically set
> xattrs via the inode_init_security hook using security_lsmxattr_add().
> The hook now passes its xattr state as a single struct lsm_xattrs
> object, which the kfunc takes directly.
>
> This kfunc is only callable from inode_init_security; the verifier
> rejects attempts to call it elsewhere.
>
> A previous attempt [1] required a kmalloc string output protocol for
> the xattr name. Since commit 6bcdfd2cac55 ("security: Allow all LSMs to
> provide xattrs for inode_init_security hook") [2], the xattr name is no
> longer allocated; it is a static constant.
>
> Link: https://kernsec.org/pipermail/linux-security-module-archive/2022-October/034878.html [1]
> Link: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=6bcdfd2cac55 [2]
> Suggested-by: Song Liu <song@kernel.org>
> Signed-off-by: David Windsor <dwindsor@gmail.com>
> ---
> fs/bpf_fs_kfuncs.c | 41 +++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 41 insertions(+)
>
> diff --git a/fs/bpf_fs_kfuncs.c b/fs/bpf_fs_kfuncs.c
> index f1863a891db6..7b55073934e0 100644
> --- a/fs/bpf_fs_kfuncs.c
> +++ b/fs/bpf_fs_kfuncs.c
> @@ -11,7 +11,9 @@
> #include <linux/file.h>
> #include <linux/kernfs.h>
> #include <linux/mm.h>
> +#include <linux/security.h>
> #include <linux/xattr.h>
> +#include <uapi/linux/lsm.h>
>
> __bpf_kfunc_start_defs();
>
> @@ -379,6 +381,44 @@ __bpf_kfunc struct inode *bpf_real_data_inode(struct file *file)
> return d_real_inode(file_dentry(file));
> }
>
> +/**
> + * bpf_init_inode_xattr - set an xattr on a new inode from inode_init_security
> + * @xattrs: 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, -EOPNOTSUPP if the filesystem does not accept
> + * xattrs at inode creation, negative error on other failures.
> + */
> +__bpf_kfunc int bpf_init_inode_xattr(struct lsm_xattrs *xattrs,
> + const char *name__str,
> + const struct bpf_dynptr *value_p)
> +{
> + struct bpf_dynptr_kern *value_ptr = (struct bpf_dynptr_kern *)value_p;
> + const void *value;
> + u32 value_len;
> +
> + if (!name__str)
> + return -EINVAL;
> + if (strncmp(name__str, XATTR_BPF_LSM_SUFFIX,
> + sizeof(XATTR_BPF_LSM_SUFFIX) - 1))
> + return -EPERM;
> +
> + if (!xattrs->xattrs)
> + return -EOPNOTSUPP;
> +
> + value_len = __bpf_dynptr_size(value_ptr);
> + value = __bpf_dynptr_data(value_ptr, value_len);
> + if (!value)
> + return -EINVAL;
> +
> + return security_lsmxattr_add(xattrs, LSM_ID_BPF,
> + name__str + sizeof(XATTR_BPF_LSM_SUFFIX) - 1,
> + value, value_len);
> +}
> +
Based on our discussion in the v5 patchset, I was expecting to see
this under security/; did something happen, or was there some
confusion?
--
paul-moore.com
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v6 bpf-next 4/4] selftests/bpf: add tests for bpf_init_inode_xattr kfunc
2026-07-30 23:45 ` [PATCH v6 bpf-next 4/4] selftests/bpf: add tests for bpf_init_inode_xattr kfunc David Windsor
@ 2026-07-30 23:55 ` sashiko-bot
0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-07-30 23:55 UTC (permalink / raw)
To: David Windsor; +Cc: selinux, bpf
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Silent test skipping if the testfile exists from a previous aborted run.
- [Low] Multi-line comment does not follow the required BPF subsystem style.
--
--- Patch [4]: [PATCH v6 bpf-next 4/4] selftests/bpf: add tests for bpf_init_inode_xattr kfunc ---
commit a299ac47c44a77d98dbdf43f3ba7c981f13fd930
Author: David Windsor <dwindsor@gmail.com>
selftests/bpf: add tests for bpf_init_inode_xattr kfunc
This commit adds selftests for BPF atomic inode xattr labeling in
the inode_init_security hook.
> --- a/tools/testing/selftests/bpf/prog_tests/fs_kfuncs.c
> +++ b/tools/testing/selftests/bpf/prog_tests/fs_kfuncs.c
[ ... ]
> @@ -268,6 +269,155 @@ static void test_fsverity(void)
> remove(testfile);
> }
>
> +static void test_init_inode_xattr(void)
> +{
> + struct test_init_inode_xattr *skel = NULL;
> + int fd = -1, err;
> + char value_out[64];
> +
> + /* This test must be run from a fs that calls
> + * security_inode_init_security().
> + */
[Severity: Low]
This isn't a bug, but does this multi-line comment violate the BPF subsystem
style guidelines? The guidelines prefer the opening /* on its own line:
/*
* This test must be run from a fs that calls
* security_inode_init_security().
*/
> + const char *testfile_new = "/dev/shm/test_progs_fs_kfuncs_new";
> +
> + skel = test_init_inode_xattr__open_and_load();
> + if (!ASSERT_OK_PTR(skel, "test_init_inode_xattr__open_and_load"))
> + return;
> +
> + skel->bss->monitored_pid = getpid();
> + err = test_init_inode_xattr__attach(skel);
> + if (!ASSERT_OK(err, "test_init_inode_xattr__attach"))
> + goto out;
> +
> + /* Trigger inode_init_security */
> + fd = open(testfile_new, O_CREAT | O_RDWR, 0644);
[Severity: Medium]
Could this result in silently skipping the test if the file already exists
from a previous aborted run?
By omitting O_EXCL, if the test file is already present, open() will not
create a new inode. This means the inode_init_security hook won't trigger,
and the test will hit the skip condition below. Should we use
O_CREAT | O_EXCL | O_RDWR here to ensure we always create a new inode?
> + if (!ASSERT_GE(fd, 0, "create_file"))
> + goto out;
> +
> + /*
> + * Probably should not be needed as we will be labeling a file
> + * in /dev/shm, but just in case we check if the hook was actually
> + * called.
> + */
> + if (!skel->bss->hook_ran) {
> + printf("%s:SKIP:inode_init_security hook was not invoked\n",
> + __func__);
> + test__skip();
> + goto out;
> + }
[ ... ]
> +static void test_init_inode_xattr_slot_limit(void)
> +{
[ ... ]
> + /* Trigger inode_init_security once with all programs attached. */
> + fd = open(testfile_slot, O_CREAT | O_RDWR, 0644);
[Severity: Medium]
Does this face the same issue where a pre-existing file from a previous
aborted run could cause the test to silently skip?
Adding O_EXCL would guarantee the inode is created and the hook is
triggered.
> + if (!ASSERT_GE(fd, 0, "create_file"))
> + goto out;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260730234533.1912709-1-dwindsor@gmail.com?part=4
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v6 bpf-next 2/4] security: add security_lsmxattr_add()
2026-07-30 23:45 ` [PATCH v6 bpf-next 2/4] security: add security_lsmxattr_add() David Windsor
@ 2026-07-31 0:00 ` sashiko-bot
2026-07-31 0:30 ` David Windsor
0 siblings, 1 reply; 12+ messages in thread
From: sashiko-bot @ 2026-07-31 0:00 UTC (permalink / raw)
To: David Windsor; +Cc: bpf, selinux
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The dynamically allocated `xattr->name` pointer is used after being freed by `security_inode_init_security()` in the ocfs2 filesystem.
--
--- Patch [2]: [PATCH v6 bpf-next 2/4] security: add security_lsmxattr_add() ---
commit cefa37d110516b84cf53133f4f60bba93ba53641
Author: David Windsor <dwindsor@gmail.com>
security: add security_lsmxattr_add()
This commit adds security_lsmxattr_add(), which claims a slot in the
inode_init_security xattr array on behalf of the calling LSM. It fills the
slot with a dynamically allocated copy of the given name and value.
> diff --git a/security/security.c b/security/security.c
> index 2ad7f09c1a611..ae72102cd29b8 100644
> --- a/security/security.c
> +++ b/security/security.c
[ ... ]
> @@ -1376,6 +1377,101 @@ int security_inode_init_security(struct inode *inode, struct inode *dir,
[ ... ]
> +int security_lsmxattr_add(struct lsm_xattrs *xattrs, u64 lsm_id,
> + const char *name_extra, const void *value,
> + size_t value_len)
> +{
[ ... ]
> + /* Combine xattr value + name into one allocation. */
> + xattr_value = kmalloc(value_len + name_len + 1, GFP_NOWAIT);
> + if (!xattr_value)
> + return -ENOMEM;
> +
> + memcpy(xattr_value, value, value_len);
> + memcpy(xattr_value + value_len, suffix, suffix_len);
> + if (extra_len)
> + memcpy(xattr_value + value_len + suffix_len, name_extra,
> + extra_len);
> + ((char *)xattr_value)[value_len + name_len] = '\0';
> +
> + xattr = lsm_get_xattr_slot(xattrs);
> + if (!xattr) {
> + kfree(xattr_value);
> + return -ENOSPC;
> + }
> +
> + xattr->value = xattr_value;
> + xattr->name = (const char *)xattr_value + value_len;
[Severity: High]
Does this code cause a use-after-free for filesystems like ocfs2?
Looking at ocfs2, the ocfs2_initxattrs() callback saves the xattr->name
pointer directly into si->name, assuming it has a static lifetime (which
was true for SELinux and Smack):
fs/ocfs2/xattr.c:ocfs2_initxattrs() {
...
si->name = xattr_array->name;
...
}
But here xattr->name points inside the dynamically allocated xattr->value
block. When security_inode_init_security() completes, it frees
xattr->value:
security/security.c:security_inode_init_security() {
...
ret = initxattrs(inode, xattrs.xattrs, fs_data);
out:
for (; xattrs.xattr_count > 0; xattrs.xattr_count--)
kfree(xattrs.xattrs[xattrs.xattr_count - 1].value);
...
}
After this free, ocfs2_mknod() will call ocfs2_calc_xattr_init(), which
dereferences the now-dangling si->name pointer:
fs/ocfs2/xattr.c:ocfs2_calc_xattr_init() {
...
s_size = ocfs2_xattr_entry_real_size(strlen(si->name),
si->value_len);
...
}
Could the dynamically allocated name trigger a use-after-free here?
> + xattr->value_len = value_len;
> +
> + return 0;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260730234533.1912709-1-dwindsor@gmail.com?part=2
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v6 bpf-next 3/4] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling
2026-07-30 23:53 ` Paul Moore
@ 2026-07-31 0:01 ` David Windsor
0 siblings, 0 replies; 12+ messages in thread
From: David Windsor @ 2026-07-31 0:01 UTC (permalink / raw)
To: Paul Moore
Cc: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Martin KaFai Lau, Eduard Zingerman, Song Liu, Yonghong Song,
John Fastabend, KP Singh, Jiri Olsa, Kumar Kartikeya Dwivedi,
Emil Tsalapatis, Matt Bobrowski, James Morris, Serge E . Hallyn,
Casey Schaufler, Stephen Smalley, Ondrej Mosnacek, Mimi Zohar,
Roberto Sassu, Dmitry Kasatkin, Eric Snowberg, Alexander Viro,
Christian Brauner, Jan Kara, Shuah Khan, bpf,
linux-security-module, linux-fsdevel, linux-integrity, selinux,
linux-kselftest, linux-kernel
On Thu, Jul 30, 2026 at 7:53 PM Paul Moore <paul@paul-moore.com> wrote:
>
> Based on our discussion in the v5 patchset, I was expecting to see
> this under security/; did something happen, or was there some
> confusion?
>
Nothing happened, perhaps some confusion. I was basing the placement
off of Kumar's comment in v5:
> I don't think we should do that in v6 though. What you're describing affects all
other xattr kfuncs. That's a broader discussion we should have separately when
folks are back from vacation, and more importantly involve FS folks where the
code currently lives. As you say yourself, the lines are blurry, xattrs are as
much a filesystem concept.
My take on this was, leave it in fs/ for now and then litigate the
placement of all of the fs-related kfuncs after bpf_init_inode_xattr
lands.
Happy to place this in security/ in v7, if that's preferred. Apologies
if I misread the thread.
-David
> --
> paul-moore.com
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v6 bpf-next 3/4] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling
2026-07-30 23:45 ` [PATCH v6 bpf-next 3/4] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling David Windsor
2026-07-30 23:53 ` Paul Moore
@ 2026-07-31 0:04 ` sashiko-bot
1 sibling, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-07-31 0:04 UTC (permalink / raw)
To: David Windsor; +Cc: bpf, selinux
> Add bpf_init_inode_xattr() kfunc for BPF LSM programs to atomically set
> xattrs via the inode_init_security hook using security_lsmxattr_add().
> The hook now passes its xattr state as a single struct lsm_xattrs
> object, which the kfunc takes directly.
>
> This kfunc is only callable from inode_init_security; the verifier
> rejects attempts to call it elsewhere.
>
> A previous attempt [1] required a kmalloc string output protocol for
> the xattr name. Since commit 6bcdfd2cac55 ("security: Allow all LSMs to
> provide xattrs for inode_init_security hook") [2], the xattr name is no
> longer allocated; it is a static constant.
>
> Link: https://kernsec.org/pipermail/linux-security-module-archive/2022-October/034878.html [1]
> Link: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=6bcdfd2cac55 [2]
> Suggested-by: Song Liu <song@kernel.org>
> Signed-off-by: David Windsor <dwindsor@gmail.com>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260730234533.1912709-1-dwindsor@gmail.com?part=3
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v6 bpf-next 2/4] security: add security_lsmxattr_add()
2026-07-31 0:00 ` sashiko-bot
@ 2026-07-31 0:30 ` David Windsor
0 siblings, 0 replies; 12+ messages in thread
From: David Windsor @ 2026-07-31 0:30 UTC (permalink / raw)
To: sashiko-reviews; +Cc: bpf, selinux
On Thu, Jul 30, 2026 at 8:00 PM <sashiko-bot@kernel.org> wrote:
>
> > +int security_lsmxattr_add(struct lsm_xattrs *xattrs, u64 lsm_id,
> > + const char *name_extra, const void *value,
> > + size_t value_len)
> > +{
> [ ... ]
> > + /* Combine xattr value + name into one allocation. */
> > + xattr_value = kmalloc(value_len + name_len + 1, GFP_NOWAIT);
> > + if (!xattr_value)
> > + return -ENOMEM;
> > +
> > + memcpy(xattr_value, value, value_len);
> > + memcpy(xattr_value + value_len, suffix, suffix_len);
> > + if (extra_len)
> > + memcpy(xattr_value + value_len + suffix_len, name_extra,
> > + extra_len);
> > + ((char *)xattr_value)[value_len + name_len] = '\0';
> > +
> > + xattr = lsm_get_xattr_slot(xattrs);
> > + if (!xattr) {
> > + kfree(xattr_value);
> > + return -ENOSPC;
> > + }
> > +
> > + xattr->value = xattr_value;
> > + xattr->name = (const char *)xattr_value + value_len;
>
> [Severity: High]
> Does this code cause a use-after-free for filesystems like ocfs2?
>
> Looking at ocfs2, the ocfs2_initxattrs() callback saves the xattr->name
> pointer directly into si->name, assuming it has a static lifetime (which
> was true for SELinux and Smack):
>
> fs/ocfs2/xattr.c:ocfs2_initxattrs() {
> ...
> si->name = xattr_array->name;
> ...
> }
>
> But here xattr->name points inside the dynamically allocated xattr->value
> block. When security_inode_init_security() completes, it frees
> xattr->value:
>
> security/security.c:security_inode_init_security() {
> ...
> ret = initxattrs(inode, xattrs.xattrs, fs_data);
> out:
> for (; xattrs.xattr_count > 0; xattrs.xattr_count--)
> kfree(xattrs.xattrs[xattrs.xattr_count - 1].value);
> ...
> }
>
> After this free, ocfs2_mknod() will call ocfs2_calc_xattr_init(), which
> dereferences the now-dangling si->name pointer:
>
> fs/ocfs2/xattr.c:ocfs2_calc_xattr_init() {
> ...
> s_size = ocfs2_xattr_entry_real_size(strlen(si->name),
> si->value_len);
> ...
> }
>
> Could the dynamically allocated name trigger a use-after-free here?
>
This is real. ocfs stashes the name pointer beyond the call; it's the
only fs who does so. We can special-case ocfs and be done with it.
Will do this in v7.
> > + xattr->value_len = value_len;
> > +
> > + return 0;
> > +}
>
> --
> Sashiko AI review · https://sashiko.dev/#/patchset/20260730234533.1912709-1-dwindsor@gmail.com?part=2
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-07-31 0:30 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-30 23:45 [PATCH v6 bpf-next 0/4] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling David Windsor
2026-07-30 23:45 ` [PATCH v6 bpf-next 1/4] security: introduce struct lsm_xattrs David Windsor
2026-07-30 23:53 ` sashiko-bot
2026-07-30 23:45 ` [PATCH v6 bpf-next 2/4] security: add security_lsmxattr_add() David Windsor
2026-07-31 0:00 ` sashiko-bot
2026-07-31 0:30 ` David Windsor
2026-07-30 23:45 ` [PATCH v6 bpf-next 3/4] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling David Windsor
2026-07-30 23:53 ` Paul Moore
2026-07-31 0:01 ` David Windsor
2026-07-31 0:04 ` sashiko-bot
2026-07-30 23:45 ` [PATCH v6 bpf-next 4/4] selftests/bpf: add tests for bpf_init_inode_xattr kfunc David Windsor
2026-07-30 23:55 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.