Merge branch 'bpf-lsm-xattrs-at-inode-creation-support'

Daniel Borkmann says:

====================
BPF LSM xattrs at inode creation support

Many in-kernel LSMs store security labels in extended file system attributes
(xattrs). For these LSMs, atomic labeling during inode creation is critical:
If the inode becomes accessible before its xattr is set, it is briefly
unlabeled, which can disrupt LSMs making policy decisions based on file
labels. Existing LSMs solve this by setting xattrs in the inode_init_security
hook, which runs before the inode becomes accessible. BPF LSM programs
currently lack this capability, and this series addresses this gap along
with BPF selftests. Thanks!
====================

Link: https://patch.msgid.link/20260915150739.284189-1-daniel@iogearbox.net
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
This commit is contained in:
Alexei Starovoitov 2026-09-19 19:02:38 +00:00
commit b99f71407c
No known key found for this signature in database
19 changed files with 1075 additions and 14 deletions

View File

@ -315,6 +315,29 @@ However, there is no obligation to prove to the verifier that such a pointer is
non-NULL before use, in-line with existing semantics of arena pointers used in
a program (or obtained from any other source).
2.3.9 __ctx_out Annotation
--------------------------
This annotation is used to indicate that the argument is an output
parameter of the attached hook, passed through from the program's
context. The verifier requires the register to be a trusted read-only
pointer to fixed-size memory, which can only be produced by loading an
argument described by the program's ctx_arg_info from the context. The
program itself cannot write through the pointer; the kfunc may.
An example is given below::
__bpf_kfunc int bpf_inode_init_xattr(struct xattr *xattrs,
int *xattr_count__ctx_out,
...)
{
...
}
In this case, a program attached to the ``inode_init_security`` LSM hook
can pass the hook's own ``xattr_count`` argument through to the kfunc,
which claims xattr slots by writing through it.
.. _BPF_kfunc_nodef:
2.4 Using an existing kernel function

View File

@ -11,6 +11,7 @@
#include <linux/fsnotify.h>
#include <linux/file.h>
#include <linux/kernfs.h>
#include <linux/lsm_hooks.h>
#include <linux/mm.h>
#include <linux/net.h>
#include <linux/xattr.h>
@ -328,6 +329,89 @@ __bpf_kfunc int bpf_remove_dentry_xattr(struct dentry *dentry, const char *name_
return ret;
}
static int bpf_inode_init_xattrs_claimed(const struct xattr *xattrs, int xattr_count)
{
const size_t suffix_len = sizeof(XATTR_BPF_LSM_SUFFIX) - 1;
int i, claimed = 0;
for (i = 0; i < xattr_count; i++) {
const char *name = xattrs[i].name;
if (name && !strncmp(name, XATTR_BPF_LSM_SUFFIX, suffix_len))
claimed++;
}
return claimed;
}
/**
* bpf_inode_init_xattr - attach a xattr to an inode that is being created
* @xattrs: xattr array the inode_init_security hook was handed
* @xattr_count__ctx_out: xattr count the inode_init_security hook was handed
* @name__str: name of the xattr
* @value_p: xattr value
*
* Claim one of the slots the BPF LSM reserved in the xattr array, so that
* the xattr is written out as part of the transaction creating the inode.
*
* For security reasons, only *name__str* with prefix "security.bpf." is
* allowed. The slot stores the name without that "security." prefix, which
* the filesystem's initxattrs() callback puts back.
*
* At most BPF_LSM_INODE_INIT_XATTRS xattrs can be attached to one inode,
* matching the number of slots the BPF LSM reserves.
*
* Return: 0 on success, a negative value on error.
*/
__bpf_kfunc int bpf_inode_init_xattr(struct xattr *xattrs,
int *xattr_count__ctx_out,
const char *name__str,
const struct bpf_dynptr *value_p)
{
const struct bpf_dynptr_kern *value_ptr = (struct bpf_dynptr_kern *)value_p;
int *xattr_count = xattr_count__ctx_out;
const char *suffix;
struct xattr *slot;
const void *value;
size_t name_len;
u32 value_len;
char *buf;
if (!match_security_bpf_prefix(name__str))
return -EPERM;
if (bpf_inode_init_xattrs_claimed(xattrs, *xattr_count) >=
BPF_LSM_INODE_INIT_XATTRS)
return -ENOSPC;
suffix = name__str + XATTR_SECURITY_PREFIX_LEN;
name_len = strlen(suffix);
if (name_len <= sizeof(XATTR_BPF_LSM_SUFFIX) - 1 ||
name_len > XATTR_NAME_MAX - XATTR_SECURITY_PREFIX_LEN)
return -EINVAL;
value_len = __bpf_dynptr_size(value_ptr);
value = __bpf_dynptr_data(value_ptr, value_len);
if (!value)
return -EINVAL;
if (value_len > XATTR_SIZE_MAX)
return -E2BIG;
buf = kmalloc(value_len + name_len + 1, GFP_NOWAIT | __GFP_NOWARN);
if (!buf)
return -ENOMEM;
memcpy(buf, value, value_len);
memcpy(buf + value_len, suffix, name_len + 1);
slot = lsm_get_xattr_slot(xattrs, xattr_count);
if (!slot) {
kfree(buf);
return -ENOSPC;
}
slot->value = buf;
slot->value_len = value_len;
slot->name = buf + value_len;
return 0;
}
#ifdef CONFIG_CGROUPS
/**
* bpf_cgroup_read_xattr - read xattr of a cgroup's node in cgroupfs
@ -425,6 +509,7 @@ BTF_ID_FLAGS(func, bpf_get_file_xattr, KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_set_dentry_xattr, KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_remove_dentry_xattr, KF_SLEEPABLE)
BTF_ID_FLAGS(func, bpf_real_data_inode, KF_SLEEPABLE | KF_RET_NULL)
BTF_ID_FLAGS(func, bpf_inode_init_xattr)
#ifdef CONFIG_NET
BTF_ID_FLAGS(func, bpf_sock_read_xattr, KF_RCU)
#endif
@ -436,10 +521,24 @@ BTF_ID(func, bpf_set_dentry_xattr)
BTF_ID(func, bpf_remove_dentry_xattr)
BTF_SET_END(bpf_fs_kfunc_lsm_only_ids)
BTF_ID_LIST_SINGLE(bpf_inode_init_xattr_ids, func, bpf_inode_init_xattr)
BTF_SET_START(bpf_inode_init_xattr_hooks)
BTF_ID(func, bpf_lsm_inode_init_security)
BTF_SET_END(bpf_inode_init_xattr_hooks)
static int bpf_fs_kfuncs_filter(const struct bpf_prog *prog, u32 kfunc_id)
{
if (!btf_id_set8_contains(&bpf_fs_kfunc_set_ids, kfunc_id))
return 0;
if (kfunc_id == bpf_inode_init_xattr_ids[0]) {
if (prog->type != BPF_PROG_TYPE_LSM ||
prog->expected_attach_type != BPF_LSM_MAC ||
!btf_id_set_contains(&bpf_inode_init_xattr_hooks,
prog->aux->attach_btf_id))
return -EACCES;
return 0;
}
if (prog->type == BPF_PROG_TYPE_LSM)
return 0;
if (prog->type != BPF_PROG_TYPE_STRUCT_OPS)

View File

@ -924,6 +924,7 @@ enum bpf_arg_type {
ARG_PTR_TO_TASK_WORK, /* pointer to bpf_task_work */
ARG_PTR_TO_IRQ_FLAG, /* pointer to saved IRQ flags on the stack */
ARG_PTR_TO_RES_SPIN_LOCK, /* pointer to bpf_res_spin_lock */
ARG_PTR_TO_CTX_OUT, /* hook output argument passed through from ctx */
ARG_PTR_TO_PROG_AUX, /* pointer to the caller's bpf_prog_aux */
ARG_IGNORE, /* argument the verifier does not check at all */
__BPF_ARG_TYPE_MAX,
@ -1138,6 +1139,7 @@ struct bpf_insn_access_aux {
u32 ref_id;
};
};
u32 mem_size;
struct bpf_verifier_log *log; /* for verbose logs */
bool is_retval; /* is accessing function return value ? */
};
@ -1716,6 +1718,7 @@ struct bpf_ctx_arg_aux {
struct btf *btf;
u32 btf_id;
u32 ref_id;
u32 mem_size;
bool refcounted;
};

View File

@ -21,6 +21,13 @@ extern bool bpf_lsm_initialized __ro_after_init;
#include <linux/lsm_hook_defs.h>
#undef LSM_HOOK
/*
* Number of xattr slots the BPF LSM reserves in the array handed to
* security_inode_init_security(), i.e. the maximum number of labels
* a policy may attach to an inode while it is being created.
*/
#define BPF_LSM_INODE_INIT_XATTRS 2
struct bpf_storage_blob {
struct bpf_local_storage __rcu *storage;
};
@ -28,7 +35,7 @@ struct bpf_storage_blob {
extern struct lsm_blob_sizes bpf_lsm_blob_sizes;
int bpf_lsm_verify_prog(struct bpf_verifier_log *vlog,
const struct bpf_prog *prog);
struct bpf_prog *prog);
bool bpf_lsm_is_sleepable_hook(u32 btf_id);
bool bpf_lsm_is_trusted(const struct bpf_prog *prog);
@ -71,7 +78,7 @@ static inline bool bpf_lsm_is_trusted(const struct bpf_prog *prog)
}
static inline int bpf_lsm_verify_prog(struct bpf_verifier_log *vlog,
const struct bpf_prog *prog)
struct bpf_prog *prog)
{
return -EOPNOTSUPP;
}

View File

@ -116,8 +116,11 @@ void bpf_lsm_find_cgroup_shim(const struct bpf_prog *prog,
}
#endif
BTF_ID_LIST_SINGLE(bpf_lsm_inode_init_security_btf_id, func,
bpf_lsm_inode_init_security)
int bpf_lsm_verify_prog(struct bpf_verifier_log *vlog,
const struct bpf_prog *prog)
struct bpf_prog *prog)
{
u32 btf_id = prog->aux->attach_btf_id;
const char *func_name = prog->aux->attach_func_name;
@ -140,6 +143,23 @@ int bpf_lsm_verify_prog(struct bpf_verifier_log *vlog,
return -EINVAL;
}
if (btf_id == bpf_lsm_inode_init_security_btf_id[0]) {
/*
* inode, dir, qstr, xattrs, xattr_count
*
* The trusted pointer this hands the program is only as
* trustworthy as the context it is loaded from, which
* lsm_verifier_ops keeps read-only.
*/
static const struct bpf_ctx_arg_aux xattr_count_arg_info = {
.offset = 4 * sizeof(u64),
.reg_type = PTR_TO_MEM | MEM_RDONLY | PTR_TRUSTED,
.mem_size = sizeof(int),
};
return bpf_prog_ctx_arg_info_init(prog, &xattr_count_arg_info, 1);
}
return 0;
}
@ -414,8 +434,8 @@ const struct bpf_prog_ops lsm_prog_ops = {
};
const struct bpf_verifier_ops lsm_verifier_ops = {
.get_func_proto = bpf_lsm_func_proto,
.is_valid_access = btf_ctx_access,
.get_func_proto = bpf_lsm_func_proto,
.is_valid_access = bpf_tracing_btf_ctx_access,
};
/* hooks return 0 or 1 */

View File

@ -4,6 +4,7 @@
*/
#include <linux/fs.h>
#include <linux/xattr.h>
#include <linux/bpf_lsm.h>
/*
@ -12,8 +13,22 @@
* explicitly enforces that BPF LSM programs check for NULL before attempting to
* dereference it.
*/
int bpf_lsm_mmap_file(struct file *file__nullable, unsigned long reqprot,
unsigned long prot, unsigned long flags)
noinline int bpf_lsm_mmap_file(struct file *file__nullable, unsigned long reqprot,
unsigned long prot, unsigned long flags)
{
return 0;
}
/*
* Strong definition of the inode_init_security() BPF LSM hook. Both the
* qstr and the xattr array are NULL for some callers, so the __nullable
* suffix marks it as PTR_MAYBE_NULL. BPF LSM programs have to check before
* dereferencing them or handing them to bpf_inode_init_xattr().
*/
noinline int bpf_lsm_inode_init_security(struct inode *inode, struct inode *dir,
const struct qstr *qstr__nullable,
struct xattr *xattrs__nullable,
int *xattr_count)
{
return -EOPNOTSUPP;
}

View File

@ -6995,8 +6995,9 @@ bool btf_ctx_access(int off, int size, enum bpf_access_type type,
}
/*
* Check for PTR_TO_RDONLY_BUF_OR_NULL, PTR_TO_RDWR_BUF_OR_NULL or
* PTR_TO_ARENA (both nullable and non-nullable cases).
* Check for PTR_TO_RDONLY_BUF_OR_NULL, PTR_TO_RDWR_BUF_OR_NULL,
* PTR_TO_ARENA (both nullable and non-nullable cases) or fixed-size
* PTR_TO_MEM.
*/
for (i = 0; i < prog->aux->ctx_arg_info_size; i++) {
const struct bpf_ctx_arg_aux *ctx_arg_info = &prog->aux->ctx_arg_info[i];
@ -7006,8 +7007,10 @@ bool btf_ctx_access(int off, int size, enum bpf_access_type type,
flag = type_flag(ctx_arg_info->reg_type);
if (ctx_arg_info->offset == off &&
(type == PTR_TO_ARENA ||
type == PTR_TO_MEM ||
(type == PTR_TO_BUF && (flag & PTR_MAYBE_NULL)))) {
info->reg_type = ctx_arg_info->reg_type;
info->mem_size = ctx_arg_info->mem_size;
return true;
}
}

View File

@ -984,6 +984,8 @@ const char *bpf_diag_arg_type_plain(enum bpf_arg_type type)
return "the address of a stack iterator object for iterator new, next, and destroy calls";
case ARG_PTR_TO_IRQ_FLAG:
return "the same stack slot used by bpf_local_irq_save() or bpf_res_spin_lock_irqsave()";
case ARG_PTR_TO_CTX_OUT:
return "the attach hook's own output argument, loaded directly from the program context";
default:
return "a value with one of the accepted pointer or scalar types for this call";
}

View File

