From: Daniel Borkmann <daniel@iogearbox.net>
To: alexei.starovoitov@gmail.com
Cc: brauner@kernel.org, dwindsor@gmail.com, john.fastabend@gmail.com,
memxor@gmail.com, kpsingh@kernel.org, matt@bobrowski.net,
bpf@vger.kernel.org
Subject: [PATCH bpf-next 3/8] bpf: Support passing context output arguments to kfuncs
Date: Tue, 15 Sep 2026 17:07:34 +0200 [thread overview]
Message-ID: <20260915150739.284189-4-daniel@iogearbox.net> (raw)
In-Reply-To: <20260915150739.284189-1-daniel@iogearbox.net>
From: David Windsor <dwindsor@gmail.com>
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>
Signed-off-by: David Windsor <dwindsor@gmail.com>
Co-developed-by: Daniel Borkmann <daniel@iogearbox.net>
Signed-off-by: Daniel Borkmann <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 2a5fa346aada..f72413a383ba 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -923,6 +923,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,
@@ -1137,6 +1138,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 ? */
};
@@ -1715,6 +1717,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 7daf4c286c9b..8bc463e31704 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -6994,8 +6994,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];
@@ -7005,8 +7006,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 6c6b8d8520cd..cf067634d3c3 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -6579,6 +6579,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;
@@ -8358,6 +8360,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 = {
@@ -8429,6 +8434,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,
@@ -9421,6 +9427,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;
@@ -12137,6 +12150,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") ||
@@ -12900,7 +12918,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()) {
--
2.43.0
next prev parent reply other threads:[~2026-09-15 15:07 UTC|newest]
Thread overview: 31+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-15 15:07 [PATCH bpf-next 0/8] BPF LSM xattrs at inode creation support Daniel Borkmann
2026-09-15 15:07 ` [PATCH bpf-next 1/8] ocfs2: Copy the xattr name in ocfs2_initxattrs Daniel Borkmann
2026-09-15 15:16 ` sashiko-bot
2026-09-15 16:26 ` bot+bpf-ci
2026-09-16 2:47 ` Heming Zhao
2026-09-16 7:07 ` Daniel Borkmann
2026-09-16 7:28 ` Heming Zhao
2026-09-16 7:28 ` Joseph Qi
2026-09-16 7:37 ` Daniel Borkmann
2026-09-16 7:49 ` Joseph Qi
2026-09-16 7:29 ` Heming Zhao
2026-09-15 15:07 ` [PATCH bpf-next 2/8] bpf, lsm: Reject writes into the BPF LSM program context Daniel Borkmann
2026-09-15 15:07 ` Daniel Borkmann [this message]
2026-09-15 15:07 ` [PATCH bpf-next 4/8] bpf, lsm: Let BPF LSM provide xattrs at inode creation Daniel Borkmann
2026-09-23 16:57 ` Paul Moore
2026-09-23 19:11 ` Daniel Borkmann
2026-09-23 20:51 ` Paul Moore
2026-09-23 19:14 ` David Windsor
2026-09-23 20:56 ` Paul Moore
2026-09-23 21:07 ` Paul Moore
2026-09-24 16:14 ` Justin Suess
2026-09-24 16:23 ` Paul Moore
2026-09-24 18:37 ` Justin Suess
2026-09-24 19:33 ` Paul Moore
2026-09-24 19:34 ` Paul Moore
2026-09-15 15:07 ` [PATCH bpf-next 5/8] bpf, lsm: Mark the BPF LSM hook overrides noinline Daniel Borkmann
2026-09-15 15:07 ` [PATCH bpf-next 6/8] selftests/bpf: Test that the BPF LSM context is read-only Daniel Borkmann
2026-09-15 15:07 ` [PATCH bpf-next 7/8] selftests/bpf: Add verifier tests for the __ctx_out plumbing Daniel Borkmann
2026-09-15 16:26 ` bot+bpf-ci
2026-09-15 15:07 ` [PATCH bpf-next 8/8] selftests/bpf: Add tests for BPF LSM inode init labelling Daniel Borkmann
2026-09-19 19:10 ` [PATCH bpf-next 0/8] BPF LSM xattrs at inode creation support patchwork-bot+netdevbpf
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260915150739.284189-4-daniel@iogearbox.net \
--to=daniel@iogearbox.net \
--cc=alexei.starovoitov@gmail.com \
--cc=bpf@vger.kernel.org \
--cc=brauner@kernel.org \
--cc=dwindsor@gmail.com \
--cc=john.fastabend@gmail.com \
--cc=kpsingh@kernel.org \
--cc=matt@bobrowski.net \
--cc=memxor@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox