* [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs
@ 2026-05-22 13:56 Daniel Borkmann
2026-05-22 13:56 ` [PATCH bpf-next 2/3] selftests/bpf: Add bpffs xattr API coverage test Daniel Borkmann
` (3 more replies)
0 siblings, 4 replies; 12+ messages in thread
From: Daniel Borkmann @ 2026-05-22 13:56 UTC (permalink / raw)
To: bpf; +Cc: brauner, kpsingh, john.fastabend
Add support for extended attributes on bpffs inodes so that user space
and BPF LSM programs can attach metadata, for example, a content hash
or a security label - to a pinned object or directory. BPF LSM or user
space tooling can then uniformly look at this (e.g. security.bpf.*) in
similar way to other fs'es. The store is in-memory and non-persistent:
it lives only for the lifetime of the mount, like everything else in
bpffs. The modelling is similar to tmpfs.
bpf_fill_super() is open-coded instead of using simple_fill_super(),
because the root inode must now be allocated through bpf_fs_alloc_inode()
i.e. carry the bpf_fs_inode wrapper and come from the right cache -
which requires s_op (and s_xattr) to be installed before the first
inode is created.
bpf_fs_listxattr() is only reachable through the filesystem via
i_op->listxattr, so the BPF token inode is left untouched. Name-based
fsetxattr()/fgetxattr() on a token fd still work since the get/set
handlers are installed at the superblock.
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Cc: Christian Brauner <brauner@kernel.org>
---
kernel/bpf/inode.c | 254 ++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 237 insertions(+), 17 deletions(-)
diff --git a/kernel/bpf/inode.c b/kernel/bpf/inode.c
index 25c06a011825..638aa996cbaf 100644
--- a/kernel/bpf/inode.c
+++ b/kernel/bpf/inode.c
@@ -21,6 +21,9 @@
#include <linux/bpf.h>
#include <linux/bpf_trace.h>
#include <linux/kstrtox.h>
+#include <linux/xattr.h>
+#include <linux/security.h>
+
#include "preload/bpf_preload.h"
enum bpf_type {
@@ -30,6 +33,22 @@ enum bpf_type {
BPF_TYPE_LINK,
};
+struct bpf_fs_inode {
+ struct simple_xattrs *xattrs;
+ struct inode vfs_inode;
+};
+
+static inline struct bpf_fs_inode *BPF_FS_I(struct inode *inode)
+{
+ return container_of(inode, struct bpf_fs_inode, vfs_inode);
+}
+
+static struct kmem_cache *bpf_fs_inode_cachep __ro_after_init;
+
+static int bpf_fs_initxattrs(struct inode *inode,
+ const struct xattr *xattr_array, void *fs_info);
+static ssize_t bpf_fs_listxattr(struct dentry *dentry, char *buf, size_t size);
+
static void *bpf_any_get(void *raw, enum bpf_type type)
{
switch (type) {
@@ -94,10 +113,17 @@ static void *bpf_fd_probe_obj(u32 ufd, enum bpf_type *type)
}
static const struct inode_operations bpf_dir_iops;
+static const struct inode_operations bpf_symlink_iops;
-static const struct inode_operations bpf_prog_iops = { };
-static const struct inode_operations bpf_map_iops = { };
-static const struct inode_operations bpf_link_iops = { };
+static const struct inode_operations bpf_prog_iops = {
+ .listxattr = bpf_fs_listxattr,
+};
+static const struct inode_operations bpf_map_iops = {
+ .listxattr = bpf_fs_listxattr,
+};
+static const struct inode_operations bpf_link_iops = {
+ .listxattr = bpf_fs_listxattr,
+};
struct inode *bpf_get_inode(struct super_block *sb,
const struct inode *dir,
@@ -153,11 +179,19 @@ static struct dentry *bpf_mkdir(struct mnt_idmap *idmap, struct inode *dir,
struct dentry *dentry, umode_t mode)
{
struct inode *inode;
+ int ret;
inode = bpf_get_inode(dir->i_sb, dir, mode | S_IFDIR);
if (IS_ERR(inode))
return ERR_CAST(inode);
+ ret = security_inode_init_security(inode, dir, &dentry->d_name,
+ bpf_fs_initxattrs, NULL);
+ if (ret && ret != -EOPNOTSUPP) {
+ iput(inode);
+ return ERR_PTR(ret);
+ }
+
inode->i_op = &bpf_dir_iops;
inode->i_fop = &simple_dir_operations;
@@ -330,10 +364,20 @@ static int bpf_mkobj_ops(struct dentry *dentry, umode_t mode, void *raw,
const struct file_operations *fops)
{
struct inode *dir = dentry->d_parent->d_inode;
- struct inode *inode = bpf_get_inode(dir->i_sb, dir, mode);
+ struct inode *inode;
+ int ret;
+
+ inode = bpf_get_inode(dir->i_sb, dir, mode);
if (IS_ERR(inode))
return PTR_ERR(inode);
+ ret = security_inode_init_security(inode, dir, &dentry->d_name,
+ bpf_fs_initxattrs, NULL);
+ if (ret && ret != -EOPNOTSUPP) {
+ iput(inode);
+ return ret;
+ }
+
inode->i_op = iops;
inode->i_fop = fops;
inode->i_private = raw;
@@ -382,9 +426,11 @@ bpf_lookup(struct inode *dir, struct dentry *dentry, unsigned flags)
static int bpf_symlink(struct mnt_idmap *idmap, struct inode *dir,
struct dentry *dentry, const char *target)
{
- char *link = kstrdup(target, GFP_USER | __GFP_NOWARN);
struct inode *inode;
+ char *link;
+ int ret;
+ link = kstrdup(target, GFP_USER | __GFP_NOWARN);
if (!link)
return -ENOMEM;
@@ -394,13 +440,25 @@ static int bpf_symlink(struct mnt_idmap *idmap, struct inode *dir,
return PTR_ERR(inode);
}
- inode->i_op = &simple_symlink_inode_operations;
+ inode->i_op = &bpf_symlink_iops;
inode->i_link = link;
+ ret = security_inode_init_security(inode, dir, &dentry->d_name,
+ bpf_fs_initxattrs, NULL);
+ if (ret && ret != -EOPNOTSUPP) {
+ iput(inode);
+ return ret;
+ }
+
bpf_dentry_finalize(dentry, inode, dir);
return 0;
}
+static const struct inode_operations bpf_symlink_iops = {
+ .get_link = simple_get_link,
+ .listxattr = bpf_fs_listxattr,
+};
+
static const struct inode_operations bpf_dir_iops = {
.lookup = bpf_lookup,
.mkdir = bpf_mkdir,
@@ -409,6 +467,7 @@ static const struct inode_operations bpf_dir_iops = {
.rename = simple_rename,
.link = simple_link,
.unlink = simple_unlink,
+ .listxattr = bpf_fs_listxattr,
};
/* pin iterator link into bpffs */
@@ -762,22 +821,151 @@ static int bpf_show_options(struct seq_file *m, struct dentry *root)
return 0;
}
+static struct inode *bpf_fs_alloc_inode(struct super_block *sb)
+{
+ struct bpf_fs_inode *bi;
+
+ bi = alloc_inode_sb(sb, bpf_fs_inode_cachep, GFP_KERNEL);
+ if (!bi)
+ return NULL;
+ bi->xattrs = NULL;
+ return &bi->vfs_inode;
+}
+
static void bpf_destroy_inode(struct inode *inode)
{
+ struct bpf_fs_inode *bi = BPF_FS_I(inode);
enum bpf_type type;
if (S_ISLNK(inode->i_mode))
kfree(inode->i_link);
if (!bpf_inode_type(inode, &type))
bpf_any_put(inode->i_private, type);
- free_inode_nonrcu(inode);
+ if (bi->xattrs) {
+ simple_xattrs_free(bi->xattrs, NULL);
+ kfree(bi->xattrs);
+ }
+}
+
+static void bpf_free_inode(struct inode *inode)
+{
+ kmem_cache_free(bpf_fs_inode_cachep, BPF_FS_I(inode));
+}
+
+static int bpf_fs_xattr_get(const struct xattr_handler *handler,
+ struct dentry *unused, struct inode *inode,
+ const char *name, void *value, size_t size)
+{
+ struct simple_xattrs *xattrs;
+
+ name = xattr_full_name(handler, name);
+ xattrs = READ_ONCE(BPF_FS_I(inode)->xattrs);
+ if (!xattrs)
+ return -ENODATA;
+ return simple_xattr_get(xattrs, name, value, size);
+}
+
+static int bpf_fs_xattr_set(const struct xattr_handler *handler,
+ struct mnt_idmap *idmap, struct dentry *unused,
+ struct inode *inode, const char *name,
+ const void *value, size_t size, int flags)
+{
+ struct bpf_fs_inode *bi = BPF_FS_I(inode);
+ struct simple_xattrs *xattrs;
+ struct simple_xattr *old;
+
+ name = xattr_full_name(handler, name);
+ xattrs = simple_xattrs_lazy_alloc(&bi->xattrs, value, flags);
+ if (IS_ERR_OR_NULL(xattrs))
+ return PTR_ERR(xattrs);
+ old = simple_xattr_set(xattrs, name, value, size, flags);
+ if (IS_ERR(old))
+ return PTR_ERR(old);
+ simple_xattr_free_rcu(old);
+ inode_set_ctime_current(inode);
+ return 0;
+}
+
+static const struct xattr_handler bpf_fs_trusted_xattr_handler = {
+ .prefix = XATTR_TRUSTED_PREFIX,
+ .get = bpf_fs_xattr_get,
+ .set = bpf_fs_xattr_set,
+};
+
+static const struct xattr_handler bpf_fs_security_xattr_handler = {
+ .prefix = XATTR_SECURITY_PREFIX,
+ .get = bpf_fs_xattr_get,
+ .set = bpf_fs_xattr_set,
+};
+
+static const struct xattr_handler bpf_fs_user_xattr_handler = {
+ .prefix = XATTR_USER_PREFIX,
+ .get = bpf_fs_xattr_get,
+ .set = bpf_fs_xattr_set,
+};
+
+static const struct xattr_handler * const bpf_fs_xattr_handlers[] = {
+ &bpf_fs_trusted_xattr_handler,
+ &bpf_fs_security_xattr_handler,
+ &bpf_fs_user_xattr_handler,
+ NULL,
+};
+
+static ssize_t bpf_fs_listxattr(struct dentry *dentry, char *buf, size_t size)
+{
+ struct inode *inode = d_inode(dentry);
+
+ return simple_xattr_list(inode, READ_ONCE(BPF_FS_I(inode)->xattrs),
+ buf, size);
+}
+
+static int bpf_fs_initxattrs(struct inode *inode,
+ const struct xattr *xattr_array, void *fs_info)
+{
+ struct bpf_fs_inode *bi = BPF_FS_I(inode);
+ const struct xattr *xattr;
+ size_t len;
+
+ CLASS(simple_xattrs, xattrs)();
+ if (IS_ERR(xattrs))
+ return PTR_ERR(xattrs);
+
+ for (xattr = xattr_array; xattr->name != NULL; xattr++) {
+ CLASS(simple_xattr, new_xattr)(xattr->value, xattr->value_len);
+ if (IS_ERR(new_xattr))
+ break;
+
+ len = strlen(xattr->name) + 1;
+ new_xattr->name = kmalloc(XATTR_SECURITY_PREFIX_LEN + len,
+ GFP_KERNEL_ACCOUNT);
+ if (!new_xattr->name)
+ break;
+
+ memcpy(new_xattr->name, XATTR_SECURITY_PREFIX,
+ XATTR_SECURITY_PREFIX_LEN);
+ memcpy(new_xattr->name + XATTR_SECURITY_PREFIX_LEN,
+ xattr->name, len);
+
+ if (simple_xattr_add(xattrs, new_xattr))
+ break;
+ retain_and_null_ptr(new_xattr);
+ }
+
+ if (xattr->name != NULL)
+ return -ENOMEM;
+
+ /* Paired with: READ_ONCE() on ->xattrs, release/dependency pair. */
+ smp_store_release(&bi->xattrs, no_free_ptr(xattrs));
+ return 0;
}
const struct super_operations bpf_super_ops = {
.statfs = simple_statfs,
.drop_inode = inode_just_drop,
.show_options = bpf_show_options,
+ .alloc_inode = bpf_fs_alloc_inode,
.destroy_inode = bpf_destroy_inode,
+ .free_inode = bpf_free_inode,
};
enum {
@@ -996,25 +1184,38 @@ static int populate_bpffs(struct dentry *parent)
static int bpf_fill_super(struct super_block *sb, struct fs_context *fc)
{
- static const struct tree_descr bpf_rfiles[] = { { "" } };
struct bpf_mount_opts *opts = sb->s_fs_info;
struct inode *inode;
- int ret;
/* Mounting an instance of BPF FS requires privileges */
if (fc->user_ns != &init_user_ns && !capable(CAP_SYS_ADMIN))
return -EPERM;
- ret = simple_fill_super(sb, BPF_FS_MAGIC, bpf_rfiles);
- if (ret)
- return ret;
-
+ sb->s_blocksize = PAGE_SIZE;
+ sb->s_blocksize_bits = PAGE_SHIFT;
+ sb->s_magic = BPF_FS_MAGIC;
sb->s_op = &bpf_super_ops;
+ sb->s_xattr = bpf_fs_xattr_handlers;
+ sb->s_iflags |= SB_I_NOEXEC;
+ sb->s_iflags |= SB_I_NODEV;
+ sb->s_time_gran = 1;
+
+ inode = bpf_get_inode(sb, NULL, S_IFDIR | 0777);
+ if (IS_ERR(inode))
+ return PTR_ERR(inode);
+
+ inode->i_ino = 1;
+ inode->i_op = &bpf_dir_iops;
+ inode->i_fop = &simple_dir_operations;
+ set_nlink(inode, 2);
+
+ sb->s_root = d_make_root(inode);
+ if (!sb->s_root)
+ return -ENOMEM;
- inode = sb->s_root->d_inode;
+ inode = d_inode(sb->s_root);
inode->i_uid = opts->uid;
inode->i_gid = opts->gid;
- inode->i_op = &bpf_dir_iops;
inode->i_mode &= ~S_IALLUGO;
populate_bpffs(sb->s_root);
inode->i_mode |= S_ISVTX | opts->mode;
@@ -1080,18 +1281,37 @@ static struct file_system_type bpf_fs_type = {
.fs_flags = FS_USERNS_MOUNT,
};
+static void bpf_fs_inode_init_once(void *foo)
+{
+ struct bpf_fs_inode *bi = foo;
+
+ inode_init_once(&bi->vfs_inode);
+}
+
static int __init bpf_init(void)
{
int ret;
+ bpf_fs_inode_cachep = kmem_cache_create("bpf_fs_inode_cache",
+ sizeof(struct bpf_fs_inode), 0,
+ SLAB_RECLAIM_ACCOUNT | SLAB_ACCOUNT,
+ bpf_fs_inode_init_once);
+ if (!bpf_fs_inode_cachep)
+ return -ENOMEM;
+
ret = sysfs_create_mount_point(fs_kobj, "bpf");
if (ret)
- return ret;
+ goto out_cache;
ret = register_filesystem(&bpf_fs_type);
- if (ret)
+ if (ret) {
sysfs_remove_mount_point(fs_kobj, "bpf");
+ goto out_cache;
+ }
+ return 0;
+out_cache:
+ kmem_cache_destroy(bpf_fs_inode_cachep);
return ret;
}
fs_initcall(bpf_init);
--
2.43.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH bpf-next 2/3] selftests/bpf: Add bpffs xattr API coverage test
2026-05-22 13:56 [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs Daniel Borkmann
@ 2026-05-22 13:56 ` Daniel Borkmann
2026-05-22 14:59 ` sashiko-bot
2026-05-22 13:56 ` [PATCH bpf-next 3/3] selftests/bpf: Add xattr label test with BPF LSM Daniel Borkmann
` (2 subsequent siblings)
3 siblings, 1 reply; 12+ messages in thread
From: Daniel Borkmann @ 2026-05-22 13:56 UTC (permalink / raw)
To: bpf; +Cc: brauner, kpsingh, john.fastabend
Add basic bpffs xattr API coverage tests:
- all three supported namespaces (user.*, trusted.*, security.*) on a
single inode, plus listxattr enumerating them and removexattr,
- rejection of an unsupported namespace (bogus.*),
- flag and error semantics: XATTR_CREATE on an existing key (EEXIST),
XATTR_REPLACE on a missing key (ENODATA), removexattr of a missing
key (ENODATA), zero-length value round-trip, undersized getxattr
buffer (ERANGE) and the size-query form,
- every bpffs inode type: the mount root, a directory, and pinned map,
prog, link and symlink inodes (listxattr on each),
- that a child does not inherit the parent's xattrs (bpffs has no
propagation; an LSM-driven security_inode_init_security() is the only
path that seeds child xattrs),
- that re-pinning at a path that previously held a labeled object
yields a fresh, xattr-less inode,
- that the store is mount-scoped: after umount + fresh mount the pin
and its xattrs are gone.
Note that the security_inode_init_security() -> bpf_fs_initxattrs() path
is not covered here. BPF LSM today does not yet reserve LSM xattr slots
and the slot populate API is not exposed as a kfunc (on todo, but also
independent of this set).
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
.../selftests/bpf/prog_tests/bpffs_xattr.c | 325 ++++++++++++++++++
1 file changed, 325 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/bpffs_xattr.c
diff --git a/tools/testing/selftests/bpf/prog_tests/bpffs_xattr.c b/tools/testing/selftests/bpf/prog_tests/bpffs_xattr.c
new file mode 100644
index 000000000000..3d02e30da338
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/bpffs_xattr.c
@@ -0,0 +1,325 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Isovalent */
+
+#define _GNU_SOURCE
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/xattr.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <test_progs.h>
+
+#include "test_link_pinning.skel.h"
+
+static const char val_a[] = "value-a";
+static const char val_b[] = "value-b-different-length-than-a";
+
+static int mount_bpffs(char dir[64])
+{
+ int err;
+
+ strcpy(dir, "/tmp/bpffs_xattr.XXXXXX");
+ if (!mkdtemp(dir))
+ return -errno;
+ err = mount("bpf", dir, "bpf", 0, NULL);
+ if (err) {
+ err = -errno;
+ rmdir(dir);
+ }
+ return err;
+}
+
+static void umount_bpffs(const char *dir)
+{
+ umount(dir);
+ rmdir(dir);
+}
+
+static bool list_contains(const char *list, ssize_t list_len, const char *name)
+{
+ const char *p = list;
+
+ while (p < list + list_len) {
+ if (!strcmp(p, name))
+ return true;
+ p += strlen(p) + 1;
+ }
+ return false;
+}
+
+static void test_prefixes(void)
+{
+ char dir[64], list[256], buf[64];
+ ssize_t n;
+ int err;
+
+ if (!ASSERT_OK(mount_bpffs(dir), "mount"))
+ return;
+
+ if (!ASSERT_OK(setxattr(dir, "user.foo", val_a, sizeof(val_a),
+ XATTR_CREATE), "set_user"))
+ goto out;
+ if (!ASSERT_OK(setxattr(dir, "trusted.bar", val_a, sizeof(val_a),
+ XATTR_CREATE), "set_trusted"))
+ goto out;
+ if (!ASSERT_OK(setxattr(dir, "security.baz", val_a, sizeof(val_a),
+ XATTR_CREATE), "set_security"))
+ goto out;
+
+ err = getxattr(dir, "user.foo", buf, sizeof(buf));
+ ASSERT_EQ(err, sizeof(val_a), "get_user_len");
+ ASSERT_MEMEQ(buf, val_a, sizeof(val_a), "get_user_val");
+
+ err = getxattr(dir, "trusted.bar", buf, sizeof(buf));
+ ASSERT_EQ(err, sizeof(val_a), "get_trusted_len");
+ ASSERT_MEMEQ(buf, val_a, sizeof(val_a), "get_trusted_val");
+
+ err = getxattr(dir, "security.baz", buf, sizeof(buf));
+ ASSERT_EQ(err, sizeof(val_a), "get_security_len");
+ ASSERT_MEMEQ(buf, val_a, sizeof(val_a), "get_security_val");
+
+ n = listxattr(dir, list, sizeof(list));
+ if (!ASSERT_GT(n, 0, "listxattr"))
+ goto out;
+ ASSERT_TRUE(list_contains(list, n, "user.foo"), "list_user");
+ ASSERT_TRUE(list_contains(list, n, "trusted.bar"), "list_trusted");
+ ASSERT_TRUE(list_contains(list, n, "security.baz"), "list_security");
+
+ ASSERT_OK(removexattr(dir, "trusted.bar"), "remove_trusted");
+ err = getxattr(dir, "trusted.bar", buf, sizeof(buf));
+ ASSERT_EQ(err, -1, "get_after_remove");
+ ASSERT_EQ(errno, ENODATA, "get_after_remove_errno");
+
+ err = getxattr(dir, "user.foo", buf, sizeof(buf));
+ ASSERT_EQ(err, sizeof(val_a), "user_survives_remove");
+out:
+ umount_bpffs(dir);
+}
+
+static void test_unsupported_prefix(void)
+{
+ char dir[64];
+ int err;
+
+ if (!ASSERT_OK(mount_bpffs(dir), "mount"))
+ return;
+
+ err = setxattr(dir, "bogus.foo", val_a, sizeof(val_a), XATTR_CREATE);
+ ASSERT_EQ(err, -1, "set_bogus_ret");
+ ASSERT_EQ(errno, EOPNOTSUPP, "set_bogus_errno");
+
+ err = setxattr(dir, "system.posix_acl_default", val_a, sizeof(val_a),
+ XATTR_CREATE);
+ ASSERT_EQ(err, -1, "set_system_ret");
+
+ umount_bpffs(dir);
+}
+
+static void test_flags(void)
+{
+ char dir[64], buf[8];
+ int err;
+
+ if (!ASSERT_OK(mount_bpffs(dir), "mount"))
+ return;
+
+ err = setxattr(dir, "user.x", val_a, sizeof(val_a), XATTR_REPLACE);
+ ASSERT_EQ(err, -1, "replace_missing_ret");
+ ASSERT_EQ(errno, ENODATA, "replace_missing_errno");
+
+ err = removexattr(dir, "user.x");
+ ASSERT_EQ(err, -1, "remove_missing_ret");
+ ASSERT_EQ(errno, ENODATA, "remove_missing_errno");
+
+ ASSERT_OK(setxattr(dir, "user.empty", "", 0, XATTR_CREATE), "set_empty");
+ err = getxattr(dir, "user.empty", buf, sizeof(buf));
+ ASSERT_EQ(err, 0, "get_empty_len");
+
+ ASSERT_OK(setxattr(dir, "user.x", val_a, sizeof(val_a), XATTR_CREATE),
+ "create_x");
+ err = setxattr(dir, "user.x", val_b, sizeof(val_b), XATTR_CREATE);
+ ASSERT_EQ(err, -1, "create_existing_ret");
+ ASSERT_EQ(errno, EEXIST, "create_existing_errno");
+
+ ASSERT_OK(setxattr(dir, "user.x", val_b, sizeof(val_b), XATTR_REPLACE),
+ "replace_existing");
+
+ err = getxattr(dir, "user.x", buf, sizeof(buf));
+ ASSERT_EQ(err, -1, "get_too_small_ret");
+ ASSERT_EQ(errno, ERANGE, "get_too_small_errno");
+
+ err = getxattr(dir, "user.x", NULL, 0);
+ ASSERT_EQ(err, sizeof(val_b), "get_query_len");
+
+ umount_bpffs(dir);
+}
+
+static void test_inode_types(void)
+{
+ char dir[64], child[128], map_path[128], prog_path[128];
+ char link_path[128], sym_path[128];
+ struct test_link_pinning *skel = NULL;
+ int map_fd = -1, err;
+ char list[128], buf[64];
+
+ if (!ASSERT_OK(mount_bpffs(dir), "mount"))
+ return;
+
+ /* mount root inode */
+ ASSERT_OK(setxattr(dir, "user.root", val_a, sizeof(val_a), XATTR_CREATE),
+ "root_set");
+ err = getxattr(dir, "user.root", buf, sizeof(buf));
+ ASSERT_EQ(err, sizeof(val_a), "root_get");
+ ASSERT_GT(listxattr(dir, list, sizeof(list)), 0, "root_list");
+
+ /* directory: confirm no inheritance from parent */
+ snprintf(child, sizeof(child), "%s/d", dir);
+ if (!ASSERT_OK(mkdir(child, 0755), "mkdir"))
+ goto out;
+ err = getxattr(child, "user.root", buf, sizeof(buf));
+ ASSERT_EQ(err, -1, "no_inherit_ret");
+ ASSERT_EQ(errno, ENODATA, "no_inherit_errno");
+ ASSERT_OK(setxattr(child, "user.dir", val_a, sizeof(val_a), XATTR_CREATE),
+ "dir_set");
+ ASSERT_GT(listxattr(child, list, sizeof(list)), 0, "dir_list");
+
+ /* map pin */
+ snprintf(map_path, sizeof(map_path), "%s/m", dir);
+ map_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "x", 4, 4, 1, NULL);
+ if (!ASSERT_GE(map_fd, 0, "map_create"))
+ goto out;
+ if (!ASSERT_OK(bpf_obj_pin(map_fd, map_path), "map_pin"))
+ goto out;
+ ASSERT_OK(setxattr(map_path, "user.map", val_a, sizeof(val_a),
+ XATTR_CREATE), "map_set");
+ ASSERT_GT(listxattr(map_path, list, sizeof(list)), 0, "map_list");
+
+ /* prog pin + link pin via a noop raw_tp prog */
+ skel = test_link_pinning__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "skel"))
+ goto out;
+
+ snprintf(prog_path, sizeof(prog_path), "%s/p", dir);
+ if (!ASSERT_OK(bpf_obj_pin(bpf_program__fd(skel->progs.raw_tp_prog),
+ prog_path), "prog_pin"))
+ goto out;
+ ASSERT_OK(setxattr(prog_path, "user.prog", val_a, sizeof(val_a),
+ XATTR_CREATE), "prog_set");
+ ASSERT_GT(listxattr(prog_path, list, sizeof(list)), 0, "prog_list");
+
+ if (!ASSERT_OK(test_link_pinning__attach(skel), "skel_attach"))
+ goto out;
+
+ snprintf(link_path, sizeof(link_path), "%s/l", dir);
+ if (!ASSERT_OK(bpf_obj_pin(bpf_link__fd(skel->links.raw_tp_prog),
+ link_path), "link_pin"))
+ goto out;
+ ASSERT_OK(setxattr(link_path, "user.link", val_a, sizeof(val_a),
+ XATTR_CREATE), "link_set");
+ ASSERT_GT(listxattr(link_path, list, sizeof(list)), 0, "link_list");
+
+ /* symlink: user.* is rejected by VFS on S_IFLNK; trusted.* works */
+ snprintf(sym_path, sizeof(sym_path), "%s/s", dir);
+ if (!ASSERT_OK(symlink("m", sym_path), "symlink_create"))
+ goto out;
+ ASSERT_OK(lsetxattr(sym_path, "trusted.sym", val_a, sizeof(val_a),
+ XATTR_CREATE), "sym_set");
+ ASSERT_GT(llistxattr(sym_path, list, sizeof(list)), 0, "sym_list");
+out:
+ test_link_pinning__destroy(skel);
+ if (map_fd >= 0)
+ close(map_fd);
+ umount_bpffs(dir);
+}
+
+static void test_repin_same_path(void)
+{
+ int map_a = -1, map_b = -1, err;
+ char dir[64], path[128], buf[16];
+
+ if (!ASSERT_OK(mount_bpffs(dir), "mount"))
+ return;
+ snprintf(path, sizeof(path), "%s/m", dir);
+
+ map_a = bpf_map_create(BPF_MAP_TYPE_ARRAY, "a", 4, 4, 1, NULL);
+ if (!ASSERT_GE(map_a, 0, "map_a"))
+ goto out;
+ if (!ASSERT_OK(bpf_obj_pin(map_a, path), "pin_a"))
+ goto out;
+ ASSERT_OK(setxattr(path, "user.k", val_a, sizeof(val_a), XATTR_CREATE),
+ "set_a");
+ if (!ASSERT_OK(unlink(path), "unlink_a"))
+ goto out;
+
+ map_b = bpf_map_create(BPF_MAP_TYPE_ARRAY, "b", 4, 4, 1, NULL);
+ if (!ASSERT_GE(map_b, 0, "map_b"))
+ goto out;
+ if (!ASSERT_OK(bpf_obj_pin(map_b, path), "pin_b"))
+ goto out;
+
+ err = getxattr(path, "user.k", buf, sizeof(buf));
+ ASSERT_EQ(err, -1, "fresh_inode_ret");
+ ASSERT_EQ(errno, ENODATA, "fresh_inode_errno");
+out:
+ if (map_a >= 0)
+ close(map_a);
+ if (map_b >= 0)
+ close(map_b);
+ umount_bpffs(dir);
+}
+
+static void test_umount_lifetime(void)
+{
+ char dir[64], path[128], buf[16];
+ int map_fd = -1, err;
+
+ if (!ASSERT_OK(mount_bpffs(dir), "mount"))
+ return;
+ snprintf(path, sizeof(path), "%s/m", dir);
+
+ map_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "x", 4, 4, 1, NULL);
+ if (!ASSERT_GE(map_fd, 0, "map_create"))
+ goto out;
+ if (!ASSERT_OK(bpf_obj_pin(map_fd, path), "pin"))
+ goto out;
+ ASSERT_OK(setxattr(path, "user.k", val_a, sizeof(val_a), XATTR_CREATE),
+ "set");
+
+ close(map_fd);
+ map_fd = -1;
+
+ if (!ASSERT_OK(umount(dir), "umount"))
+ goto out;
+ if (!ASSERT_OK(mount("bpf", dir, "bpf", 0, NULL), "mount_fresh"))
+ goto out;
+
+ err = access(path, F_OK);
+ ASSERT_EQ(err, -1, "path_gone_ret");
+ ASSERT_EQ(errno, ENOENT, "path_gone_errno");
+
+ err = getxattr(dir, "user.k", buf, sizeof(buf));
+ ASSERT_EQ(err, -1, "root_xattr_gone_ret");
+ ASSERT_EQ(errno, ENODATA, "root_xattr_gone_errno");
+out:
+ if (map_fd >= 0)
+ close(map_fd);
+ umount(dir);
+ rmdir(dir);
+}
+
+void test_bpffs_xattr(void)
+{
+ if (test__start_subtest("prefixes"))
+ test_prefixes();
+ if (test__start_subtest("unsupported_prefix"))
+ test_unsupported_prefix();
+ if (test__start_subtest("flags"))
+ test_flags();
+ if (test__start_subtest("inode_types"))
+ test_inode_types();
+ if (test__start_subtest("repin_same_path"))
+ test_repin_same_path();
+ if (test__start_subtest("umount_lifetime"))
+ test_umount_lifetime();
+}
--
2.43.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH bpf-next 3/3] selftests/bpf: Add xattr label test with BPF LSM
2026-05-22 13:56 [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs Daniel Borkmann
2026-05-22 13:56 ` [PATCH bpf-next 2/3] selftests/bpf: Add bpffs xattr API coverage test Daniel Borkmann
@ 2026-05-22 13:56 ` Daniel Borkmann
2026-05-22 15:19 ` sashiko-bot
2026-05-22 14:48 ` [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs sashiko-bot
2026-05-27 12:19 ` Christian Brauner
3 siblings, 1 reply; 12+ messages in thread
From: Daniel Borkmann @ 2026-05-22 13:56 UTC (permalink / raw)
To: bpf; +Cc: brauner, kpsingh, john.fastabend
Exercise bpffs extended attribute support together with a BPF LSM
that turns a security.bpf.* label into an immutable, lifecycle-bound
attribute. The example is modeling a user space application invoked
during init that labels pinned BPF objects via "security.bpf.foo"
and having the LSM locking it down. All tests combined now:
# LDLIBS=-static PKG_CONFIG='pkg-config --static' ./vmtest.sh -- ./test_progs -t bpffs_xattr
[...]
./test_progs -t bpffs_xattr
[ 1.654579] bpf_testmod: loading out-of-tree module taints kernel.
[ 1.655378] bpf_testmod: module verification failed: signature and/or required key missing - tainting kernel
#34/1 bpffs_xattr/prefixes:OK
#34/2 bpffs_xattr/unsupported_prefix:OK
#34/3 bpffs_xattr/flags:OK
#34/4 bpffs_xattr/inode_types:OK
#34/5 bpffs_xattr/repin_same_path:OK
#34/6 bpffs_xattr/umount_lifetime:OK
#34 bpffs_xattr:OK
#35 bpffs_xattr_label:OK
Summary: 2/6 PASSED, 0 SKIPPED, 0 FAILED
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
---
.../bpf/prog_tests/bpffs_xattr_label.c | 114 ++++++++++++++++++
.../selftests/bpf/progs/bpffs_xattr_label.c | 102 ++++++++++++++++
2 files changed, 216 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/bpffs_xattr_label.c
create mode 100644 tools/testing/selftests/bpf/progs/bpffs_xattr_label.c
diff --git a/tools/testing/selftests/bpf/prog_tests/bpffs_xattr_label.c b/tools/testing/selftests/bpf/prog_tests/bpffs_xattr_label.c
new file mode 100644
index 000000000000..384dde8bb30f
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/bpffs_xattr_label.c
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Isovalent */
+
+#define _GNU_SOURCE
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/xattr.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <test_progs.h>
+
+#include "bpffs_xattr_label.skel.h"
+
+/*
+ * Exercises the bpffs xattr support together with a BPF LSM example:
+ *
+ * 1. pin a BPF map and create a directory in bpffs,
+ * 2. attach the labels (security.bpf.foo) with XATTR_CREATE,
+ * 3. confirm the kernel (BPF LSM) reads back exactly what userspace set,
+ * 4. confirm the label cannot be changed/removed and the pin cannot be
+ * unlinked/rmdir'd/renamed while labeled,
+ * 5. confirm an unlabeled pin is unaffected (control).
+ */
+
+static const char value_foo[] = "0123456789abcdef0123456789abcdef";
+static const char xattr_foo[] = "security.bpf.foo";
+
+void test_bpffs_xattr_label(void)
+{
+ char map_path[128], labeled_dir[128], plain_dir[128], rename_to[128];
+ char dir[] = "/tmp/bpffs_xattr_label.XXXXXX";
+ struct bpffs_xattr_label *skel = NULL;
+ bool mounted = false;
+ int map_fd = -1, err;
+ char out[64];
+
+ if (!ASSERT_OK_PTR(mkdtemp(dir), "mkdtemp"))
+ return;
+
+ err = mount("bpf", dir, "bpf", 0, NULL);
+ if (!ASSERT_OK(err, "mount_bpffs"))
+ goto out;
+ mounted = true;
+
+ snprintf(map_path, sizeof(map_path), "%s/themap", dir);
+ snprintf(labeled_dir, sizeof(labeled_dir), "%s/labeled", dir);
+ snprintf(plain_dir, sizeof(plain_dir), "%s/plain", dir);
+ snprintf(rename_to, sizeof(rename_to), "%s/renamed", dir);
+
+ map_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "label_map", 4, 4, 1, NULL);
+ if (!ASSERT_GE(map_fd, 0, "map_create"))
+ goto out;
+ if (!ASSERT_OK(bpf_obj_pin(map_fd, map_path), "map_pin"))
+ goto out;
+ if (!ASSERT_OK(mkdir(labeled_dir, 0755), "mkdir_labeled"))
+ goto out;
+ if (!ASSERT_OK(mkdir(plain_dir, 0755), "mkdir_plain"))
+ goto out;
+
+ skel = bpffs_xattr_label__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "skel_open_load"))
+ goto out;
+ skel->bss->monitored_pid = getpid();
+ if (!ASSERT_OK(bpffs_xattr_label__attach(skel), "skel_attach"))
+ goto out;
+
+ err = setxattr(map_path, xattr_foo, value_foo, sizeof(value_foo),
+ XATTR_CREATE);
+ if (!ASSERT_OK(err, "setxattr_map_create"))
+ goto out;
+ ASSERT_OK(setxattr(labeled_dir, xattr_foo, value_foo, sizeof(value_foo),
+ XATTR_CREATE), "setxattr_dir_create");
+
+ err = getxattr(map_path, xattr_foo, out, sizeof(out));
+ ASSERT_EQ(err, sizeof(value_foo), "getxattr_len");
+ ASSERT_MEMEQ(out, value_foo, sizeof(value_foo), "getxattr_value");
+
+ ASSERT_EQ(skel->bss->read_value_len, sizeof(value_foo), "bpf_read_len");
+ ASSERT_MEMEQ(skel->bss->read_value, value_foo, sizeof(value_foo),
+ "bpf_read_value");
+
+ err = setxattr(map_path, xattr_foo, "changed", sizeof("changed"), 0);
+ ASSERT_EQ(err, -1, "setxattr_replace_ret");
+ ASSERT_EQ(errno, EPERM, "setxattr_replace_errno");
+
+ ASSERT_EQ(removexattr(map_path, xattr_foo), -1, "removexattr_ret");
+ ASSERT_EQ(errno, EPERM, "removexattr_errno");
+
+ ASSERT_EQ(unlink(map_path), -1, "unlink_labeled_ret");
+ ASSERT_EQ(errno, EPERM, "unlink_labeled_errno");
+ ASSERT_EQ(rename(map_path, rename_to), -1, "rename_labeled_ret");
+ ASSERT_EQ(errno, EPERM, "rename_labeled_errno");
+
+ ASSERT_EQ(rmdir(labeled_dir), -1, "rmdir_labeled_ret");
+ ASSERT_EQ(errno, EPERM, "rmdir_labeled_errno");
+
+ ASSERT_OK(rmdir(plain_dir), "rmdir_plain");
+
+ err = getxattr(map_path, xattr_foo, out, sizeof(out));
+ ASSERT_EQ(err, sizeof(value_foo), "getxattr_len_after");
+ ASSERT_MEMEQ(out, value_foo, sizeof(value_foo), "getxattr_value_after");
+
+out:
+ bpffs_xattr_label__destroy(skel);
+ if (map_fd >= 0)
+ close(map_fd);
+ if (mounted) {
+ unlink(map_path);
+ rmdir(labeled_dir);
+ rmdir(plain_dir);
+ umount(dir);
+ }
+ rmdir(dir);
+}
diff --git a/tools/testing/selftests/bpf/progs/bpffs_xattr_label.c b/tools/testing/selftests/bpf/progs/bpffs_xattr_label.c
new file mode 100644
index 000000000000..3891611c3b1c
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/bpffs_xattr_label.c
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Isovalent */
+
+#include "vmlinux.h"
+#include <errno.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include "bpf_kfuncs.h"
+#include "bpf_misc.h"
+
+const char xattr_foo[] = "security.bpf.foo";
+char _license[] SEC("license") = "GPL";
+
+__u32 monitored_pid;
+
+char read_value[64];
+int read_value_len;
+
+char label_check_buf[64];
+char name_buf[64];
+
+static __always_inline bool name_is_foo(const char *name)
+{
+ bpf_probe_read_kernel(name_buf, sizeof(name_buf), name);
+ return !bpf_strncmp(name_buf, sizeof(xattr_foo), xattr_foo);
+}
+
+static __always_inline bool has_label(struct dentry *dentry)
+{
+ struct bpf_dynptr ptr;
+
+ bpf_dynptr_from_mem(label_check_buf, sizeof(label_check_buf), 0, &ptr);
+ return bpf_get_dentry_xattr(dentry, xattr_foo, &ptr) >= 0;
+}
+
+SEC("lsm.s/inode_setxattr")
+int BPF_PROG(label_setxattr, struct mnt_idmap *idmap, struct dentry *dentry,
+ const char *name, const void *value, size_t size, int flags)
+{
+ if ((bpf_get_current_pid_tgid() >> 32) != monitored_pid)
+ return 0;
+ if (!name_is_foo(name))
+ return 0;
+ if (has_label(dentry))
+ return -EPERM;
+ return 0;
+}
+
+SEC("lsm.s/inode_removexattr")
+int BPF_PROG(label_removexattr, struct mnt_idmap *idmap, struct dentry *dentry,
+ const char *name)
+{
+ if ((bpf_get_current_pid_tgid() >> 32) != monitored_pid)
+ return 0;
+ if (!name_is_foo(name))
+ return 0;
+ return -EPERM;
+}
+
+SEC("lsm.s/inode_getxattr")
+int BPF_PROG(label_getxattr, struct dentry *dentry, const char *name)
+{
+ struct bpf_dynptr ptr;
+ int ret;
+
+ if ((bpf_get_current_pid_tgid() >> 32) != monitored_pid)
+ return 0;
+ if (!name_is_foo(name))
+ return 0;
+
+ bpf_dynptr_from_mem(read_value, sizeof(read_value), 0, &ptr);
+ ret = bpf_get_dentry_xattr(dentry, xattr_foo, &ptr);
+ if (ret >= 0)
+ read_value_len = ret;
+ return 0;
+}
+
+SEC("lsm.s/inode_unlink")
+int BPF_PROG(label_unlink, struct inode *dir, struct dentry *victim)
+{
+ if ((bpf_get_current_pid_tgid() >> 32) != monitored_pid)
+ return 0;
+ return has_label(victim) ? -EPERM : 0;
+}
+
+SEC("lsm.s/inode_rmdir")
+int BPF_PROG(label_rmdir, struct inode *dir, struct dentry *victim)
+{
+ if ((bpf_get_current_pid_tgid() >> 32) != monitored_pid)
+ return 0;
+ return has_label(victim) ? -EPERM : 0;
+}
+
+SEC("lsm.s/inode_rename")
+int BPF_PROG(label_rename, struct inode *old_dir, struct dentry *old_dentry,
+ struct inode *new_dir, struct dentry *new_dentry,
+ unsigned int flags)
+{
+ if ((bpf_get_current_pid_tgid() >> 32) != monitored_pid)
+ return 0;
+ return has_label(old_dentry) ? -EPERM : 0;
+}
--
2.43.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs
2026-05-22 13:56 [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs Daniel Borkmann
2026-05-22 13:56 ` [PATCH bpf-next 2/3] selftests/bpf: Add bpffs xattr API coverage test Daniel Borkmann
2026-05-22 13:56 ` [PATCH bpf-next 3/3] selftests/bpf: Add xattr label test with BPF LSM Daniel Borkmann
@ 2026-05-22 14:48 ` sashiko-bot
2026-05-22 15:09 ` Daniel Borkmann
2026-05-27 12:19 ` Christian Brauner
3 siblings, 1 reply; 12+ messages in thread
From: sashiko-bot @ 2026-05-22 14:48 UTC (permalink / raw)
To: Daniel Borkmann; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Unprivileged users can exhaust kernel memory via unbounded user.* extended attributes on bpffs.
- [High] NULL pointer dereference in bpf_fs_initxattrs() if an LSM returns a NULL xattr value.
--
commit 57543eec66ace3d4d002256a01cff4f1797f61e2
Author: Daniel Borkmann <daniel@iogearbox.net>
bpf: Add simple xattr support to bpffs
This commit adds support for extended attributes on bpffs inodes, allowing user
space and BPF LSM programs to attach metadata to pinned objects or directories.
The xattr store is in-memory and lives only for the lifetime of the mount.
> diff --git a/kernel/bpf/inode.c b/kernel/bpf/inode.c
> index 25c06a0118258..638aa996cbaf4 100644
> --- a/kernel/bpf/inode.c
> +++ b/kernel/bpf/inode.c
[ ... ]
> @@ -762,22 +821,151 @@ static int bpf_show_options(struct seq_file *m, struct dentry *root)
> return 0;
> }
[ ... ]
> +static int bpf_fs_xattr_set(const struct xattr_handler *handler,
> + struct mnt_idmap *idmap, struct dentry *unused,
> + struct inode *inode, const char *name,
> + const void *value, size_t size, int flags)
> +{
> + struct bpf_fs_inode *bi = BPF_FS_I(inode);
> + struct simple_xattrs *xattrs;
> + struct simple_xattr *old;
> +
> + name = xattr_full_name(handler, name);
> + xattrs = simple_xattrs_lazy_alloc(&bi->xattrs, value, flags);
> + if (IS_ERR_OR_NULL(xattrs))
> + return PTR_ERR(xattrs);
> + old = simple_xattr_set(xattrs, name, value, size, flags);
[Severity: High]
Does this code allow an unprivileged user to exhaust kernel memory?
Because bpffs can be mounted in an unprivileged user namespace
(FS_USERNS_MOUNT), a user could create directories and repeatedly set user.*
extended attributes.
Since simple_xattr_set() does not enforce per-inode limits like
simple_xattr_set_limited() does, couldn't this lead to an unbounded memory
allocation and soft lockups during rhashtable traversals?
> + if (IS_ERR(old))
> + return PTR_ERR(old);
> + simple_xattr_free_rcu(old);
> + inode_set_ctime_current(inode);
> + return 0;
> +}
[ ... ]
> +static int bpf_fs_initxattrs(struct inode *inode,
> + const struct xattr *xattr_array, void *fs_info)
> +{
> + struct bpf_fs_inode *bi = BPF_FS_I(inode);
> + const struct xattr *xattr;
> + size_t len;
> +
> + CLASS(simple_xattrs, xattrs)();
> + if (IS_ERR(xattrs))
> + return PTR_ERR(xattrs);
> +
> + for (xattr = xattr_array; xattr->name != NULL; xattr++) {
> + CLASS(simple_xattr, new_xattr)(xattr->value, xattr->value_len);
> + if (IS_ERR(new_xattr))
> + break;
> +
> + len = strlen(xattr->name) + 1;
> + new_xattr->name = kmalloc(XATTR_SECURITY_PREFIX_LEN + len,
[Severity: High]
Can this cause a NULL pointer dereference?
If an LSM program returns a NULL xattr value, the simple_xattr class
constructor will call simple_xattr_alloc() which can return NULL.
The IS_ERR(new_xattr) check evaluates to false for a NULL pointer, leading
to a NULL pointer dereference when accessing new_xattr->name here.
> + GFP_KERNEL_ACCOUNT);
> + if (!new_xattr->name)
> + break;
> +
> + memcpy(new_xattr->name, XATTR_SECURITY_PREFIX,
> + XATTR_SECURITY_PREFIX_LEN);
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260522135626.120732-1-daniel@iogearbox.net?part=1
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next 2/3] selftests/bpf: Add bpffs xattr API coverage test
2026-05-22 13:56 ` [PATCH bpf-next 2/3] selftests/bpf: Add bpffs xattr API coverage test Daniel Borkmann
@ 2026-05-22 14:59 ` sashiko-bot
0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-05-22 14:59 UTC (permalink / raw)
To: Daniel Borkmann; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The test `test_umount_lifetime` attempts to verify that root directory xattrs are cleared upon umount, but trivially succeeds because the xattr was never actually set on the root directory.
--
commit eee62f5b4a9e13a37724984ba00f475e46c7075f
Author: Daniel Borkmann <daniel@iogearbox.net>
selftests/bpf: Add bpffs xattr API coverage test
This commit adds basic bpffs xattr API coverage tests, including validation
of supported namespaces, flag semantics, inode types, and mount-scoped
lifetimes.
> diff --git a/tools/testing/selftests/bpf/prog_tests/bpffs_xattr.c b/tools/testing/selftests/bpf/prog_tests/bpffs_xattr.c
> new file mode 100644
> index 0000000000000..3d02e30da338e
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/bpffs_xattr.c
[ ... ]
> +static void test_umount_lifetime(void)
> +{
> + char dir[64], path[128], buf[16];
> + int map_fd = -1, err;
> +
> + if (!ASSERT_OK(mount_bpffs(dir), "mount"))
> + return;
> + snprintf(path, sizeof(path), "%s/m", dir);
> +
> + map_fd = bpf_map_create(BPF_MAP_TYPE_ARRAY, "x", 4, 4, 1, NULL);
> + if (!ASSERT_GE(map_fd, 0, "map_create"))
> + goto out;
> + if (!ASSERT_OK(bpf_obj_pin(map_fd, path), "pin"))
> + goto out;
> + ASSERT_OK(setxattr(path, "user.k", val_a, sizeof(val_a), XATTR_CREATE),
> + "set");
> +
> + close(map_fd);
> + map_fd = -1;
> +
> + if (!ASSERT_OK(umount(dir), "umount"))
> + goto out;
> + if (!ASSERT_OK(mount("bpf", dir, "bpf", 0, NULL), "mount_fresh"))
> + goto out;
> +
> + err = access(path, F_OK);
> + ASSERT_EQ(err, -1, "path_gone_ret");
> + ASSERT_EQ(errno, ENOENT, "path_gone_errno");
> +
> + err = getxattr(dir, "user.k", buf, sizeof(buf));
[Severity: Medium]
Does this test actually verify the root directory's xattr lifetime?
It looks like the user.k xattr is set on the pinned map file (path) earlier
in the function, but after remounting, this check looks for the xattr on the
mount root (dir).
Because user.k was never set on dir initially, won't this check trivially
succeed with ENODATA?
> + ASSERT_EQ(err, -1, "root_xattr_gone_ret");
> + ASSERT_EQ(errno, ENODATA, "root_xattr_gone_errno");
> +out:
> + if (map_fd >= 0)
> + close(map_fd);
> + umount(dir);
> + rmdir(dir);
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260522135626.120732-1-daniel@iogearbox.net?part=2
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs
2026-05-22 14:48 ` [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs sashiko-bot
@ 2026-05-22 15:09 ` Daniel Borkmann
2026-05-22 22:21 ` Daniel Borkmann
0 siblings, 1 reply; 12+ messages in thread
From: Daniel Borkmann @ 2026-05-22 15:09 UTC (permalink / raw)
To: sashiko-reviews; +Cc: bpf
On 5/22/26 4:48 PM, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
> - [High] Unprivileged users can exhaust kernel memory via unbounded user.* extended attributes on bpffs.
> - [High] NULL pointer dereference in bpf_fs_initxattrs() if an LSM returns a NULL xattr value.
> --
>
> commit 57543eec66ace3d4d002256a01cff4f1797f61e2
> Author: Daniel Borkmann <daniel@iogearbox.net>
>
> bpf: Add simple xattr support to bpffs
>
> This commit adds support for extended attributes on bpffs inodes, allowing user
> space and BPF LSM programs to attach metadata to pinned objects or directories.
> The xattr store is in-memory and lives only for the lifetime of the mount.
>
>> diff --git a/kernel/bpf/inode.c b/kernel/bpf/inode.c
>> index 25c06a0118258..638aa996cbaf4 100644
>> --- a/kernel/bpf/inode.c
>> +++ b/kernel/bpf/inode.c
>
> [ ... ]
>
>> @@ -762,22 +821,151 @@ static int bpf_show_options(struct seq_file *m, struct dentry *root)
>> return 0;
>> }
>
> [ ... ]
>
>> +static int bpf_fs_xattr_set(const struct xattr_handler *handler,
>> + struct mnt_idmap *idmap, struct dentry *unused,
>> + struct inode *inode, const char *name,
>> + const void *value, size_t size, int flags)
>> +{
>> + struct bpf_fs_inode *bi = BPF_FS_I(inode);
>> + struct simple_xattrs *xattrs;
>> + struct simple_xattr *old;
>> +
>> + name = xattr_full_name(handler, name);
>> + xattrs = simple_xattrs_lazy_alloc(&bi->xattrs, value, flags);
>> + if (IS_ERR_OR_NULL(xattrs))
>> + return PTR_ERR(xattrs);
>> + old = simple_xattr_set(xattrs, name, value, size, flags);
>
> [Severity: High]
> Does this code allow an unprivileged user to exhaust kernel memory?
>
> Because bpffs can be mounted in an unprivileged user namespace
> (FS_USERNS_MOUNT), a user could create directories and repeatedly set user.*
> extended attributes.
>
> Since simple_xattr_set() does not enforce per-inode limits like
> simple_xattr_set_limited() does, couldn't this lead to an unbounded memory
> allocation and soft lockups during rhashtable traversals?
Ok, for user.* simple_xattr_set_limited should be the choice then, will
fix in a v2.
>> + if (IS_ERR(old))
>> + return PTR_ERR(old);
>> + simple_xattr_free_rcu(old);
>> + inode_set_ctime_current(inode);
>> + return 0;
>> +}
>
> [ ... ]
>
>> +static int bpf_fs_initxattrs(struct inode *inode,
>> + const struct xattr *xattr_array, void *fs_info)
>> +{
>> + struct bpf_fs_inode *bi = BPF_FS_I(inode);
>> + const struct xattr *xattr;
>> + size_t len;
>> +
>> + CLASS(simple_xattrs, xattrs)();
>> + if (IS_ERR(xattrs))
>> + return PTR_ERR(xattrs);
>> +
>> + for (xattr = xattr_array; xattr->name != NULL; xattr++) {
>> + CLASS(simple_xattr, new_xattr)(xattr->value, xattr->value_len);
>> + if (IS_ERR(new_xattr))
>> + break;
>> +
>> + len = strlen(xattr->name) + 1;
>> + new_xattr->name = kmalloc(XATTR_SECURITY_PREFIX_LEN + len,
>
> [Severity: High]
> Can this cause a NULL pointer dereference?
>
> If an LSM program returns a NULL xattr value, the simple_xattr class
> constructor will call simple_xattr_alloc() which can return NULL.
>
> The IS_ERR(new_xattr) check evaluates to false for a NULL pointer, leading
> to a NULL pointer dereference when accessing new_xattr->name here.
(false positive: security_inode_init_security() array always have non-NULL
value paired with non-NULL name.. also in line with shmem_initxattrs)
>> + GFP_KERNEL_ACCOUNT);
>> + if (!new_xattr->name)
>> + break;
>> +
>> + memcpy(new_xattr->name, XATTR_SECURITY_PREFIX,
>> + XATTR_SECURITY_PREFIX_LEN);
>
> [ ... ]
>
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next 3/3] selftests/bpf: Add xattr label test with BPF LSM
2026-05-22 13:56 ` [PATCH bpf-next 3/3] selftests/bpf: Add xattr label test with BPF LSM Daniel Borkmann
@ 2026-05-22 15:19 ` sashiko-bot
0 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-05-22 15:19 UTC (permalink / raw)
To: Daniel Borkmann; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Medium] The `label_rename` LSM hook fails to validate `new_dentry`, allowing bypass of the lock-down policy by renaming over a labeled file.
- [Medium] The `has_label` helper fails open on read errors, such as `-ERANGE`, allowing an oversized label to bypass immutability protections.
- [Medium] The BPF LSM program uses global variables for scratch buffers, leading to data races under concurrent execution.
- [Medium] The BPF program incorrectly uses `bpf_probe_read_kernel()` instead of `bpf_probe_read_kernel_str()` to read null-terminated strings, risking page faults.
--
commit b09844def8309ef8ad642a29ec66fa9162a327a3
Author: Daniel Borkmann <daniel@iogearbox.net>
selftests/bpf: Add xattr label test with BPF LSM
This patch adds a selftest exercising bpffs extended attribute support
together with a BPF LSM example. The LSM restricts modifications and
lifecycle operations on BPF objects pinned in bpffs that are labeled
with "security.bpf.foo".
> diff --git a/tools/testing/selftests/bpf/progs/bpffs_xattr_label.c b/tools/testing/selftests/bpf/progs/bpffs_xattr_label.c
> new file mode 100644
> index 0000000000000..3891611c3b1c1
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/progs/bpffs_xattr_label.c
> @@ -0,0 +1,102 @@
[ ... ]
> +__u32 monitored_pid;
> +
> +char read_value[64];
> +int read_value_len;
> +
> +char label_check_buf[64];
> +char name_buf[64];
[Severity: Medium]
Since these buffers are shared globally, couldn't a multi-threaded
application trigger a data race when concurrently performing filesystem
operations? The buffers are small enough (64 bytes) that they might be
allocated safely on the BPF program's stack instead.
> +
> +static __always_inline bool name_is_foo(const char *name)
> +{
> + bpf_probe_read_kernel(name_buf, sizeof(name_buf), name);
[Severity: Medium]
Could unconditionally reading 64 bytes cause a page fault if the
null-terminated name string is near the end of a page and the subsequent
page is unmapped? Using bpf_probe_read_kernel_str() might be safer here.
> + return !bpf_strncmp(name_buf, sizeof(xattr_foo), xattr_foo);
> +}
> +
> +static __always_inline bool has_label(struct dentry *dentry)
> +{
> + struct bpf_dynptr ptr;
> +
> + bpf_dynptr_from_mem(label_check_buf, sizeof(label_check_buf), 0, &ptr);
> + return bpf_get_dentry_xattr(dentry, xattr_foo, &ptr) >= 0;
[Severity: Medium]
If an attacker creates a label larger than the 64-byte label_check_buf,
bpf_get_dentry_xattr() will return -ERANGE. This causes the function to
return false, failing open and bypassing the immutability protections.
Should this explicitly handle errors like -ERANGE?
> +}
[ ... ]
> +SEC("lsm.s/inode_rename")
> +int BPF_PROG(label_rename, struct inode *old_dir, struct dentry *old_dentry,
> + struct inode *new_dir, struct dentry *new_dentry,
> + unsigned int flags)
> +{
> + if ((bpf_get_current_pid_tgid() >> 32) != monitored_pid)
> + return 0;
> + return has_label(old_dentry) ? -EPERM : 0;
[Severity: Medium]
Because this doesn't check new_dentry, could an attacker rename an unlabeled
file over a labeled file? This would effectively delete the protected object
without triggering the label_unlink or label_rmdir hooks, bypassing the
immutability policy.
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260522135626.120732-1-daniel@iogearbox.net?part=3
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs
2026-05-22 15:09 ` Daniel Borkmann
@ 2026-05-22 22:21 ` Daniel Borkmann
0 siblings, 0 replies; 12+ messages in thread
From: Daniel Borkmann @ 2026-05-22 22:21 UTC (permalink / raw)
To: sashiko-reviews; +Cc: bpf
On 5/22/26 5:09 PM, Daniel Borkmann wrote:
> On 5/22/26 4:48 PM, sashiko-bot@kernel.org wrote:
[...]
>>> +static int bpf_fs_xattr_set(const struct xattr_handler *handler,
>>> + struct mnt_idmap *idmap, struct dentry *unused,
>>> + struct inode *inode, const char *name,
>>> + const void *value, size_t size, int flags)
>>> +{
>>> + struct bpf_fs_inode *bi = BPF_FS_I(inode);
>>> + struct simple_xattrs *xattrs;
>>> + struct simple_xattr *old;
>>> +
>>> + name = xattr_full_name(handler, name);
>>> + xattrs = simple_xattrs_lazy_alloc(&bi->xattrs, value, flags);
>>> + if (IS_ERR_OR_NULL(xattrs))
>>> + return PTR_ERR(xattrs);
>>> + old = simple_xattr_set(xattrs, name, value, size, flags);
>>
>> [Severity: High]
>> Does this code allow an unprivileged user to exhaust kernel memory?
>>
>> Because bpffs can be mounted in an unprivileged user namespace
>> (FS_USERNS_MOUNT), a user could create directories and repeatedly set user.*
>> extended attributes.
>>
>> Since simple_xattr_set() does not enforce per-inode limits like
>> simple_xattr_set_limited() does, couldn't this lead to an unbounded memory
>> allocation and soft lockups during rhashtable traversals?
>
> Ok, for user.* simple_xattr_set_limited should be the choice then, will
> fix in a v2.
Looked some more into this, my plan is to drop the user.* as unsupported
and have the security.* ones accounted via simple_xattr_limits (given userns).
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs
2026-05-22 13:56 [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs Daniel Borkmann
` (2 preceding siblings ...)
2026-05-22 14:48 ` [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs sashiko-bot
@ 2026-05-27 12:19 ` Christian Brauner
2026-05-27 12:36 ` Daniel Borkmann
3 siblings, 1 reply; 12+ messages in thread
From: Christian Brauner @ 2026-05-27 12:19 UTC (permalink / raw)
To: Daniel Borkmann; +Cc: bpf, kpsingh, john.fastabend
On Fri, May 22, 2026 at 03:56:24PM +0200, Daniel Borkmann wrote:
> Add support for extended attributes on bpffs inodes so that user space
> and BPF LSM programs can attach metadata, for example, a content hash
> or a security label - to a pinned object or directory. BPF LSM or user
> space tooling can then uniformly look at this (e.g. security.bpf.*) in
> similar way to other fs'es. The store is in-memory and non-persistent:
> it lives only for the lifetime of the mount, like everything else in
> bpffs. The modelling is similar to tmpfs.
>
> bpf_fill_super() is open-coded instead of using simple_fill_super(),
> because the root inode must now be allocated through bpf_fs_alloc_inode()
> i.e. carry the bpf_fs_inode wrapper and come from the right cache -
> which requires s_op (and s_xattr) to be installed before the first
> inode is created.
>
> bpf_fs_listxattr() is only reachable through the filesystem via
> i_op->listxattr, so the BPF token inode is left untouched. Name-based
> fsetxattr()/fgetxattr() on a token fd still work since the get/set
> handlers are installed at the superblock.
>
> Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
> Cc: Christian Brauner <brauner@kernel.org>
> ---
> kernel/bpf/inode.c | 254 ++++++++++++++++++++++++++++++++++++++++++---
> 1 file changed, 237 insertions(+), 17 deletions(-)
>
> diff --git a/kernel/bpf/inode.c b/kernel/bpf/inode.c
> index 25c06a011825..638aa996cbaf 100644
> --- a/kernel/bpf/inode.c
> +++ b/kernel/bpf/inode.c
> @@ -21,6 +21,9 @@
> #include <linux/bpf.h>
> #include <linux/bpf_trace.h>
> #include <linux/kstrtox.h>
> +#include <linux/xattr.h>
> +#include <linux/security.h>
> +
> #include "preload/bpf_preload.h"
>
> enum bpf_type {
> @@ -30,6 +33,22 @@ enum bpf_type {
> BPF_TYPE_LINK,
> };
>
> +struct bpf_fs_inode {
> + struct simple_xattrs *xattrs;
> + struct inode vfs_inode;
> +};
Note that we are in the process of making this a lot more elegant and
the interface changes significantly because of this.. The patches for
this are currently in vfs-7.2.misc. I can shove them onto vfs-7.2.xattr
and keep that branch stable so you can build it on top of it. You can
merge that branch into -next. I won't touch it again. Just tell me if
you want to do that.
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs
2026-05-27 12:19 ` Christian Brauner
@ 2026-05-27 12:36 ` Daniel Borkmann
2026-05-28 12:54 ` Christian Brauner
0 siblings, 1 reply; 12+ messages in thread
From: Daniel Borkmann @ 2026-05-27 12:36 UTC (permalink / raw)
To: Christian Brauner; +Cc: bpf, kpsingh, john.fastabend
On 5/27/26 2:19 PM, Christian Brauner wrote:
> On Fri, May 22, 2026 at 03:56:24PM +0200, Daniel Borkmann wrote:
>> Add support for extended attributes on bpffs inodes so that user space
>> and BPF LSM programs can attach metadata, for example, a content hash
>> or a security label - to a pinned object or directory. BPF LSM or user
>> space tooling can then uniformly look at this (e.g. security.bpf.*) in
>> similar way to other fs'es. The store is in-memory and non-persistent:
>> it lives only for the lifetime of the mount, like everything else in
>> bpffs. The modelling is similar to tmpfs.
>>
>> bpf_fill_super() is open-coded instead of using simple_fill_super(),
>> because the root inode must now be allocated through bpf_fs_alloc_inode()
>> i.e. carry the bpf_fs_inode wrapper and come from the right cache -
>> which requires s_op (and s_xattr) to be installed before the first
>> inode is created.
>>
>> bpf_fs_listxattr() is only reachable through the filesystem via
>> i_op->listxattr, so the BPF token inode is left untouched. Name-based
>> fsetxattr()/fgetxattr() on a token fd still work since the get/set
>> handlers are installed at the superblock.
>>
>> Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
>> Cc: Christian Brauner <brauner@kernel.org>
>> ---
>> kernel/bpf/inode.c | 254 ++++++++++++++++++++++++++++++++++++++++++---
>> 1 file changed, 237 insertions(+), 17 deletions(-)
>>
>> diff --git a/kernel/bpf/inode.c b/kernel/bpf/inode.c
>> index 25c06a011825..638aa996cbaf 100644
>> --- a/kernel/bpf/inode.c
>> +++ b/kernel/bpf/inode.c
>> @@ -21,6 +21,9 @@
>> #include <linux/bpf.h>
>> #include <linux/bpf_trace.h>
>> #include <linux/kstrtox.h>
>> +#include <linux/xattr.h>
>> +#include <linux/security.h>
>> +
>> #include "preload/bpf_preload.h"
>>
>> enum bpf_type {
>> @@ -30,6 +33,22 @@ enum bpf_type {
>> BPF_TYPE_LINK,
>> };
>>
>> +struct bpf_fs_inode {
>> + struct simple_xattrs *xattrs;
>> + struct inode vfs_inode;
>> +};
>
> Note that we are in the process of making this a lot more elegant and
> the interface changes significantly because of this.. The patches for
> this are currently in vfs-7.2.misc. I can shove them onto vfs-7.2.xattr
> and keep that branch stable so you can build it on top of it. You can
> merge that branch into -next. I won't touch it again. Just tell me if
> you want to do that.
Good to know, thanks! We could probably pull that particular branch into
bpf-next once you push it out, and then I can rework & resubmit the patches
on top of it.
Thanks,
Daniel
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs
2026-05-27 12:36 ` Daniel Borkmann
@ 2026-05-28 12:54 ` Christian Brauner
2026-05-29 7:39 ` Daniel Borkmann
0 siblings, 1 reply; 12+ messages in thread
From: Christian Brauner @ 2026-05-28 12:54 UTC (permalink / raw)
To: Daniel Borkmann; +Cc: Christian Brauner, bpf, kpsingh, john.fastabend
On 2026-05-27 14:36 +0200, Daniel Borkmann wrote:
> On 5/27/26 2:19 PM, Christian Brauner wrote:
> > On Fri, May 22, 2026 at 03:56:24PM +0200, Daniel Borkmann wrote:
> >> Add support for extended attributes on bpffs inodes so that user space
> >> and BPF LSM programs can attach metadata, for example, a content hash
> >> or a security label - to a pinned object or directory. BPF LSM or user
> >> space tooling can then uniformly look at this (e.g. security.bpf.*) in
> >> similar way to other fs'es. The store is in-memory and non-persistent:
> >> it lives only for the lifetime of the mount, like everything else in
> >> bpffs. The modelling is similar to tmpfs.
> >>
> >> bpf_fill_super() is open-coded instead of using simple_fill_super(),
> >> because the root inode must now be allocated through bpf_fs_alloc_inode()
> >> i.e. carry the bpf_fs_inode wrapper and come from the right cache -
> >> which requires s_op (and s_xattr) to be installed before the first
> >> inode is created.
> >>
> >> bpf_fs_listxattr() is only reachable through the filesystem via
> >> i_op->listxattr, so the BPF token inode is left untouched. Name-based
> >> fsetxattr()/fgetxattr() on a token fd still work since the get/set
> >> handlers are installed at the superblock.
> >>
> >> Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
> >> Cc: Christian Brauner <brauner@kernel.org>
> >> ---
> >> kernel/bpf/inode.c | 254 ++++++++++++++++++++++++++++++++++++++++++---
> >> 1 file changed, 237 insertions(+), 17 deletions(-)
> >>
> >> diff --git a/kernel/bpf/inode.c b/kernel/bpf/inode.c
> >> index 25c06a011825..638aa996cbaf 100644
> >> --- a/kernel/bpf/inode.c
> >> +++ b/kernel/bpf/inode.c
> >> @@ -21,6 +21,9 @@
> >> #include <linux/bpf.h>
> >> #include <linux/bpf_trace.h>
> >> #include <linux/kstrtox.h>
> >> +#include <linux/xattr.h>
> >> +#include <linux/security.h>
> >> +
> >> #include "preload/bpf_preload.h"
> >>
> >> enum bpf_type {
> >> @@ -30,6 +33,22 @@ enum bpf_type {
> >> BPF_TYPE_LINK,
> >> };
> >>
> >> +struct bpf_fs_inode {
> >> + struct simple_xattrs *xattrs;
> >> + struct inode vfs_inode;
> >> +};
> >
> > Note that we are in the process of making this a lot more elegant and
> > the interface changes significantly because of this.. The patches for
> > this are currently in vfs-7.2.misc. I can shove them onto vfs-7.2.xattr
> > and keep that branch stable so you can build it on top of it. You can
> > merge that branch into -next. I won't touch it again. Just tell me if
> > you want to do that.
>
> Good to know, thanks! We could probably pull that particular branch into
> bpf-next once you push it out, and then I can rework & resubmit the patches
> on top of it.
Nice, thanks. I've pushed vfs-7.2.xattr and marked it as shared now.
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs
2026-05-28 12:54 ` Christian Brauner
@ 2026-05-29 7:39 ` Daniel Borkmann
0 siblings, 0 replies; 12+ messages in thread
From: Daniel Borkmann @ 2026-05-29 7:39 UTC (permalink / raw)
To: Christian Brauner; +Cc: bpf, kpsingh, john.fastabend
On 5/28/26 2:54 PM, Christian Brauner wrote:
> On 2026-05-27 14:36 +0200, Daniel Borkmann wrote:
>> On 5/27/26 2:19 PM, Christian Brauner wrote:
>>> On Fri, May 22, 2026 at 03:56:24PM +0200, Daniel Borkmann wrote:
>>>> Add support for extended attributes on bpffs inodes so that user space
>>>> and BPF LSM programs can attach metadata, for example, a content hash
>>>> or a security label - to a pinned object or directory. BPF LSM or user
>>>> space tooling can then uniformly look at this (e.g. security.bpf.*) in
>>>> similar way to other fs'es. The store is in-memory and non-persistent:
>>>> it lives only for the lifetime of the mount, like everything else in
>>>> bpffs. The modelling is similar to tmpfs.
>>>>
>>>> bpf_fill_super() is open-coded instead of using simple_fill_super(),
>>>> because the root inode must now be allocated through bpf_fs_alloc_inode()
>>>> i.e. carry the bpf_fs_inode wrapper and come from the right cache -
>>>> which requires s_op (and s_xattr) to be installed before the first
>>>> inode is created.
>>>>
>>>> bpf_fs_listxattr() is only reachable through the filesystem via
>>>> i_op->listxattr, so the BPF token inode is left untouched. Name-based
>>>> fsetxattr()/fgetxattr() on a token fd still work since the get/set
>>>> handlers are installed at the superblock.
>>>>
>>>> Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
>>>> Cc: Christian Brauner <brauner@kernel.org>
>>>> ---
>>>> kernel/bpf/inode.c | 254 ++++++++++++++++++++++++++++++++++++++++++---
>>>> 1 file changed, 237 insertions(+), 17 deletions(-)
>>>>
>>>> diff --git a/kernel/bpf/inode.c b/kernel/bpf/inode.c
>>>> index 25c06a011825..638aa996cbaf 100644
>>>> --- a/kernel/bpf/inode.c
>>>> +++ b/kernel/bpf/inode.c
>>>> @@ -21,6 +21,9 @@
>>>> #include <linux/bpf.h>
>>>> #include <linux/bpf_trace.h>
>>>> #include <linux/kstrtox.h>
>>>> +#include <linux/xattr.h>
>>>> +#include <linux/security.h>
>>>> +
>>>> #include "preload/bpf_preload.h"
>>>>
>>>> enum bpf_type {
>>>> @@ -30,6 +33,22 @@ enum bpf_type {
>>>> BPF_TYPE_LINK,
>>>> };
>>>>
>>>> +struct bpf_fs_inode {
>>>> + struct simple_xattrs *xattrs;
>>>> + struct inode vfs_inode;
>>>> +};
>>>
>>> Note that we are in the process of making this a lot more elegant and
>>> the interface changes significantly because of this.. The patches for
>>> this are currently in vfs-7.2.misc. I can shove them onto vfs-7.2.xattr
>>> and keep that branch stable so you can build it on top of it. You can
>>> merge that branch into -next. I won't touch it again. Just tell me if
>>> you want to do that.
>>
>> Good to know, thanks! We could probably pull that particular branch into
>> bpf-next once you push it out, and then I can rework & resubmit the patches
>> on top of it.
>
> Nice, thanks. I've pushed vfs-7.2.xattr and marked it as shared now.
Awesome, thanks!
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-05-29 7:39 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-22 13:56 [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs Daniel Borkmann
2026-05-22 13:56 ` [PATCH bpf-next 2/3] selftests/bpf: Add bpffs xattr API coverage test Daniel Borkmann
2026-05-22 14:59 ` sashiko-bot
2026-05-22 13:56 ` [PATCH bpf-next 3/3] selftests/bpf: Add xattr label test with BPF LSM Daniel Borkmann
2026-05-22 15:19 ` sashiko-bot
2026-05-22 14:48 ` [PATCH bpf-next 1/3] bpf: Add simple xattr support to bpffs sashiko-bot
2026-05-22 15:09 ` Daniel Borkmann
2026-05-22 22:21 ` Daniel Borkmann
2026-05-27 12:19 ` Christian Brauner
2026-05-27 12:36 ` Daniel Borkmann
2026-05-28 12:54 ` Christian Brauner
2026-05-29 7:39 ` Daniel Borkmann
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.