From 806fc431e0a399aa50451e19a04327783df7cbc7 Mon Sep 17 00:00:00 2001 From: David Windsor Date: Tue, 15 Sep 2026 17:07:34 +0200 Subject: [PATCH] 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 Co-developed-by: Daniel Borkmann Signed-off-by: David Windsor Signed-off-by: Daniel Borkmann Signed-off-by: Alexei Starovoitov Link: https://patch.msgid.link/20260915150739.284189-4-daniel@iogearbox.net --- Documentation/bpf/kfuncs.rst | 23 +++++++++++++++++++++++ include/linux/bpf.h | 3 +++ kernel/bpf/btf.c | 7 +++++-- kernel/bpf/diagnostics.c | 2 ++ kernel/bpf/verifier.c | 36 +++++++++++++++++++++++++++++++++++- 5 files changed, 68 insertions(+), 3 deletions(-) diff --git a/Documentation/bpf/kfuncs.rst b/Documentation/bpf/kfuncs.rst index 6c2c048dccef..3f300118a623 100644 --- a/Documentation/bpf/kfuncs.rst +++ b/Documentation/bpf/kfuncs.rst @@ -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 diff --git a/include/linux/bpf.h b/include/linux/bpf.h index 3abde9a2a375..5033b934ffd9 100644 --- a/include/linux/bpf.h +++ b/include/linux/bpf.h @@ -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; }; diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c index e3fb45dcee39..314ecb0e593b 100644 --- a/kernel/bpf/btf.c +++ b/kernel/bpf/btf.c @@ -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; } } diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c index a2cac59c6639..787932f92cdf 100644 --- a/kernel/bpf/diagnostics.c +++ b/kernel/bpf/diagnostics.c @@ -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"; } diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index 3539a768b921..18aad4886f9c 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -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()) {