* [PATCH bpf-next v3 1/2] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling
2026-06-18 20:34 [PATCH bpf-next v3 0/2] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling David Windsor
@ 2026-06-18 20:34 ` David Windsor
2026-06-18 20:43 ` sashiko-bot
2026-06-18 21:22 ` bot+bpf-ci
2026-06-18 20:34 ` [PATCH bpf-next v3 2/2] selftests/bpf: add tests for bpf_init_inode_xattr kfunc David Windsor
1 sibling, 2 replies; 7+ messages in thread
From: David Windsor @ 2026-06-18 20:34 UTC (permalink / raw)
To: 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, David Windsor
Add bpf_init_inode_xattr() kfunc for BPF LSM programs to atomically set
xattrs via the inode_init_security hook using lsm_get_xattr_slot().
The inode_init_security hook previously took the xattr array and count
as two separate output parameters (struct xattr *xattrs, int
*xattr_count), which BPF programs cannot write to. Pass the xattr state
as a single context object (struct xattr_ctx) instead, and have
bpf_init_inode_xattr() take that context directly. Update the existing
in-tree callers of inode_init_security to take and forward the new
xattr_ctx.
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.
Because we rely on the hook-specific ctx layout, the kfunc is
restricted to lsm/inode_init_security. Restrict the xattr names that
may be set via this kfunc to the bpf.* namespace.
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 | 106 +++++++++++++++++++++++++++++-
include/linux/bpf.h | 1 +
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 | 5 ++
kernel/bpf/bpf_lsm.c | 10 +++
kernel/bpf/trampoline.c | 3 +
security/bpf/hooks.c | 1 +
security/integrity/evm/evm_main.c | 8 ++-
security/security.c | 7 +-
security/selinux/hooks.c | 4 +-
security/smack/smack_lsm.c | 27 ++++----
14 files changed, 166 insertions(+), 38 deletions(-)
diff --git a/fs/bpf_fs_kfuncs.c b/fs/bpf_fs_kfuncs.c
index 768aca2dc0f0..7abc3f3d1a67 100644
--- a/fs/bpf_fs_kfuncs.c
+++ b/fs/bpf_fs_kfuncs.c
@@ -10,6 +10,7 @@
#include <linux/fsnotify.h>
#include <linux/file.h>
#include <linux/kernfs.h>
+#include <linux/lsm_hooks.h>
#include <linux/mm.h>
#include <linux/xattr.h>
@@ -374,6 +375,97 @@ __bpf_kfunc struct inode *bpf_real_inode(struct dentry *dentry)
return d_real_inode(dentry);
}
+static int bpf_xattrs_used(const struct xattr_ctx *ctx)
+{
+ const size_t prefix_len = sizeof(XATTR_BPF_LSM_SUFFIX) - 1;
+ int i, n = 0;
+
+ for (i = 0; i < *ctx->xattr_count; i++) {
+ const char *name = ctx->xattrs[i].name;
+
+ if (name && !strncmp(name, XATTR_BPF_LSM_SUFFIX, prefix_len))
+ n++;
+ }
+ return n;
+}
+
+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;
+
+ 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;
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) */
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;
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)
{
struct evm_xattr *xattr_data;
struct xattr *xattr, *evm_xattr;
+ struct xattr *xattrs;
bool evm_protected_xattrs = false;
int rc;
+ xattrs = xattr_ctx ? xattr_ctx->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(xattr_ctx);
/*
* 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..8f82a1352356 100644
--- a/security/security.c
+++ b/security/security.c
@@ -1334,6 +1334,7 @@ int security_inode_init_security(struct inode *inode, struct inode *dir,
{
struct lsm_static_call *scall;
struct xattr *new_xattrs = NULL;
+ struct xattr_ctx xattr_ctx;
int ret = -EOPNOTSUPP, xattr_count = 0;
if (unlikely(IS_PRIVATE(inode)))
@@ -1349,10 +1350,12 @@ int security_inode_init_security(struct inode *inode, struct inode *dir,
if (!new_xattrs)
return -ENOMEM;
}
+ xattr_ctx.xattrs = new_xattrs;
+ xattr_ctx.xattr_count = &xattr_count;
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,
+ &xattr_ctx);
if (ret && ret != -EOPNOTSUPP)
goto out;
/*
diff --git a/security/selinux/hooks.c b/security/selinux/hooks.c
index 1a713d96206f..faa8a6b9c45b 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 xattr_ctx *xattr_ctx)
{
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(xattr_ctx);
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..8ed5648a0116 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 xattr_ctx *xattr_ctx,
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(xattr_ctx);
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)
+ * @xattr_ctx: 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 xattr_ctx *xattr_ctx)
{
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(xattr_ctx,
+ 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(xattr_ctx,
+ 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] 7+ messages in thread* [PATCH bpf-next v3 2/2] selftests/bpf: add tests for bpf_init_inode_xattr kfunc
2026-06-18 20:34 [PATCH bpf-next v3 0/2] bpf: add bpf_init_inode_xattr kfunc for atomic inode labeling David Windsor
2026-06-18 20:34 ` [PATCH bpf-next v3 1/2] " David Windsor
@ 2026-06-18 20:34 ` David Windsor
2026-06-18 20:45 ` sashiko-bot
1 sibling, 1 reply; 7+ messages in thread
From: David Windsor @ 2026-06-18 20:34 UTC (permalink / raw)
To: 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, 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 | 105 +++++++++++++++++-
.../bpf/progs/test_init_inode_xattr.c | 31 ++++++
3 files changed, 140 insertions(+), 1 deletion(-)
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..69d3641ee2d8 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 xattr_ctx;
+extern int bpf_init_inode_xattr(struct xattr_ctx *xattr_ctx,
+ 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..0898898fb125 100644
--- a/tools/testing/selftests/bpf/prog_tests/fs_kfuncs.c
+++ b/tools/testing/selftests/bpf/prog_tests/fs_kfuncs.c
@@ -9,9 +9,10 @@
#include <test_progs.h>
#include "test_get_xattr.skel.h"
#include "test_set_remove_xattr.skel.h"
+#include "test_init_inode_xattr.skel.h"
#include "test_fsverity.skel.h"
-static const char testfile[] = "/tmp/test_progs_fs_kfuncs";
+static const char testfile[] = "/tmp/labelme";
static void test_get_xattr(const char *name, const char *value, bool allow_access)
{
@@ -268,6 +269,102 @@ 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];
+ const char *testfile_new = "/tmp/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;
+
+ 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
+
+/* At most INIT_INODE_XATTR_MAX programs can attach to inode_init_security. */
+static void test_init_inode_xattr_attach_cap(void)
+{
+ struct test_init_inode_xattr *skel[INIT_INODE_XATTR_MAX + 1] = {};
+ struct bpf_link *link[INIT_INODE_XATTR_MAX + 1] = {};
+ struct bpf_link *extra = NULL;
+ int i, err;
+
+ /* Fill all available xattr slots */
+ for (i = 0; i < INIT_INODE_XATTR_MAX; i++) {
+ skel[i] = test_init_inode_xattr__open_and_load();
+ if (!ASSERT_OK_PTR(skel[i], "open_and_load"))
+ goto out;
+
+ link[i] = bpf_program__attach_lsm(skel[i]->progs.test_init_inode_xattr);
+ if (!ASSERT_OK_PTR(link[i], "attach_within_cap"))
+ goto out;
+ }
+
+ skel[INIT_INODE_XATTR_MAX] = test_init_inode_xattr__open_and_load();
+ if (!ASSERT_OK_PTR(skel[INIT_INODE_XATTR_MAX], "open_and_load_extra"))
+ goto out;
+
+ /* New additions fail with -E2BIG */
+ extra = bpf_program__attach_lsm(skel[INIT_INODE_XATTR_MAX]->progs.test_init_inode_xattr);
+ err = -errno;
+ if (!ASSERT_ERR_PTR(extra, "attach_over_cap_should_fail")) {
+ bpf_link__destroy(extra);
+ goto out;
+ }
+ ASSERT_EQ(err, -E2BIG, "attach_over_cap_errno");
+
+ bpf_link__destroy(link[0]);
+ link[0] = NULL; /* avoid double free in cleanup */
+
+ /* Freeing a slot lets the extra program attach */
+ extra = bpf_program__attach_lsm(skel[INIT_INODE_XATTR_MAX]->progs.test_init_inode_xattr);
+ ASSERT_OK_PTR(extra, "attach_after_detach");
+
+out:
+ bpf_link__destroy(extra);
+ for (i = 0; i <= INIT_INODE_XATTR_MAX; i++) {
+ bpf_link__destroy(link[i]);
+ test_init_inode_xattr__destroy(skel[i]);
+ }
+}
+
void test_fs_kfuncs(void)
{
/* Matches xattr_names in progs/test_get_xattr.c */
@@ -286,6 +383,12 @@ void test_fs_kfuncs(void)
if (test__start_subtest("set_remove_xattr"))
test_set_remove_xattr();
+ if (test__start_subtest("init_inode_xattr"))
+ test_init_inode_xattr();
+
+ if (test__start_subtest("init_inode_xattr_attach_cap"))
+ test_init_inode_xattr_attach_cap();
+
if (test__start_subtest("fsverity"))
test_fsverity();
}
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..6f0e8b02ff88
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/test_init_inode_xattr.c
@@ -0,0 +1,31 @@
+// 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 init_result = -1;
+
+static const char xattr_name[] = "bpf.test_label";
+char xattr_value[] = "unconfined_u:object_r:user_home_t:s0";
+
+SEC("lsm.s/inode_init_security")
+int BPF_PROG(test_init_inode_xattr, struct inode *inode, struct inode *dir,
+ const struct qstr *qstr, struct xattr_ctx *xattr_ctx)
+{
+ struct bpf_dynptr value_ptr;
+ __u32 pid;
+
+ pid = bpf_get_current_pid_tgid() >> 32;
+ if (pid != monitored_pid)
+ return 0;
+
+ bpf_dynptr_from_mem(xattr_value, sizeof(xattr_value), 0, &value_ptr);
+ init_result = bpf_init_inode_xattr(xattr_ctx, xattr_name, &value_ptr);
+
+ return 0;
+}
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread