bpf: Support passing context output arguments to kfuncs

Allows programs to pass a context argument that points to a scalar
output value on to a kfunc that writes the result on the program's
behalf.

A ctx_arg_info entry can now describe a fixed-size PTR_TO_MEM context
argument through a new mem_size field, honored in btf_ctx_access().
Loading the argument yields a PTR_TO_MEM | MEM_RDONLY | PTR_TRUSTED
register of known size, readable but not writable by the program. A
kfunc argument tagged with the new "__ctx_out" suffix accepts only that
register type with a size matching the pointed-to type, so the only
value a program can pass is an output argument from its own context.

The pointer stays read-only to the program because the value is
trusted by whoever invoked the BPF program. In the first use case, the
inode_init_security LSM hook, every LSM receives a shared xattr array
and an int *xattr_count that lsm_get_xattr_slot() increments; a program
that could store a garbage count would push another LSM's write out of
bounds, so only the kfunc itself writes through it.

Suggested-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
Co-developed-by: Daniel Borkmann <daniel@iogearbox.net>
Signed-off-by: David Windsor <dwindsor@gmail.com>
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
Link: https://patch.msgid.link/20260915150739.284189-4-daniel@iogearbox.net
This commit is contained in:
David Windsor 2026-09-15 17:07:34 +02:00 committed by Alexei Starovoitov
parent c42da1d3ba
commit 806fc431e0
No known key found for this signature in database
5 changed files with 68 additions and 3 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

@ -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

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