BPF List
 help / color / mirror / Atom feed
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


  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