@ -6584,6 +6584,8 @@ static int check_mem_access(struct bpf_verifier_env *env, int insn_idx, struct b
regs[value_regno].btf = info.btf;
regs[value_regno].btf_id = info.btf_id;
regs[value_regno].id = info.ref_id;
} else if (base_type(info.reg_type) == PTR_TO_MEM) {
regs[value_regno].mem_size = info.mem_size;
}
if (type_may_be_null(info.reg_type) && !regs[value_regno].id)
regs[value_regno].id = ++env->id_gen;
@ -8363,6 +8365,9 @@ static const struct bpf_reg_types arena_types = {
SCALAR_VALUE,
}
};
static const struct bpf_reg_types ctx_out_types = {
.types = { PTR_TO_MEM | MEM_RDONLY | PTR_TRUSTED },
};
static const struct bpf_reg_types alloc_obj_drop_types = {
.types = {
@ -8434,6 +8439,7 @@ static const struct bpf_reg_types *compatible_reg_types[__BPF_ARG_TYPE_MAX] = {
[ARG_PTR_TO_TASK_WORK] = &map_value_types,
[ARG_PTR_TO_IRQ_FLAG] = &stack_ptr_types,
[ARG_PTR_TO_ARENA] = &arena_types,
[ARG_PTR_TO_CTX_OUT] = &ctx_out_types,
};
static void bpf_diag_call_arg(struct bpf_verifier_env *env, u32 insn_idx, argno_t argno,
@ -9426,6 +9432,13 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 p
if (err < 0)
return err;
break;
case ARG_PTR_TO_CTX_OUT:
if (reg->mem_size != arg_size) {
verbose(env, "%s expected %u bytes of ctx-provided memory, got %u\n",
reg_arg_name(env, argno), arg_size, reg->mem_size);
return -EINVAL;
}
break;
case ARG_PTR_TO_RES_SPIN_LOCK:
{
int flags;
@ -12142,6 +12155,11 @@ static bool is_kfunc_arg_irq_flag(const struct btf *btf, const struct btf_param
return btf_param_match_suffix(btf, arg, "__irq_flag");
}
static bool is_kfunc_arg_ctx_out(const struct btf *btf, const struct btf_param *arg)
{
return btf_param_match_suffix(btf, arg, "__ctx_out");
}
static bool is_kfunc_arg_arena(const struct btf *btf, const struct btf_param *arg)
{
return btf_param_match_suffix(btf, arg, "__arena__nullable") ||
@ -12905,7 +12923,23 @@ get_kfunc_arg_type(struct bpf_verifier_env *env, struct bpf_call_arg_meta *meta,
arg_type = ARG_PTR_TO_IRQ_FLAG;
else if (is_kfunc_arg_res_spin_lock(meta->btf, &args[arg]))
arg_type = ARG_PTR_TO_RES_SPIN_LOCK;
else if (is_kfunc_arg_callback(env, meta->btf, &args[arg]))
else if (is_kfunc_arg_ctx_out(meta->btf, &args[arg])) {
if (!btf_type_is_scalar(ref_t)) {
verbose(env, "%s __ctx_out argument must point to a scalar\n",
reg_arg_name(env, argno));
return -EINVAL;
}
resolve_ret = btf_resolve_size(meta->btf, ref_t, &type_size);
if (IS_ERR(resolve_ret)) {
verbose(env,
"%s reference type('%s %s') size cannot be determined: %ld\n",
reg_arg_name(env, argno), btf_type_str(ref_t),
ref_tname, PTR_ERR(resolve_ret));
return -EINVAL;
}
proto->arg_size[arg] = type_size;
arg_type = ARG_PTR_TO_CTX_OUT | MEM_FIXED_SIZE;
} else if (is_kfunc_arg_callback(env, meta->btf, &args[arg]))
arg_type = ARG_PTR_TO_FUNC;
else if (is_kfunc_arg_arena(meta->btf, &args[arg])) {
if (!bpf_jit_supports_arena_args()) {

View File

@ -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) = {

View File

@ -78,18 +78,29 @@ extern void bpf_key_put(struct bpf_key *key) __ksym;
extern int bpf_verify_pkcs7_signature(const struct bpf_dynptr *data_ptr,
const struct bpf_dynptr *sig_ptr,
struct bpf_key *trusted_keyring) __ksym;
struct dentry;
/* Description
/*
* Description
* Returns xattr of a dentry
* Returns
* Error code
*/
struct dentry;
extern int bpf_get_dentry_xattr(struct dentry *dentry, const char *name,
struct bpf_dynptr *value_ptr) __ksym __weak;
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;
/*
* Description
* Attach a xattr to an inode that is being created, from a program on the
* inode_init_security LSM hook. *xattrs* and *xattr_count* must be the
* hook's own arguments, passed through unmodified.
* Returns
* 0 on success, a negative value on error
*/
struct xattr;
extern int bpf_inode_init_xattr(struct xattr *xattrs, int *xattr_count,
const char *name__str,
const struct bpf_dynptr *value_p) __ksym __weak;
#endif

View File

@ -124,6 +124,8 @@ CONFIG_SECURITY=y
CONFIG_SECURITYFS=y
CONFIG_SYN_COOKIES=y
CONFIG_TEST_BPF=m
CONFIG_TMPFS=y
CONFIG_TMPFS_XATTR=y
CONFIG_UDMABUF=y
CONFIG_USERFAULTFD=y
CONFIG_VSOCKETS=y

View File

@ -0,0 +1,399 @@
// SPDX-License-Identifier: GPL-2.0
#define _GNU_SOURCE
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/xattr.h>
#include <test_progs.h>
#include "lsm_inode_init_xattr.skel.h"
#include "lsm_inode_init_xattr_budget.skel.h"
#include "lsm_inode_init_xattr_value.skel.h"
#define INIT_XATTRS_MAX 2
#define VALUE_SIZE_MAX (64 * 1024)
#define TESTDIR "/tmp/test_progs_lsm_init_xattr"
#define RAMFSDIR "/tmp/test_progs_lsm_init_xattr_ramfs"
static bool testdir_mounted;
static bool ramfsdir_mounted;
static int testdir_setup(void)
{
if (mkdir(TESTDIR, 0755) && errno != EEXIST)
return -errno;
if (mount("tmpfs", TESTDIR, "tmpfs", 0, NULL))
return -errno;
testdir_mounted = true;
if (mkdir(RAMFSDIR, 0755) && errno != EEXIST)
return -errno;
if (mount("ramfs", RAMFSDIR, "ramfs", 0, NULL))
return -errno;
ramfsdir_mounted = true;
return 0;
}
static void testdir_cleanup(void)
{
if (ramfsdir_mounted)
umount(RAMFSDIR);
rmdir(RAMFSDIR);
if (testdir_mounted)
umount(TESTDIR);
rmdir(TESTDIR);
}
static bool lsm_is_active(const char *name)
{
char buf[512], *tok;
int fd, len;
fd = open("/sys/kernel/security/lsm", O_RDONLY);
if (fd < 0)
return true;
len = read(fd, buf, sizeof(buf) - 1);
close(fd);
if (len <= 0)
return true;
buf[len] = '\0';
for (tok = strtok(buf, ",\n"); tok; tok = strtok(NULL, ",\n"))
if (!strcmp(tok, name))
return true;
return false;
}
static int read_label(const char *path, const char *name, char *buf, size_t sz)
{
int ret = getxattr(path, name, buf, sz);
return ret < 0 ? -errno : ret;
}
static void assert_label(const char *path, const char *name, const char *want)
{
char buf[64] = {};
int ret;
ret = read_label(path, name, buf, sizeof(buf));
if (!ASSERT_EQ(ret, (int)strlen(want) + 1, name))
return;
ASSERT_STREQ(buf, want, name);
}
static struct lsm_inode_init_xattr *policy_attach(void)
{
struct lsm_inode_init_xattr *skel;
skel = lsm_inode_init_xattr__open_and_load();
if (!ASSERT_OK_PTR(skel, "skel_open_and_load"))
return NULL;
skel->bss->monitored_pid = getpid();
if (!ASSERT_OK(lsm_inode_init_xattr__attach(skel), "skel_attach")) {
lsm_inode_init_xattr__destroy(skel);
return NULL;
}
return skel;
}
static void test_init_labels(void)
{
struct lsm_inode_init_xattr *skel;
const char *file = TESTDIR "/file";
const char *subdir = TESTDIR "/subdir";
int fd = -1;
skel = policy_attach();
if (!skel)
return;
fd = open(file, O_CREAT | O_RDWR, 0644);
if (!ASSERT_GE(fd, 0, "create_file"))
goto out;
if (!ASSERT_TRUE(skel->bss->hook_ran, "hook_ran"))
goto out;
ASSERT_OK(skel->data->zone_err, "zone_err");
ASSERT_OK(skel->data->origin_err, "origin_err");
assert_label(file, "security.bpf.zone", "default");
assert_label(file, "security.bpf.origin", "created");
if (!ASSERT_OK(mkdir(subdir, 0755), "mkdir"))
goto out;
assert_label(subdir, "security.bpf.zone", "default");
assert_label(subdir, "security.bpf.origin", "created");
rmdir(subdir);
out:
if (fd >= 0)
close(fd);
remove(file);
lsm_inode_init_xattr__destroy(skel);
}
static void test_inherit_from_parent(void)
{
const char *zonedir = TESTDIR "/zoned";
const char *file = TESTDIR "/zoned/file";
struct lsm_inode_init_xattr *skel;
int dirfd = -1, fd = -1, err;
struct {
char v[32];
__u32 len;
} label = {};
if (!ASSERT_OK(mkdir(zonedir, 0755), "mkdir_zoned"))
return;
skel = policy_attach();
if (!skel)
goto out_dir;
dirfd = open(zonedir, O_RDONLY | O_DIRECTORY);
if (!ASSERT_GE(dirfd, 0, "open_zoned"))
goto out;
strncpy(label.v, "restricted", sizeof(label.v) - 1);
label.len = strlen("restricted") + 1;
err = bpf_map_update_elem(bpf_map__fd(skel->maps.inode_zone), &dirfd,
&label, BPF_ANY);
if (!ASSERT_OK(err, "seed_parent_zone"))
goto out;
fd = open(file, O_CREAT | O_RDWR, 0644);
if (!ASSERT_GE(fd, 0, "create_file"))
goto out;
if (!ASSERT_TRUE(skel->bss->hook_ran, "hook_ran"))
goto out;
ASSERT_EQ(skel->bss->inherited, 1, "inherited");
ASSERT_OK(skel->data->zone_err, "zone_err");
assert_label(file, "security.bpf.zone", "restricted");
assert_label(file, "security.bpf.origin", "created");
out:
if (fd >= 0)
close(fd);
if (dirfd >= 0)
close(dirfd);
remove(file);
lsm_inode_init_xattr__destroy(skel);
out_dir:
rmdir(zonedir);
}
static void test_refused_claims(void)
{
const char *file = TESTDIR "/refused";
struct lsm_inode_init_xattr *skel;
char buf[64];
int fd = -1;
skel = policy_attach();
if (!skel)
return;
fd = open(file, O_CREAT | O_RDWR, 0644);
if (!ASSERT_GE(fd, 0, "create_file"))
goto out;
if (!ASSERT_TRUE(skel->bss->hook_ran, "hook_ran"))
goto out;
ASSERT_EQ(skel->data->overflow_err, -ENOSPC, "overflow_err");
ASSERT_EQ(read_label(file, "security.bpf.overflow", buf, sizeof(buf)),
-ENODATA, "overflow_absent");
ASSERT_EQ(skel->data->toolong_err, -EINVAL, "toolong_err");
ASSERT_EQ(skel->data->selinux_err, -EPERM, "selinux_err");
ASSERT_EQ(skel->data->user_err, -EPERM, "user_err");
if (!lsm_is_active("selinux"))
ASSERT_EQ(read_label(file, "security.selinux", buf, sizeof(buf)),
-ENODATA, "selinux_absent");
ASSERT_EQ(read_label(file, "user.zone", buf, sizeof(buf)),
-ENODATA, "user_absent");
out:
if (fd >= 0)
close(fd);
remove(file);
lsm_inode_init_xattr__destroy(skel);
}
static void test_null_xattrs(void)
{
const char *file = RAMFSDIR "/file";
struct lsm_inode_init_xattr *skel;
int fd = -1;
skel = policy_attach();
if (!skel)
return;
fd = open(file, O_CREAT | O_RDWR, 0644);
if (!ASSERT_GE(fd, 0, "create_file"))
goto out;
if (!ASSERT_TRUE(skel->bss->hook_ran, "hook_ran"))
goto out;
ASSERT_EQ(skel->data->zone_err, -EOPNOTSUPP, "zone_err");
out:
if (fd >= 0)
close(fd);
remove(file);
lsm_inode_init_xattr__destroy(skel);
}
static void test_shared_budget(void)
{
struct lsm_inode_init_xattr_budget *skel[INIT_XATTRS_MAX + 1] = {};
struct bpf_link *link[INIT_XATTRS_MAX + 1] = {};
const char *file = TESTDIR "/budget";
int claimed = 0, refused = 0;
int i, fd = -1;
for (i = 0; i <= INIT_XATTRS_MAX; i++) {
skel[i] = lsm_inode_init_xattr_budget__open();
if (!ASSERT_OK_PTR(skel[i], "skel_open"))
goto out;
snprintf(skel[i]->rodata->xattr_name,
sizeof(skel[i]->rodata->xattr_name),
"security.bpf.slot%d", i);
if (!ASSERT_OK(lsm_inode_init_xattr_budget__load(skel[i]),
"skel_load"))
goto out;
skel[i]->bss->monitored_pid = getpid();
link[i] = bpf_program__attach_lsm(skel[i]->progs.claim_one);
if (!ASSERT_OK_PTR(link[i], "attach"))
goto out;
}
fd = open(file, O_CREAT | O_RDWR, 0644);
if (!ASSERT_GE(fd, 0, "create_file"))
goto out;
for (i = 0; i <= INIT_XATTRS_MAX; i++) {
int err = skel[i]->data->claim_err;
ASSERT_TRUE(skel[i]->bss->hook_ran, "hook_ran");
if (!err)
claimed++;
else if (ASSERT_EQ(err, -ENOSPC, "claim_err"))
refused++;
}
ASSERT_EQ(claimed, INIT_XATTRS_MAX, "claimed");
ASSERT_EQ(refused, 1, "refused");
out:
if (fd >= 0)
close(fd);
remove(file);
for (i = 0; i <= INIT_XATTRS_MAX; i++) {
bpf_link__destroy(link[i]);
lsm_inode_init_xattr_budget__destroy(skel[i]);
}
}
static int claim_value(const char *file, __u32 len, __u64 flags,
int hook_retval, int *open_errno)
{
struct lsm_inode_init_xattr_value *skel;
struct bpf_link *link = NULL;
int err = 1, fd;
skel = lsm_inode_init_xattr_value__open();
if (!ASSERT_OK_PTR(skel, "skel_open"))
return 1;
skel->rodata->value_len = len;
skel->rodata->dynptr_flags = flags;
skel->rodata->hook_retval = hook_retval;
if (!ASSERT_OK(lsm_inode_init_xattr_value__load(skel), "skel_load"))
goto out;
skel->bss->monitored_pid = getpid();
link = bpf_program__attach_lsm(skel->progs.claim_value);
if (!ASSERT_OK_PTR(link, "attach"))
goto out;
fd = open(file, O_CREAT | O_RDWR | O_EXCL, 0644);
*open_errno = fd < 0 ? errno : 0;
if (fd >= 0)
close(fd);
if (ASSERT_TRUE(skel->bss->hook_ran, "hook_ran"))
err = skel->data->claim_err;
out:
bpf_link__destroy(link);
lsm_inode_init_xattr_value__destroy(skel);
return err;
}
static void test_value_shapes(void)
{
const char *file = TESTDIR "/value";
char buf[64];
int err, open_errno;
remove(file);
err = claim_value(file, 0, 0, 0, &open_errno);
ASSERT_OK(err, "empty_value_err");
ASSERT_OK(open_errno, "empty_value_open");
ASSERT_EQ(read_label(file, "security.bpf.value", buf, sizeof(buf)),
0, "empty_value_len");
remove(file);
err = claim_value(file, sizeof(buf), 1, 0, &open_errno);
ASSERT_EQ(err, -EINVAL, "bad_dynptr_err");
ASSERT_EQ(read_label(file, "security.bpf.value", buf, sizeof(buf)),
-ENODATA, "bad_dynptr_absent");
remove(file);
err = claim_value(file, VALUE_SIZE_MAX + 1, 0, 0, &open_errno);
ASSERT_EQ(err, -E2BIG, "oversized_err");
ASSERT_EQ(read_label(file, "security.bpf.value", buf, sizeof(buf)),
-ENODATA, "oversized_absent");
remove(file);
err = claim_value(file, 8, 0, -EPERM, &open_errno);
ASSERT_OK(err, "denied_claim_err");
ASSERT_EQ(open_errno, EPERM, "denied_open");
ASSERT_EQ(read_label(file, "security.bpf.value", buf, sizeof(buf)),
-ENOENT, "denied_absent");
remove(file);
}
void test_lsm_inode_init_xattr(void)
{
if (!ASSERT_OK(testdir_setup(), "testdir_setup"))
goto out;
if (test__start_subtest("init_labels"))
test_init_labels();
if (test__start_subtest("inherit_from_parent"))
test_inherit_from_parent();
if (test__start_subtest("refused_claims"))
test_refused_claims();
if (test__start_subtest("null_xattrs"))
test_null_xattrs();
if (test__start_subtest("shared_budget"))
test_shared_budget();
if (test__start_subtest("value_shapes"))
test_value_shapes();
out:
testdir_cleanup();
}

View File

@ -130,6 +130,7 @@
#include "verifier_bits_iter.skel.h"
#include "verifier_set_retval.skel.h"
#include "verifier_lsm.skel.h"
#include "verifier_lsm_init_xattr.skel.h"
#include "verifier_jit_inline.skel.h"
#include "irq.skel.h"
#include "verifier_ctx_ptr_param.skel.h"
@ -294,6 +295,7 @@ void test_verifier_xdp_direct_packet_access(void) { RUN(verifier_xdp_direct_pack
void test_verifier_bits_iter(void) { RUN(verifier_bits_iter); }
void test_verifier_set_retval(void) { RUN(verifier_set_retval); }
void test_verifier_lsm(void) { RUN(verifier_lsm); }
void test_verifier_lsm_init_xattr(void) { RUN(verifier_lsm_init_xattr); }
void test_irq(void) { RUN(irq); }
void test_verifier_mtu(void) { RUN(verifier_mtu); }
void test_verifier_jit_inline(void) { RUN(verifier_jit_inline); }

View File

@ -0,0 +1,98 @@
// SPDX-License-Identifier: GPL-2.0
#include "vmlinux.h"
#include <errno.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_helpers.h>
#include "bpf_kfuncs.h"
char _license[] SEC("license") = "GPL";
#define LABEL_MAX 32
struct label {
char v[LABEL_MAX];
__u32 len;
};
struct {
__uint(type, BPF_MAP_TYPE_INODE_STORAGE);
__uint(map_flags, BPF_F_NO_PREALLOC);
__type(key, int);
__type(value, struct label);
} inode_zone SEC(".maps");
__u32 monitored_pid;
const char xattr_zone[] = "security.bpf.zone";
const char xattr_origin[] = "security.bpf.origin";
const char xattr_overflow[] = "security.bpf.overflow";
/* One byte over XATTR_NAME_MAX once "security." is prepended again. */
const char xattr_toolong[] = "security.bpf." "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const char xattr_selinux[] = "security.selinux";
const char xattr_user[] = "user.zone";
char zone_scratch[LABEL_MAX];
char origin_value[] = "created";
__u32 hook_ran;
__s32 zone_err = 1;
__s32 origin_err = 1;
__s32 overflow_err = 1;
__s32 toolong_err = 1;
__s32 selinux_err = 1;
__s32 user_err = 1;
__u32 inherited;
SEC("lsm/inode_init_security")
int BPF_PROG(init_label, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
struct bpf_dynptr value;
struct label *parent;
int len = 0;
if ((bpf_get_current_pid_tgid() >> 32) != monitored_pid)
return 0;
hook_ran = 1;
if (!xattrs) {
/* The filesystem takes no xattrs at inode creation. */
zone_err = -EOPNOTSUPP;
return 0;
}
__builtin_memset(zone_scratch, 0, LABEL_MAX);
if (dir) {
parent = bpf_inode_storage_get(&inode_zone, dir, 0, 0);
if (parent && parent->len > 0 && parent->len <= LABEL_MAX) {
len = parent->len;
__builtin_memcpy(zone_scratch, parent->v, LABEL_MAX);
inherited = 1;
}
}
if (!len) {
__builtin_memcpy(zone_scratch, "default", sizeof("default"));
len = sizeof("default");
}
bpf_dynptr_from_mem(zone_scratch, len, 0, &value);
/* Refused before a slot is claimed, so the budget below is intact. */
toolong_err = bpf_inode_init_xattr(xattrs, xattr_count, xattr_toolong,
&value);
zone_err = bpf_inode_init_xattr(xattrs, xattr_count, xattr_zone,
&value);
bpf_dynptr_from_mem(origin_value, sizeof(origin_value), 0, &value);
origin_err = bpf_inode_init_xattr(xattrs, xattr_count, xattr_origin,
&value);
overflow_err = bpf_inode_init_xattr(xattrs, xattr_count,
xattr_overflow, &value);
selinux_err = bpf_inode_init_xattr(xattrs, xattr_count, xattr_selinux,
&value);
user_err = bpf_inode_init_xattr(xattrs, xattr_count, xattr_user,
&value);
return 0;
}

View File

@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-2.0
#include "vmlinux.h"
#include <errno.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_helpers.h>
#include "bpf_kfuncs.h"
char _license[] SEC("license") = "GPL";
const char xattr_name[32] = "security.bpf.slot";
__u32 monitored_pid;
__u32 hook_ran;
__s32 claim_err = 1;
char claim_value[] = "v";
SEC("lsm/inode_init_security")
int BPF_PROG(claim_one, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
struct bpf_dynptr value;
if ((bpf_get_current_pid_tgid() >> 32) != monitored_pid)
return 0;
hook_ran = 1;
if (!xattrs) {
claim_err = -EOPNOTSUPP;
return 0;
}
bpf_dynptr_from_mem(claim_value, sizeof(claim_value), 0, &value);
claim_err = bpf_inode_init_xattr(xattrs, xattr_count, xattr_name,
&value);
return 0;
}

View File

@ -0,0 +1,47 @@
// SPDX-License-Identifier: GPL-2.0
#include "vmlinux.h"
#include <errno.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_helpers.h>
#include "bpf_kfuncs.h"
char _license[] SEC("license") = "GPL";
#define VALUE_MAX (64 * 1024 + 1)
const char xattr_value[] = "security.bpf.value";
/* Shape of the value handed to the kfunc, and what the hook returns after. */
const volatile __u32 value_len;
const volatile __u64 dynptr_flags;
const volatile __s32 hook_retval;
char value_src[VALUE_MAX];
__u32 monitored_pid;
__u32 hook_ran;
__s32 claim_err = 1;
SEC("lsm/inode_init_security")
int BPF_PROG(claim_value, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
struct bpf_dynptr value;
__u32 len = value_len;
if ((bpf_get_current_pid_tgid() >> 32) != monitored_pid)
return 0;
if (len > VALUE_MAX)
return 0;
hook_ran = 1;
if (!xattrs) {
claim_err = -EOPNOTSUPP;
return 0;
}
bpf_dynptr_from_mem(value_src, len, dynptr_flags, &value);
claim_err = bpf_inode_init_xattr(xattrs, xattr_count, xattr_value,
&value);
return hook_retval;
}

View File

@ -212,4 +212,17 @@ __naked int retval_load_resets_bounds(void *ctx)
::: __clobber_all);
}
SEC("lsm/file_mprotect")
__description("lsm ctx is read-only")
__failure __msg("invalid bpf_context access")
__naked int reject_ctx_write(void *ctx)
{
asm volatile (
"r6 = 0;"
"*(u64 *)(r1 + 0) = r6;"
"r0 = 0;"
"exit;"
::: __clobber_all);
}
char _license[] SEC("license") = "GPL";

View File

@ -0,0 +1,245 @@
// SPDX-License-Identifier: GPL-2.0
#include "vmlinux.h"
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_helpers.h>
#include "bpf_kfuncs.h"
#include "bpf_misc.h"
char _license[] SEC("license") = "GPL";
const char xattr_zone[] = "security.bpf.zone";
char value_buf[8] = "z";
int scratch_count;
SEC("lsm/inode_init_security")
__failure __msg("cannot write into rdonly_trusted_mem")
int BPF_PROG(reject_count_write, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
*xattr_count = 0;
return 0;
}
SEC("lsm/inode_init_security")
__success
int BPF_PROG(allow_count_read, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
scratch_count = *xattr_count;
return 0;
}
SEC("lsm/inode_init_security")
__failure __msg("Possibly NULL pointer passed to trusted")
int BPF_PROG(reject_unchecked_xattrs, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
struct bpf_dynptr value;
bpf_dynptr_from_mem(value_buf, sizeof(value_buf), 0, &value);
bpf_inode_init_xattr(xattrs, xattr_count, xattr_zone, &value);
return 0;
}
SEC("lsm/inode_init_security")
__failure __msg("invalid mem access 'trusted_ptr_or_null_'")
int BPF_PROG(reject_unchecked_qstr, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
scratch_count = qstr->len;
return 0;
}
SEC("lsm/inode_init_security")
__success
int BPF_PROG(allow_spilled_count, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
struct bpf_dynptr value;
int *saved = xattr_count;
if (!xattrs)
return 0;
bpf_dynptr_from_mem(value_buf, sizeof(value_buf), 0, &value);
bpf_inode_init_xattr(xattrs, saved, xattr_zone, &value);
return 0;
}
SEC("lsm/inode_init_security")
__failure __msg("expected=rdonly_trusted_mem")
int BPF_PROG(reject_forged_count, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
struct bpf_dynptr value;
if (!xattrs)
return 0;
bpf_dynptr_from_mem(value_buf, sizeof(value_buf), 0, &value);
bpf_inode_init_xattr(xattrs, &scratch_count, xattr_zone, &value);
return 0;
}
SEC("lsm/inode_init_security")
__failure __msg("expected=rdonly_trusted_mem")
int BPF_PROG(reject_stack_count, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
struct bpf_dynptr value;
int local = 0;
if (!xattrs)
return 0;
bpf_dynptr_from_mem(value_buf, sizeof(value_buf), 0, &value);
bpf_inode_init_xattr(xattrs, &local, xattr_zone, &value);
return 0;
}
SEC("lsm/inode_init_security")
__failure __msg("expected=rdonly_trusted_mem")
int BPF_PROG(reject_other_ctx_arg, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
struct bpf_dynptr value;
if (!xattrs)
return 0;
bpf_dynptr_from_mem(value_buf, sizeof(value_buf), 0, &value);
bpf_inode_init_xattr(xattrs, (int *)xattrs, xattr_zone, &value);
return 0;
}
SEC("lsm/inode_init_security")
__failure __msg("Possibly NULL pointer passed to trusted")
int BPF_PROG(reject_null_count, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
struct bpf_dynptr value;
if (!xattrs)
return 0;
bpf_dynptr_from_mem(value_buf, sizeof(value_buf), 0, &value);
bpf_inode_init_xattr(xattrs, NULL, xattr_zone, &value);
return 0;
}
SEC("lsm/inode_init_security")
__failure __msg("dereference of modified rdonly_trusted_mem ptr")
int BPF_PROG(reject_shifted_count, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
struct bpf_dynptr value;
if (!xattrs)
return 0;
bpf_dynptr_from_mem(value_buf, sizeof(value_buf), 0, &value);
bpf_inode_init_xattr(xattrs, xattr_count + 1, xattr_zone, &value);
return 0;
}
SEC("lsm/inode_init_security")
__failure __msg("variable rdonly_trusted_mem access var_off=")
int BPF_PROG(reject_var_shifted_count, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
struct bpf_dynptr value;
if (!xattrs)
return 0;
bpf_dynptr_from_mem(value_buf, sizeof(value_buf), 0, &value);
bpf_inode_init_xattr(xattrs, xattr_count + (scratch_count & 1),
xattr_zone, &value);
return 0;
}
SEC("lsm/inode_init_security")
__failure __msg("invalid bpf_context access")
int reject_ctx_forged_count(unsigned long long *ctx)
{
volatile unsigned long long *slot = ctx;
struct bpf_dynptr value;
bpf_dynptr_from_mem(value_buf, sizeof(value_buf), 0, &value);
slot[4] = 0;
bpf_inode_init_xattr((struct xattr *)(long)slot[3],
(int *)(long)slot[4], xattr_zone, &value);
return 0;
}
static __noinline int claim_via_subprog(struct xattr *xattrs, int *xattr_count,
struct bpf_dynptr *value)
{
return bpf_inode_init_xattr(xattrs, xattr_count, xattr_zone, value);
}
SEC("lsm/inode_init_security")
__success
int BPF_PROG(allow_count_via_subprog, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
struct bpf_dynptr value;
if (!xattrs)
return 0;
bpf_dynptr_from_mem(value_buf, sizeof(value_buf), 0, &value);
claim_via_subprog(xattrs, xattr_count, &value);
return 0;
}
SEC("lsm/inode_init_security")
__failure __msg("access beyond struct xattr at off 24")
int BPF_PROG(reject_shifted_xattrs, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
struct bpf_dynptr value;
if (!xattrs)
return 0;
bpf_dynptr_from_mem(value_buf, sizeof(value_buf), 0, &value);
bpf_inode_init_xattr(xattrs + 1, xattr_count, xattr_zone, &value);
return 0;
}
SEC("lsm.s/inode_init_security")
__failure __msg("bpf_lsm_inode_init_security is not sleepable")
int BPF_PROG(reject_sleepable, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
return 0;
}
SEC("lsm_cgroup/inode_init_security")
__failure __msg("calling kernel function bpf_inode_init_xattr is not allowed")
int BPF_PROG(reject_lsm_cgroup, struct inode *inode, struct inode *dir,
const struct qstr *qstr, struct xattr *xattrs, int *xattr_count)
{
struct bpf_dynptr value;
if (!xattrs)
return 0;
bpf_dynptr_from_mem(value_buf, sizeof(value_buf), 0, &value);
bpf_inode_init_xattr(xattrs, xattr_count, xattr_zone, &value);
return 0;
}
SEC("lsm/inode_setxattr")
__failure __msg("calling kernel function bpf_inode_init_xattr is not allowed")
int BPF_PROG(reject_wrong_hook, struct mnt_idmap *idmap, struct dentry *dentry,
const char *name, const void *value, size_t size, int flags)
{
struct bpf_dynptr val;
bpf_dynptr_from_mem(value_buf, sizeof(value_buf), 0, &val);
bpf_inode_init_xattr(NULL, &scratch_count, xattr_zone, &val);
return 0;
}