* [PATCH bpf-next v3 0/5] bpf: bound trusted fields and derived returns to their parent's lifetime
@ 2026-10-23 21:20 Justin Suess
2026-10-23 21:20 ` [PATCH bpf-next v3 1/5] bpf: Don't apply the trusted field lists to an RCU-protected parent Justin Suess
` (4 more replies)
0 siblings, 5 replies; 8+ messages in thread
From: Justin Suess @ 2026-10-23 21:20 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Song Liu, Chris Mason, Xu Yunxiang, Amery Hung, Christian Brauner,
bpf, Justin Suess
Howdy folks,
Fixes to the lifetime of trusted pointers derived from other pointers:
fields walked out of an RCU-protected parent, and returns a kfunc
borrows from one of its arguments.
A field in the __safe_trusted or __safe_trusted_or_null sets becomes
PTR_TRUSTED regardless of how its parent is kept alive, even though
kfuncs.rst defines the annotations as marking a field "guaranteed to
be valid (trusted or rcu ...) as long as its parent pointer is valid".
For an RCU-protected parent that bound was never enforced: mm->exe_file
read under bpf_rcu_read_lock() is MEM_RCU, yet it hands out
file->f_inode as fully trusted, and the inode pointer survives
bpf_rcu_read_unlock(). It remains dereferenceable without PROBE_MEM
and passable to kfuncs expecting trusted arguments after the grace
period. Patch 1 stops applying the trusted lists to a parent that is
not itself trusted, and moves file->f_inode to the __safe_rcu_or_null
list, since inode is RCU-freed and a SLAB_TYPESAFE_BY_RCU file read
under RCU may have been reused with a NULL f_inode. Patch 2 adds the
tests.
A kfunc that returns a pointer it borrows from an argument, without
taking a reference of its own, has the same gap one level up: the
verifier types the return as trusted for the whole program.
bpf_real_data_inode(file) hands out an inode the file keeps alive, and
it stays usable after bpf_put_file(). Patch 3 adds a __derives_ret
argument annotation: the return inherits whatever keeps that argument
alive, its reference or its parent_id, and a return derived from an
RCU-protected argument of a KF_RCU kfunc is MEM_RCU. Patch 4 annotates
bpf_real_data_inode(), patch 5 adds the tests.
The referenced-parent case, a trusted field walked out of an acquired
object staying trusted after the reference is released, was patch 3 of
v2. It is dropped here along with the iterator patches: Xu Yunxiang's
series [1] carries the same field-load rule and binds iterator
elements to the iterator as well, so this series only keeps what it
does not cover. Patch 3 introduces reg_lifetime_id() in the same shape
as [1] so the two merge cleanly in either order.
With both series in, a trusted pointer is valid exactly as long as
whatever pins the pointer it was derived from: the program (hook
arguments), a reference (released by a kfunc), or an RCU critical
section.
Changes in v3:
- Rework patch 1 as Alexei suggested: gate the trusted lists on
is_trusted_reg() instead of substituting MEM_RCU for PTR_TRUSTED,
which would have handed out non-RCU-freed objects such as
vma->vm_mm as RCU pointers; add file->f_inode to __safe_rcu_or_null.
Not for stable.
- Add the __derives_ret annotation, the bpf_real_data_inode()
annotation and their tests (patches 3-5), as Kumar suggested.
- Drop the referenced-parent field binding and the iterator patches in
favor of [1].
- Rebase onto bpf-next b116ac608754.
- v2: https://lore.kernel.org/bpf/20261020145059.2146170-1-utilityemal77@gmail.com/
Changes in v2:
- Trusted fields of an RCU parent are PTR_MAYBE_NULL as well as
MEM_RCU: the parent may be a SLAB_TYPESAFE_BY_RCU object reused
under the reader, as Sashiko pointed out for file->f_inode
(patches 1, 2).
- Bind trusted iterator elements to the iterator (patches 5, 6).
- Cc stable on the three fixes.
- v1: https://lore.kernel.org/bpf/20261014173039.3666415-1-utilityemal77@gmail.com/
[1] https://lore.kernel.org/bpf/20261021210754.3270023-1-xyx2021@mail.ustc.edu.cn/
Justin
Justin Suess (5):
bpf: Don't apply the trusted field lists to an RCU-protected parent
selftests/bpf: Test f_inode of an RCU-protected file
bpf: Add a __derives_ret kfunc argument annotation
bpf: Bind the inode bpf_real_data_inode() returns to the file
selftests/bpf: Test __derives_ret return invalidation on argument
release
Documentation/bpf/kfuncs.rst | 31 ++++++++
fs/bpf_fs_kfuncs.c | 9 ++-
include/linux/bpf_verifier.h | 6 ++
kernel/bpf/btf.c | 35 ++++++++-
kernel/bpf/verifier.c | 48 +++++++++++-
.../testing/selftests/bpf/bpf_experimental.h | 9 +++
.../selftests/bpf/prog_tests/verifier.c | 2 +
.../bpf/progs/verifier_derives_ret.c | 50 +++++++++++++
.../selftests/bpf/progs/verifier_vfs_accept.c | 44 +++++++++++
.../selftests/bpf/progs/verifier_vfs_reject.c | 73 +++++++++++++++++++
.../selftests/bpf/test_kmods/bpf_testmod.c | 4 +-
.../bpf/test_kmods/bpf_testmod_kfunc.h | 2 +-
12 files changed, 302 insertions(+), 11 deletions(-)
create mode 100644 tools/testing/selftests/bpf/progs/verifier_derives_ret.c
base-commit: b116ac6087541d3d98031e786adbe9c733cd6df3
--
2.55.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH bpf-next v3 1/5] bpf: Don't apply the trusted field lists to an RCU-protected parent
2026-10-23 21:20 [PATCH bpf-next v3 0/5] bpf: bound trusted fields and derived returns to their parent's lifetime Justin Suess
@ 2026-10-23 21:20 ` Justin Suess
2026-10-23 21:58 ` sashiko-bot
2026-10-23 21:20 ` [PATCH bpf-next v3 2/5] selftests/bpf: Test f_inode of an RCU-protected file Justin Suess
` (3 subsequent siblings)
4 siblings, 1 reply; 8+ messages in thread
From: Justin Suess @ 2026-10-23 21:20 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Song Liu, Chris Mason, Xu Yunxiang, Amery Hung, Christian Brauner,
bpf, Justin Suess
Walking a field in the __safe_trusted or __safe_trusted_or_null set
yields PTR_TRUSTED whenever the parent is trusted or RCU-protected. For
an RCU-protected parent that is wrong: RCU only keeps the parent's
memory valid, and the resulting pointer survives bpf_rcu_read_unlock()
as fully trusted. mm->exe_file read under bpf_rcu_read_lock() is
MEM_RCU, and file->f_inode walked out of it escapes the RCU critical
section as a trusted inode that can be dereferenced without PROBE_MEM
and passed to kfuncs expecting trusted arguments after the grace period.
Substituting MEM_RCU for PTR_TRUSTED on an RCU parent is not a fix
either. A field of an RCU-protected object is only safe to read under
RCU if the object it points to is itself RCU-freed: inode is, but
mm_struct is not, as __mmdrop() does kmem_cache_free() without a grace
period, so an RCU-protected vma would hand out vma->vm_mm as an RCU
pointer that can be freed under the reader.
Apply the trusted lists only to a trusted parent, so a field of an
RCU-protected parent falls through to the RCU lists and is otherwise
untrusted. Add file->f_inode to the __safe_rcu_or_null list: inode is
RCU-freed, and struct file is SLAB_TYPESAFE_BY_RCU, so a file read
under RCU may have been freed and reused, in which case init_file() has
zeroed f_inode. The field may be NULL and programs have to check it.
exe_file->f_inode is now rcu_ptr_or_null_inode, bounded by the critical
section like every other RCU pointer.
Fixes: 6fcd486b3a0a ("bpf: Refactor RCU enforcement in the verifier.")
Suggested-by: Alexei Starovoitov <ast@kernel.org>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
kernel/bpf/verifier.c | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 5601d2056218..4a32875d7077 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -6517,6 +6517,11 @@ BTF_TYPE_SAFE_RCU_OR_NULL(struct request_sock) {
struct sock *sk;
};
+/* struct file is SLAB_TYPESAFE_BY_RCU. A reused file has f_inode == NULL. */
+BTF_TYPE_SAFE_RCU_OR_NULL(struct file) {
+ struct inode *f_inode;
+};
+
/* full trusted: these fields are trusted even outside of RCU CS and never NULL */
BTF_TYPE_SAFE_TRUSTED(struct bpf_iter_meta) {
struct seq_file *seq;
@@ -6593,6 +6598,7 @@ static bool type_is_rcu_or_null(struct bpf_verifier_env *env,
BTF_TYPE_EMIT(BTF_TYPE_SAFE_RCU_OR_NULL(struct mm_struct));
BTF_TYPE_EMIT(BTF_TYPE_SAFE_RCU_OR_NULL(struct sk_buff));
BTF_TYPE_EMIT(BTF_TYPE_SAFE_RCU_OR_NULL(struct request_sock));
+ BTF_TYPE_EMIT(BTF_TYPE_SAFE_RCU_OR_NULL(struct file));
return btf_nested_type_is_trusted(&env->log, reg, field_name, btf_id, "__safe_rcu_or_null");
}
@@ -6798,10 +6804,14 @@ static int check_ptr_to_btf_access(struct bpf_verifier_env *env,
*
* A regular RCU-protected pointer with __rcu tag can also be deemed
* trusted if we are in an RCU CS. Such pointer can be NULL.
+ *
+ * If the parent is not trusted, the field can't regain trusted status.
*/
- if (type_is_trusted(env, reg, field_name, btf_id)) {
+ bool trusted = is_trusted_reg(env, reg);
+
+ if (trusted && type_is_trusted(env, reg, field_name, btf_id)) {
flag |= PTR_TRUSTED;
- } else if (type_is_trusted_or_null(env, reg, field_name, btf_id)) {
+ } else if (trusted && type_is_trusted_or_null(env, reg, field_name, btf_id)) {
flag |= PTR_TRUSTED | PTR_MAYBE_NULL;
} else if (in_rcu_cs(env) && !type_may_be_null(reg->type)) {
if (type_is_rcu(env, reg, field_name, btf_id)) {
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH bpf-next v3 2/5] selftests/bpf: Test f_inode of an RCU-protected file
2026-10-23 21:20 [PATCH bpf-next v3 0/5] bpf: bound trusted fields and derived returns to their parent's lifetime Justin Suess
2026-10-23 21:20 ` [PATCH bpf-next v3 1/5] bpf: Don't apply the trusted field lists to an RCU-protected parent Justin Suess
@ 2026-10-23 21:20 ` Justin Suess
2026-10-23 21:20 ` [PATCH bpf-next v3 3/5] bpf: Add a __derives_ret kfunc argument annotation Justin Suess
` (2 subsequent siblings)
4 siblings, 0 replies; 8+ messages in thread
From: Justin Suess @ 2026-10-23 21:20 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Song Liu, Chris Mason, Xu Yunxiang, Amery Hung, Christian Brauner,
bpf, Justin Suess
Walking file->f_inode out of an RCU-protected mm->exe_file must yield
an RCU pointer that may be NULL rather than a trusted one. Add reject
tests where the inode is dereferenced without a NULL check and where it
is used as a trusted argument after bpf_rcu_read_unlock(), and an
accept test where it is checked and dereferenced inside the RCU
critical section.
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
.../selftests/bpf/progs/verifier_vfs_accept.c | 24 +++++++++
.../selftests/bpf/progs/verifier_vfs_reject.c | 51 +++++++++++++++++++
2 files changed, 75 insertions(+)
diff --git a/tools/testing/selftests/bpf/progs/verifier_vfs_accept.c b/tools/testing/selftests/bpf/progs/verifier_vfs_accept.c
index 55398c04290a..34919abcff04 100644
--- a/tools/testing/selftests/bpf/progs/verifier_vfs_accept.c
+++ b/tools/testing/selftests/bpf/progs/verifier_vfs_accept.c
@@ -100,4 +100,28 @@ int BPF_PROG(inode_rename, struct inode *old_dir, struct dentry *old_dentry,
return 0;
}
+SEC("lsm.s/bprm_committed_creds")
+__success
+int BPF_PROG(trusted_field_used_in_rcu_cs, struct linux_binprm *bprm)
+{
+ struct mm_struct *mm = bprm->mm;
+ struct inode *inode;
+ struct file *file;
+ ino_t ino = 0;
+
+ if (!mm)
+ return 0;
+
+ bpf_rcu_read_lock();
+ file = mm->exe_file;
+ if (file) {
+ inode = file->f_inode;
+ if (inode)
+ ino = inode->i_ino;
+ }
+ bpf_rcu_read_unlock();
+ __sink(ino);
+ return 0;
+}
+
char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/progs/verifier_vfs_reject.c b/tools/testing/selftests/bpf/progs/verifier_vfs_reject.c
index b5f456d57669..067b4f979902 100644
--- a/tools/testing/selftests/bpf/progs/verifier_vfs_reject.c
+++ b/tools/testing/selftests/bpf/progs/verifier_vfs_reject.c
@@ -173,4 +173,55 @@ int BPF_PROG(inode_rename, struct inode *old_dir, struct dentry *old_dentry,
return -EACCES;
return 0;
}
+
+__weak int subprog_trusted_inode(struct inode *inode __arg_trusted)
+{
+ return inode->i_ino;
+}
+
+SEC("lsm.s/bprm_committed_creds")
+__failure __msg("invalid mem access 'rcu_ptr_or_null_'")
+int BPF_PROG(trusted_field_of_rcu_parent_unchecked, struct linux_binprm *bprm)
+{
+ struct mm_struct *mm = bprm->mm;
+ struct file *file;
+ ino_t ino = 0;
+
+ if (!mm)
+ return 0;
+
+ bpf_rcu_read_lock();
+ file = mm->exe_file;
+ /* The file may have been recycled under RCU: f_inode can be NULL. */
+ if (file)
+ ino = file->f_inode->i_ino;
+ bpf_rcu_read_unlock();
+ __sink(ino);
+ return 0;
+}
+
+SEC("lsm.s/bprm_committed_creds")
+__failure __msg("R1 type=untrusted_ptr_ expected=ptr_, trusted_ptr_, rcu_ptr_")
+int BPF_PROG(trusted_field_escapes_rcu_cs, struct linux_binprm *bprm)
+{
+ struct mm_struct *mm = bprm->mm;
+ struct inode *inode;
+ struct file *file;
+
+ if (!mm)
+ return 0;
+
+ bpf_rcu_read_lock();
+ file = mm->exe_file;
+ if (!file) {
+ bpf_rcu_read_unlock();
+ return 0;
+ }
+ inode = file->f_inode;
+ bpf_rcu_read_unlock();
+
+ /* inode is an RCU pointer like file and is untrusted after unlock. */
+ subprog_trusted_inode(inode);
+ return 0;
+}
char _license[] SEC("license") = "GPL";
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH bpf-next v3 3/5] bpf: Add a __derives_ret kfunc argument annotation
2026-10-23 21:20 [PATCH bpf-next v3 0/5] bpf: bound trusted fields and derived returns to their parent's lifetime Justin Suess
2026-10-23 21:20 ` [PATCH bpf-next v3 1/5] bpf: Don't apply the trusted field lists to an RCU-protected parent Justin Suess
2026-10-23 21:20 ` [PATCH bpf-next v3 2/5] selftests/bpf: Test f_inode of an RCU-protected file Justin Suess
@ 2026-10-23 21:20 ` Justin Suess
2026-10-23 21:52 ` sashiko-bot
2026-10-23 21:20 ` [PATCH bpf-next v3 4/5] bpf: Bind the inode bpf_real_data_inode() returns to the file Justin Suess
2026-10-23 21:20 ` [PATCH bpf-next v3 5/5] selftests/bpf: Test __derives_ret return invalidation on argument release Justin Suess
4 siblings, 1 reply; 8+ messages in thread
From: Justin Suess @ 2026-10-23 21:20 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Song Liu, Chris Mason, Xu Yunxiang, Amery Hung, Christian Brauner,
bpf, Justin Suess
A kfunc that returns a pointer it borrows from one of its arguments,
without taking a reference of its own, hands out a pointer whose
lifetime is that of the argument. The verifier types every struct
pointer a non-acquire kfunc returns as PTR_TRUSTED for the whole
program, with nothing tying it to the argument it came from, so the
return outlives the reference that keeps the argument alive:
file = bpf_get_task_exe_file(task); /* acquired */
inode = bpf_real_data_inode(file); /* borrowed from file */
bpf_put_file(file); /* inode stays trusted */
mode = inode->i_mode;
The verifier cannot infer that relationship, the kfunc has to declare
it. Add a __derives_ret parameter name suffix for the argument the
return is derived from. When a kfunc with such an argument is called,
record what keeps that argument alive and set it as the return's
parent_id, so releasing the reference invalidates the return. That is
the argument's own reference if it is referenced, and its parent_id
otherwise, so a return derived from an already derived pointer stays
bound to the original reference; add reg_lifetime_id() for the
choice. On the PTR_TO_MEM return path the annotation takes
precedence over the implicit binding to a referenced argument.
A KF_RCU kfunc may be passed an RCU-protected argument, which only
guarantees its memory until the end of the critical section. A return
derived from it carries MEM_RCU instead of PTR_TRUSTED, so it is
invalidated by bpf_rcu_read_unlock() like the argument. No such kfunc
exists today, but the rule keeps the next one from reopening the hole.
Check at registration that at most one argument carries the suffix,
that it is a pointer, that the kfunc returns a pointer, and that the
kfunc is neither KF_ACQUIRE nor KF_RELEASE: an acquire returns its own
reference, a release destroys it.
Suggested-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
Documentation/bpf/kfuncs.rst | 31 +++++++++++++++++++++++++++++++
include/linux/bpf_verifier.h | 6 ++++++
kernel/bpf/btf.c | 35 ++++++++++++++++++++++++++++++++++-
kernel/bpf/verifier.c | 34 ++++++++++++++++++++++++++++++++--
4 files changed, 103 insertions(+), 3 deletions(-)
diff --git a/Documentation/bpf/kfuncs.rst b/Documentation/bpf/kfuncs.rst
index 73578a4c3b1f..ef4b1baba2a5 100644
--- a/Documentation/bpf/kfuncs.rst
+++ b/Documentation/bpf/kfuncs.rst
@@ -351,6 +351,37 @@ 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.
+2.3.10 __derives_ret Annotation
+-------------------------------
+
+This annotation is used to indicate that the pointer the kfunc returns is
+derived from the annotated pointer argument: it points into, or is kept
+alive by, the object the argument refers to, and the kfunc takes no
+reference of its own on it. The returned pointer is therefore only valid
+for as long as the argument is. The verifier binds the return to whatever
+keeps the argument alive, so the two are invalidated together, e.g. when
+the reference that pins the argument is released.
+
+An example is given below::
+
+ __bpf_kfunc struct inode *bpf_real_data_inode(struct file *file__derives_ret)
+ {
+ ...
+ }
+
+Here the inode is kept alive by the file. A program that obtained the file
+from a KF_ACQUIRE kfunc may use the inode until it releases the file, and
+no longer afterwards. A return derived from an argument that is valid for
+the whole program, such as a tracepoint or struct_ops callback argument,
+is valid for the whole program as well. A return derived from an
+RCU-protected argument, which only a KF_RCU kfunc accepts, is itself
+RCU-protected and is only valid until the end of the RCU critical section.
+
+At most one argument of a kfunc can carry the annotation. The kfunc must
+return a pointer and must not be a KF_ACQUIRE or a KF_RELEASE kfunc: the
+former returns a reference of its own, the latter has nothing left to
+lend. Registering a kfunc that breaks these rules fails.
+
.. _BPF_kfunc_nodef:
2.4 Using an existing kernel function
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 5926f13ff43c..afd8832f9ae1 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -1818,6 +1818,12 @@ struct bpf_call_arg_meta {
struct arg_constant_desc arg_constant;
/* The type of the dynptr that the kfunc writes through */
enum bpf_dynptr_type dynptr_write_type;
+ /* What keeps the __derives_ret argument alive, see reg_lifetime_id() */
+ struct {
+ bool found;
+ bool rcu;
+ u32 parent_id;
+ } derives_ret;
/* arg_{btf,btf_id,owning_ref} are used by kfunc-specific handling,
* generally to pass info about user-defined local kptr types to later
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 538129ed2418..d78ae95da35d 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -9768,6 +9768,39 @@ static int btf_check_iter_kfuncs(struct btf *btf, const char *func_name,
return 0;
}
+/*
+ * A __derives_ret argument lends its lifetime to the kfunc's return. It has
+ * to be a pointer, there can only be one, and the kfunc has to return a
+ * pointer that is neither acquired nor released: an acquire returns a
+ * reference of its own, a release has nothing left to lend.
+ */
+static int btf_check_derives_ret_kfunc(struct btf *btf, const struct btf_type *func,
+ u32 func_flags)
+{
+ const struct btf_param *args = btf_params(func);
+ const struct btf_type *t;
+ int i, n = 0;
+
+ for (i = 0; i < btf_type_vlen(func); i++) {
+ if (!btf_param_match_suffix(btf, &args[i], "__derives_ret"))
+ continue;
+ t = btf_type_skip_modifiers(btf, args[i].type, NULL);
+ if (!t || !btf_type_is_ptr(t) || n++)
+ return -EINVAL;
+ }
+ if (!n)
+ return 0;
+
+ if (func_flags & (KF_ACQUIRE | KF_RELEASE))
+ return -EINVAL;
+
+ t = btf_type_skip_modifiers(btf, func->type, NULL);
+ if (!t || !btf_type_is_ptr(t))
+ return -EINVAL;
+
+ return 0;
+}
+
static int btf_check_kfunc_name(struct btf *btf, const char *func_name, u32 kind)
{
#ifdef CONFIG_DEBUG_INFO_BTF_MODULES
@@ -9828,7 +9861,7 @@ static int btf_check_kfunc_protos(struct btf *btf, u32 func_id, u32 func_flags)
return err;
}
- return 0;
+ return btf_check_derives_ret_kfunc(btf, func, func_flags);
}
/* Kernel Function (kfunc) BTF ID set registration API */
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 4a32875d7077..5a13b5ef688b 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -1627,6 +1627,12 @@ static bool reg_is_referenced(struct bpf_verifier_env *env, const struct bpf_reg
return find_reference_state(env->cur_state, reg->id);
}
+/* The reference that keeps @reg alive, or 0 if it is valid for the whole program. */
+static u32 reg_lifetime_id(struct bpf_verifier_env *env, const struct bpf_reg_state *reg)
+{
+ return reg_is_referenced(env, reg) ? reg->id : reg->parent_id;
+}
+
static int release_lock_state(struct bpf_verifier_env *env, int type, int id, void *ptr)
{
struct bpf_verifier_state *state = env->cur_state;
@@ -9097,6 +9103,8 @@ static int process_arg_ptr_to_btf_id(struct bpf_verifier_env *env, struct bpf_re
struct bpf_call_arg_meta *meta, int insn_idx);
static bool is_kfunc_arg_nonown_allowed(const struct btf *btf,
const struct btf_param *arg);
+static bool is_kfunc_arg_derives_ret(const struct btf *btf,
+ const struct btf_param *arg);
static bool is_kfunc_arg_scalar_with_name(const struct btf *btf,
const struct btf_param *arg,
const char *name);
@@ -9848,6 +9856,13 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 p
if (reg_is_referenced(env, reg))
update_ref_obj(&meta->ref_obj, reg);
+ /* The return of a __derives_ret kfunc lives exactly as long as this argument. */
+ if (btf_arg && is_kfunc_arg_derives_ret(meta->btf, btf_arg)) {
+ meta->derives_ret.found = true;
+ meta->derives_ret.rcu = is_rcu_reg(reg);
+ meta->derives_ret.parent_id = reg_lifetime_id(env, reg);
+ }
+
switch (base_type(arg_type)) {
case ARG_CONST_SCALAR:
err = process_const_arg(env, reg, argno, meta);
@@ -13278,6 +13293,11 @@ static bool is_kfunc_arg_const_str(const struct btf *btf, const struct btf_param
return btf_param_match_suffix(btf, arg, "__str");
}
+static bool is_kfunc_arg_derives_ret(const struct btf *btf, const struct btf_param *arg)
+{
+ return btf_param_match_suffix(btf, arg, "__derives_ret");
+}
+
static bool is_kfunc_arg_irq_flag(const struct btf *btf, const struct btf_param *arg)
{
return btf_param_match_suffix(btf, arg, "__irq_flag");
@@ -15790,14 +15810,16 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
regs[BPF_REG_0].type |= MEM_RDONLY;
/* Ensures we don't access the memory after a release_reference() */
- if (meta.ref_obj.id) {
+ if (meta.derives_ret.found) {
+ regs[BPF_REG_0].parent_id = meta.derives_ret.parent_id;
+ } else if (meta.ref_obj.id) {
err = validate_ref_obj(env, &meta.ref_obj);
if (err)
return err;
regs[BPF_REG_0].parent_id = meta.ref_obj.id;
}
- if (is_kfunc_rcu_protected(&meta))
+ if (is_kfunc_rcu_protected(&meta) || meta.derives_ret.rcu)
regs[BPF_REG_0].type |= MEM_RCU;
} else {
enum bpf_reg_type type = PTR_TO_BTF_ID;
@@ -15819,6 +15841,12 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
* bpf_iter_task_next).
*/
type |= MEM_RCU;
+ } else if (meta.derives_ret.rcu) {
+ /*
+ * Derived from an RCU-protected argument, so
+ * only valid for as long as that is.
+ */
+ type |= MEM_RCU;
} else {
/*
* Any PTR_TO_BTF_ID that is returned from a BPF
@@ -15832,6 +15860,8 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
regs[BPF_REG_0].btf = desc_btf;
regs[BPF_REG_0].type = type;
regs[BPF_REG_0].btf_id = ptr_type_id;
+ if (meta.derives_ret.found)
+ regs[BPF_REG_0].parent_id = meta.derives_ret.parent_id;
}
if (is_kfunc_ret_null(&meta)) {
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH bpf-next v3 4/5] bpf: Bind the inode bpf_real_data_inode() returns to the file
2026-10-23 21:20 [PATCH bpf-next v3 0/5] bpf: bound trusted fields and derived returns to their parent's lifetime Justin Suess
` (2 preceding siblings ...)
2026-10-23 21:20 ` [PATCH bpf-next v3 3/5] bpf: Add a __derives_ret kfunc argument annotation Justin Suess
@ 2026-10-23 21:20 ` Justin Suess
2026-10-23 21:20 ` [PATCH bpf-next v3 5/5] selftests/bpf: Test __derives_ret return invalidation on argument release Justin Suess
4 siblings, 0 replies; 8+ messages in thread
From: Justin Suess @ 2026-10-23 21:20 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Song Liu, Chris Mason, Xu Yunxiang, Amery Hung, Christian Brauner,
bpf, Justin Suess
bpf_real_data_inode() returns the inode behind the file's dentry
without taking a reference on it: the file keeps the inode alive. The
verifier types the return as trusted for the whole program, so a
program that acquired the file from bpf_get_task_exe_file() can keep
using the inode after bpf_put_file(), when the file's last reference
may have gone and the inode with it.
Mark the file argument __derives_ret so the returned inode lives
exactly as long as the file does.
Fixes: 3f8c65b06faf ("bpf: have bpf_real_data_inode() take a struct file")
Suggested-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
fs/bpf_fs_kfuncs.c | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/fs/bpf_fs_kfuncs.c b/fs/bpf_fs_kfuncs.c
index 08f0847c4970..8b58379536ca 100644
--- a/fs/bpf_fs_kfuncs.c
+++ b/fs/bpf_fs_kfuncs.c
@@ -480,7 +480,7 @@ __bpf_kfunc int bpf_sock_read_xattr(struct socket *sock, const char *name__str,
/**
* bpf_real_data_inode - get the real inode hosting a file's data
- * @file: file to resolve
+ * @file__derives_ret: file to resolve, which keeps the returned inode alive
*
* Resolve @file to the inode that hosts its data. For a regular file on a
* union/overlay filesystem this is the underlying (upper or lower) inode that
@@ -491,11 +491,14 @@ __bpf_kfunc int bpf_sock_read_xattr(struct socket *sock, const char *name__str,
* inode itself is returned; for any file on a non-union filesystem the inode
* attached to @file is returned.
*
+ * The returned inode is only valid for as long as the file is: a program
+ * that acquired the file must not use the inode after releasing it.
+ *
* Return: The inode hosting @file's data, or NULL.
*/
-__bpf_kfunc struct inode *bpf_real_data_inode(struct file *file)
+__bpf_kfunc struct inode *bpf_real_data_inode(struct file *file__derives_ret)
{
- return d_real_inode(file_dentry(file));
+ return d_real_inode(file_dentry(file__derives_ret));
}
__bpf_kfunc_end_defs();
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH bpf-next v3 5/5] selftests/bpf: Test __derives_ret return invalidation on argument release
2026-10-23 21:20 [PATCH bpf-next v3 0/5] bpf: bound trusted fields and derived returns to their parent's lifetime Justin Suess
` (3 preceding siblings ...)
2026-10-23 21:20 ` [PATCH bpf-next v3 4/5] bpf: Bind the inode bpf_real_data_inode() returns to the file Justin Suess
@ 2026-10-23 21:20 ` Justin Suess
4 siblings, 0 replies; 8+ messages in thread
From: Justin Suess @ 2026-10-23 21:20 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Song Liu, Chris Mason, Xu Yunxiang, Amery Hung, Christian Brauner,
bpf, Justin Suess
A pointer a kfunc derives from a __derives_ret argument must not
outlive the reference that keeps the argument alive. Cover the inode
bpf_real_data_inode() returns for a file acquired from
bpf_get_task_exe_file(): a reject test dereferences it after
bpf_put_file(), an accept test before.
Annotate the argument of bpf_kfunc_call_test3() in bpf_testmod, which
returns its argument, and cover a socket acquired through a helper the
same way around bpf_sk_release().
Signed-off-by: Justin Suess <utilityemal77@gmail.com>
---
.../testing/selftests/bpf/bpf_experimental.h | 9 ++++
.../selftests/bpf/prog_tests/verifier.c | 2 +
.../bpf/progs/verifier_derives_ret.c | 50 +++++++++++++++++++
.../selftests/bpf/progs/verifier_vfs_accept.c | 20 ++++++++
.../selftests/bpf/progs/verifier_vfs_reject.c | 22 ++++++++
.../selftests/bpf/test_kmods/bpf_testmod.c | 4 +-
.../bpf/test_kmods/bpf_testmod_kfunc.h | 2 +-
7 files changed, 106 insertions(+), 3 deletions(-)
create mode 100644 tools/testing/selftests/bpf/progs/verifier_derives_ret.c
diff --git a/tools/testing/selftests/bpf/bpf_experimental.h b/tools/testing/selftests/bpf/bpf_experimental.h
index 2893bf06ff25..e9dfd5d2e1ec 100644
--- a/tools/testing/selftests/bpf/bpf_experimental.h
+++ b/tools/testing/selftests/bpf/bpf_experimental.h
@@ -64,6 +64,15 @@ extern struct file *bpf_get_task_exe_file(struct task_struct *task) __ksym;
*/
extern void bpf_put_file(struct file *file) __ksym;
+/* Description
+ * Resolve the supplied file to the inode hosting its data. The supplied
+ * file must be trusted/referenced; the inode is valid only as long as
+ * the file is.
+ * Returns
+ * The inode hosting the file's data, or NULL.
+ */
+extern struct inode *bpf_real_data_inode(struct file *file) __ksym;
+
/* Description
* Resolve a pathname for the supplied path and store it in the supplied
* buffer. The supplied path must be trusted/referenced.
diff --git a/tools/testing/selftests/bpf/prog_tests/verifier.c b/tools/testing/selftests/bpf/prog_tests/verifier.c
index 04740ba37e91..ff6e6515ea48 100644
--- a/tools/testing/selftests/bpf/prog_tests/verifier.c
+++ b/tools/testing/selftests/bpf/prog_tests/verifier.c
@@ -45,6 +45,7 @@
#include "verifier_ctx_sk_msg.skel.h"
#include "verifier_d_path.skel.h"
#include "verifier_default_trusted_ptr.skel.h"
+#include "verifier_derives_ret.skel.h"
#include "verifier_direct_packet_access.skel.h"
#include "verifier_direct_stack_access_wraparound.skel.h"
#include "verifier_div0.skel.h"
@@ -237,6 +238,7 @@ void test_verifier_ctx(void) { RUN_TESTS(verifier_ctx); }
void test_verifier_ctx_sk_msg(void) { RUN(verifier_ctx_sk_msg); }
void test_verifier_d_path(void) { RUN(verifier_d_path); }
void test_verifier_default_trusted_ptr(void) { RUN_TESTS(verifier_default_trusted_ptr); }
+void test_verifier_derives_ret(void) { RUN_TESTS(verifier_derives_ret); }
void test_verifier_direct_packet_access(void) { RUN(verifier_direct_packet_access); }
void test_verifier_direct_stack_access_wraparound(void) { RUN(verifier_direct_stack_access_wraparound); }
void test_verifier_div0(void) { RUN(verifier_div0); }
diff --git a/tools/testing/selftests/bpf/progs/verifier_derives_ret.c b/tools/testing/selftests/bpf/progs/verifier_derives_ret.c
new file mode 100644
index 000000000000..041d2a656233
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/verifier_derives_ret.c
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Justin Suess */
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+
+#include "bpf_misc.h"
+#include "../test_kmods/bpf_testmod_kfunc.h"
+
+/* bpf_kfunc_call_test3() returns its __derives_ret argument. */
+
+SEC("tc")
+__success
+int derived_sock_used_before_release(struct __sk_buff *skb)
+{
+ struct bpf_sock_tuple tuple = {};
+ struct sock *derived;
+ struct bpf_sock *sk;
+ int state;
+
+ sk = bpf_sk_lookup_tcp(skb, &tuple, sizeof(tuple.ipv4), BPF_F_CURRENT_NETNS, 0);
+ if (!sk)
+ return 0;
+
+ derived = bpf_kfunc_call_test3((struct sock *)sk);
+ state = derived->__sk_common.skc_state;
+ bpf_sk_release(sk);
+ return state;
+}
+
+SEC("tc")
+__failure __msg("invalid mem access 'scalar'")
+int derived_sock_outlives_release(struct __sk_buff *skb)
+{
+ struct bpf_sock_tuple tuple = {};
+ struct sock *derived;
+ struct bpf_sock *sk;
+
+ sk = bpf_sk_lookup_tcp(skb, &tuple, sizeof(tuple.ipv4), BPF_F_CURRENT_NETNS, 0);
+ if (!sk)
+ return 0;
+
+ derived = bpf_kfunc_call_test3((struct sock *)sk);
+ bpf_sk_release(sk);
+
+ /* derived was pinned by sk's reference and died with it. */
+ return derived->__sk_common.skc_state;
+}
+
+char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/progs/verifier_vfs_accept.c b/tools/testing/selftests/bpf/progs/verifier_vfs_accept.c
index 34919abcff04..3f9e1f8fba58 100644
--- a/tools/testing/selftests/bpf/progs/verifier_vfs_accept.c
+++ b/tools/testing/selftests/bpf/progs/verifier_vfs_accept.c
@@ -124,4 +124,24 @@ int BPF_PROG(trusted_field_used_in_rcu_cs, struct linux_binprm *bprm)
return 0;
}
+SEC("lsm.s/file_open")
+__success
+int BPF_PROG(derived_inode_used_before_put)
+{
+ struct task_struct *task = bpf_get_current_task_btf();
+ struct file *file = bpf_get_task_exe_file(task);
+ struct inode *inode;
+ umode_t mode = 0;
+
+ if (!file)
+ return 0;
+
+ inode = bpf_real_data_inode(file);
+ if (inode)
+ mode = inode->i_mode;
+ bpf_put_file(file);
+ __sink(mode);
+ return 0;
+}
+
char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/progs/verifier_vfs_reject.c b/tools/testing/selftests/bpf/progs/verifier_vfs_reject.c
index 067b4f979902..df2fe54deeff 100644
--- a/tools/testing/selftests/bpf/progs/verifier_vfs_reject.c
+++ b/tools/testing/selftests/bpf/progs/verifier_vfs_reject.c
@@ -224,4 +224,26 @@ int BPF_PROG(trusted_field_escapes_rcu_cs, struct linux_binprm *bprm)
subprog_trusted_inode(inode);
return 0;
}
+
+SEC("lsm.s/file_open")
+__failure __msg("invalid mem access 'scalar'")
+int BPF_PROG(derived_inode_outlives_parent_reference)
+{
+ struct task_struct *task = bpf_get_current_task_btf();
+ struct file *file = bpf_get_task_exe_file(task);
+ struct inode *inode;
+
+ if (!file)
+ return 0;
+
+ inode = bpf_real_data_inode(file);
+ if (!inode) {
+ bpf_put_file(file);
+ return 0;
+ }
+ bpf_put_file(file);
+
+ /* inode was derived from file's reference and died with it. */
+ return inode->i_mode;
+}
char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
index 6380f52f4dd2..5308daa2259d 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
@@ -1073,9 +1073,9 @@ __bpf_kfunc int bpf_kfunc_call_test2(struct sock *sk, u32 a, u32 b)
return a + b;
}
-__bpf_kfunc struct sock *bpf_kfunc_call_test3(struct sock *sk)
+__bpf_kfunc struct sock *bpf_kfunc_call_test3(struct sock *sk__derives_ret)
{
- return sk;
+ return sk__derives_ret;
}
__bpf_kfunc long noinline bpf_kfunc_call_test4(signed char a, short b, int c, long d)
diff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h b/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
index 3b810f1e438f..ccef296a9961 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
@@ -230,7 +230,7 @@ int bpf_testmod_ops3_call_test_nullable(void) __ksym;
__u64 bpf_kfunc_call_test1(struct sock *sk, __u32 a, __u64 b,
__u32 c, __u64 d) __ksym;
int bpf_kfunc_call_test2(struct sock *sk, __u32 a, __u32 b) __ksym;
-struct sock *bpf_kfunc_call_test3(struct sock *sk) __ksym;
+struct sock *bpf_kfunc_call_test3(struct sock *sk__derives_ret) __ksym;
long bpf_kfunc_call_test4(signed char a, short b, int c, long d) __ksym;
int bpf_kfunc_call_test5(__u8 a, __u16 b, __u32 c) __ksym;
#ifdef __SIZEOF_INT128__
--
2.55.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH bpf-next v3 3/5] bpf: Add a __derives_ret kfunc argument annotation
2026-10-23 21:20 ` [PATCH bpf-next v3 3/5] bpf: Add a __derives_ret kfunc argument annotation Justin Suess
@ 2026-10-23 21:52 ` sashiko-bot
0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-10-23 21:52 UTC (permalink / raw)
To: Justin Suess; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Critical] bpf: check_func_arg() fails to propagate parent_id into meta->ref_obj
--
commit 3a36b9b9ff614d7729738b098cb55d91d05fb314
Author: Justin Suess <utilityemal77@gmail.com>
bpf: Add a __derives_ret kfunc argument annotation
This commit adds a __derives_ret parameter name suffix for kfunc pointer
arguments whose lifetime bounds the returned pointer. When a kfunc with a
__derives_ret argument is called, the verifier propagates the argument's
reference lifetime (via parent_id) or RCU protection (via MEM_RCU) to the
returned register.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 4a32875d70778..5a13b5ef688b7 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -9848,6 +9856,13 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 p
> if (reg_is_referenced(env, reg))
> update_ref_obj(&meta->ref_obj, reg);
>
> + /* The return of a __derives_ret kfunc lives exactly as long as this argument. */
> + if (btf_arg && is_kfunc_arg_derives_ret(meta->btf, btf_arg)) {
> + meta->derives_ret.found = true;
> + meta->derives_ret.rcu = is_rcu_reg(reg);
> + meta->derives_ret.parent_id = reg_lifetime_id(env, reg);
> + }
[Severity: Critical]
When a pointer returned by a __derives_ret kfunc is kept alive by a parent
reference (where reg->id is not in cur_state->refs, but reg->parent_id holds
the active reference ID), should check_func_arg() and update_ref_obj() also
propagate reg_lifetime_id(env, reg) into meta->ref_obj?
Currently, in kernel/bpf/verifier.c:check_func_arg(), update_ref_obj() is
only called when reg_is_referenced(env, reg) is true, and update_ref_obj()
records reg->id rather than reg_lifetime_id(env, reg):
kernel/bpf/verifier.c:update_ref_obj() {
ref_obj->id = reg->id;
ref_obj->parent_id = reg->parent_id;
ref_obj->cnt++;
}
If a BPF program acquires a referenced object, passes it to a __derives_ret
kfunc to obtain a borrowed PTR_TO_BTF_ID | PTR_TRUSTED pointer with id == 0
and parent_id != 0, and then passes that derived pointer to another helper
or kfunc that relies on meta->ref_obj.id, meta->ref_obj.id remains 0:
1. In check_func_arg() -> process_dynptr_func() -> mark_stack_slots_dynptr()
(for example, via bpf_dynptr_from_file() or bpf_dynptr_from_skb()),
mark_stack_slots_dynptr() sets:
parent_id = ref_obj->id;
if (dynptr_type_referenced(type)) {
id = acquire_reference(env, insn_idx, parent_id);
Because ref_obj->id is 0, the dynptr and any child reference it acquires
are created with parent_id == 0 instead of the underlying reference ID,
so releasing the original reference neither rejects an unreleased file
dynptr nor invalidates the dynptr or its slices.
2. In check_kfunc_call(), for a non-__derives_ret kfunc returning PTR_TO_MEM:
if (meta.derives_ret.found) {
regs[BPF_REG_0].parent_id = meta.derives_ret.parent_id;
} else if (meta.ref_obj.id) {
err = validate_ref_obj(env, &meta.ref_obj);
if (err)
return err;
regs[BPF_REG_0].parent_id = meta.ref_obj.id;
}
When passed a __derives_ret-derived pointer, meta.ref_obj.id is 0, leaving
regs[BPF_REG_0].parent_id as 0 (and bypassing validate_ref_obj() if
another referenced argument was also passed).
3. In check_helper_call() for socket pointer-casting helpers
(is_ptr_cast_function()), meta.ref_obj.id is 0, so the cast pointer is
not bound to the original reference.
Can this allow derived dynptrs, memory pointers, or cast socket pointers to
outlive release_reference() on the original acquired object and cause a
use-after-free?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261023212041.884996-1-utilityemal77@gmail.com?part=3
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH bpf-next v3 1/5] bpf: Don't apply the trusted field lists to an RCU-protected parent
2026-10-23 21:20 ` [PATCH bpf-next v3 1/5] bpf: Don't apply the trusted field lists to an RCU-protected parent Justin Suess
@ 2026-10-23 21:58 ` sashiko-bot
0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-10-23 21:58 UTC (permalink / raw)
To: Justin Suess; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] bpf: unsafe RCU trust for file->f_inode in type_is_rcu_or_null()
- [High] bpf: bpf_inode_storage_get() accepts unreferenced RCU f_inode
- [High] bpf: check_ptr_to_btf_access() leaves RCU parent fields as plain PTR_TO_BTF_ID
--
commit 07389cd2696e6821d95557c4e45fb257d164b7dd
Author: Justin Suess <utilityemal77@gmail.com>
bpf: Don't apply the trusted field lists to an RCU-protected parent
This commit restricts the __safe_trusted and __safe_trusted_or_null BTF
field lists in check_ptr_to_btf_access() to trusted parent registers so
that fields walked from an RCU-protected parent do not gain PTR_TRUSTED.
It also adds struct file's f_inode field to BTF_TYPE_SAFE_RCU_OR_NULL and
type_is_rcu_or_null().
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 5601d20562186..4a32875d70778 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -6517,6 +6517,11 @@ BTF_TYPE_SAFE_RCU_OR_NULL(struct request_sock) {
> struct sock *sk;
> };
>
> +/* struct file is SLAB_TYPESAFE_BY_RCU. A reused file has f_inode == NULL. */
> +BTF_TYPE_SAFE_RCU_OR_NULL(struct file) {
> + struct inode *f_inode;
> +};
[Severity: High]
Is it always safe to treat file->f_inode walked from an RCU-protected
struct file as MEM_RCU after a NULL check?
When a BPF program inside an RCU read-side critical section reads
file->f_inode from a PTR_TO_BTF_ID | MEM_RCU struct file (such as
mm->exe_file or files->fd_array[i]) and checks it for non-NULL, the
verifier marks the resulting struct inode pointer as PTR_TO_BTF_ID |
MEM_RCU. Because bpf_may_fault_on_deref() is false for that type,
bpf_convert_ctx_accesses() emits raw BPF_LDX | BPF_MEM loads without
BPF_PROBE_MEM.
However, not all inodes reachable through file->f_inode are freed after an
RCU grace period. For example, pipefs uses free_inode_nonrcu():
fs/pipe.c:
static const struct super_operations pipefs_ops = {
.destroy_inode = free_inode_nonrcu,
.statfs = simple_statfs,
};
In destroy_inode(), when ops->destroy_inode is set and ops->free_inode is
NULL, the inode is freed synchronously via kmem_cache_free(inode_cachep,
inode) without call_rcu():
fs/inode.c:destroy_inode() {
...
if (ops->destroy_inode) {
ops->destroy_inode(inode);
if (!ops->free_inode)
return;
}
inode->free_inode = ops->free_inode;
call_rcu(&inode->i_rcu, i_callback);
}
In addition, because filp_cache uses SLAB_TYPESAFE_BY_RCU and file_free()
frees struct file immediately without an RCU grace period, a struct file
closed during the RCU critical section only transiently has f_inode set to
NULL in init_file() before file_init_path() or do_dentry_open() populates
file->f_inode with the newly opened file's inode:
fs/file_table.c:file_init_path() {
file->__f_path = *path;
file->f_inode = path->dentry->d_inode;
file->f_mapping = path->dentry->d_inode->i_mapping;
}
If a SLAB_TYPESAFE_BY_RCU struct file (including one read from
mm->exe_file) is closed, reallocated for a pipe via create_pipe_files() ->
alloc_file_pseudo(), and closed again within the BPF program's RCU
critical section, can the BPF program dereference a freed pipefs struct
inode without BPF_PROBE_MEM?
>
> /* full trusted: these fields are trusted even outside of RCU CS and never NULL */
> BTF_TYPE_SAFE_TRUSTED(struct bpf_iter_meta) {
> struct seq_file *seq;
> @@ -6593,6 +6598,7 @@ static bool type_is_rcu_or_null(struct bpf_verifier_env *env,
> BTF_TYPE_EMIT(BTF_TYPE_SAFE_RCU_OR_NULL(struct mm_struct));
> BTF_TYPE_EMIT(BTF_TYPE_SAFE_RCU_OR_NULL(struct sk_buff));
> BTF_TYPE_EMIT(BTF_TYPE_SAFE_RCU_OR_NULL(struct request_sock));
> + BTF_TYPE_EMIT(BTF_TYPE_SAFE_RCU_OR_NULL(struct file));
>
> return btf_nested_type_is_trusted(&env->log, reg, field_name, btf_id, "__safe_rcu_or_null");
> }
[Severity: High]
Can exposing file->f_inode as PTR_TO_BTF_ID | MEM_RCU here also allow
bpf_inode_storage_get() to allocate local storage on an inode that is
already being destroyed, leading to a memory leak and use-after-free on
inode->i_security?
bpf_inode_storage_get_proto takes ARG_PTR_TO_BTF_ID_OR_NULL for its inode
argument, which check_reg_type() matches against btf_ptr_types:
kernel/bpf/verifier.c:
static const struct bpf_reg_types btf_ptr_types = {
.types = {
PTR_TO_BTF_ID,
PTR_TO_BTF_ID | PTR_TRUSTED,
PTR_TO_BTF_ID | MEM_RCU,
},
};
Unlike bpf_task_storage_get(), bpf_cgrp_storage_get(), and
bpf_sk_storage_get(), bpf_inode_storage_get() does not verify that the
inode still has a non-zero reference count before creating storage:
kernel/bpf/bpf_inode_storage.c:bpf_inode_storage_get() {
...
/* This helper must only called from where the inode is guaranteed
* to have a refcount and cannot be freed.
*/
if (flags & BPF_LOCAL_STORAGE_GET_F_CREATE) {
sdata = bpf_local_storage_update(
inode, (struct bpf_local_storage_map *)map, value,
BPF_NOEXIST, false);
return IS_ERR(sdata) ? NULL : sdata->data;
}
...
}
When the last reference to an unlinked file (such as mm->exe_file) is
dropped, __fput() -> dput() -> iput_final() -> evict() -> destroy_inode()
-> __destroy_inode() calls security_inode_free(inode) before the RCU grace
period starts:
security/security.c:security_inode_free() {
call_void_hook(inode_free_security, inode);
if (!inode->i_security)
return;
call_rcu((struct rcu_head *)inode->i_security, inode_free_by_rcu);
}
This runs bpf_inode_storage_free(inode) synchronously while leaving
inode->i_security non-NULL until inode_free_by_rcu() runs after the grace
period.
If a BPF LSM program inside bpf_rcu_read_lock() walks file = mm->exe_file
and inode = file->f_inode and calls bpf_inode_storage_get() with
BPF_LOCAL_STORAGE_GET_F_CREATE after bpf_inode_storage_free() has already
run, bpf_local_storage_update() will attach a new bpf_local_storage with
storage->owner = inode to bpf_inode(inode)->storage. Once the RCU grace
period finishes, inode and inode->i_security are freed without
bpf_inode_storage_free() running again, and subsequent map teardown or
element deletion in bpf_selem_unlink_storage_nolock_misc() will read from
the freed inode and write NULL into the freed inode->i_security blob:
kernel/bpf/bpf_local_storage.c:bpf_selem_unlink_storage_nolock_misc() {
...
if (free_local_storage) {
local_storage->owner = NULL;
/* After this RCU_INIT, owner may be freed and cannot be used */
RCU_INIT_POINTER(*owner_storage(smap, owner), NULL);
}
...
}
> @@ -6798,10 +6804,14 @@ static int check_ptr_to_btf_access(struct bpf_verifier_env *env,
> *
> * A regular RCU-protected pointer with __rcu tag can also be deemed
> * trusted if we are in an RCU CS. Such pointer can be NULL.
> + *
> + * If the parent is not trusted, the field can't regain trusted status.
> */
> - if (type_is_trusted(env, reg, field_name, btf_id)) {
> + bool trusted = is_trusted_reg(env, reg);
> +
> + if (trusted && type_is_trusted(env, reg, field_name, btf_id)) {
> flag |= PTR_TRUSTED;
> - } else if (type_is_trusted_or_null(env, reg, field_name, btf_id)) {
> + } else if (trusted && type_is_trusted_or_null(env, reg, field_name, btf_id)) {
> flag |= PTR_TRUSTED | PTR_MAYBE_NULL;
> } else if (in_rcu_cs(env) && !type_may_be_null(reg->type)) {
> if (type_is_rcu(env, reg, field_name, btf_id)) {
[Severity: High]
When reg is an RCU-protected pointer (is_rcu_reg(reg) is true and trusted
is false) and we are in an RCU critical section, what happens to fields in
type_is_trusted() or type_is_trusted_or_null() (such as vma->vm_file,
vma->vm_mm, dentry->d_inode, socket->sk, or bprm->file) or untagged
fields (such as exe_file->f_path.dentry or exe_file->f_mapping->host)?
Looking at the rest of this if/else chain in check_ptr_to_btf_access():
} else if (in_rcu_cs(env) && !type_may_be_null(reg->type)) {
if (type_is_rcu(env, reg, field_name, btf_id)) {
/* ignore __rcu tag and mark it MEM_RCU */
flag |= MEM_RCU;
} else if (flag & MEM_RCU ||
type_is_rcu_or_null(env, reg, field_name, btf_id)) {
...
} else if (flag & (MEM_PERCPU | MEM_USER)) {
/* keep as-is */
} else {
/* walking unknown pointers yields old deprecated PTR_TO_BTF_ID */
clear_trusted_flags(&flag);
}
}
Rather than being marked PTR_UNTRUSTED as described in the commit message,
those fields fall into the else branch and call clear_trusted_flags(&flag)
(and any __nullable PTR_MAYBE_NULL bit is stripped afterwards because
bpf_may_fault_on_deref(PTR_TO_BTF_ID) is true). That leaves the child
register as a plain PTR_TO_BTF_ID with neither MEM_RCU nor PTR_UNTRUSTED.
When the program calls bpf_rcu_read_unlock(),
invalidate_rcu_protected_refs() only clears registers with MEM_RCU:
kernel/bpf/verifier.c:invalidate_rcu_protected_refs() {
bpf_for_each_reg_in_vstate_mask(env->cur_state, state, reg, stack, clear_mask, ({
if (reg->type & MEM_RCU) {
bpf_diag_mod_begin(env, reg, NULL, BPF_DIAG_MOD_WRITE);
reg->type &= ~(MEM_RCU | PTR_MAYBE_NULL | NON_OWN_REF);
reg->type |= PTR_UNTRUSTED;
bpf_diag_mod_end(env);
}
}));
}
Because plain PTR_TO_BTF_ID registers survive bpf_rcu_read_unlock() and
are accepted by btf_ptr_types and btf_id_sock_common_types, doesn't this
allow a sleepable BPF program to walk a field like exe_file->f_path.dentry
->d_inode or vma->vm_file under bpf_rcu_read_lock() and pass it to helpers
such as bpf_ima_inode_hash(), bpf_ima_file_hash(), bpf_inode_storage_get(),
or bpf_sk_storage_get() after bpf_rcu_read_unlock() without RCU protection
or a NULL check?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261023212041.884996-1-utilityemal77@gmail.com?part=1
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-10-23 21:58 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-23 21:20 [PATCH bpf-next v3 0/5] bpf: bound trusted fields and derived returns to their parent's lifetime Justin Suess
2026-10-23 21:20 ` [PATCH bpf-next v3 1/5] bpf: Don't apply the trusted field lists to an RCU-protected parent Justin Suess
2026-10-23 21:58 ` sashiko-bot
2026-10-23 21:20 ` [PATCH bpf-next v3 2/5] selftests/bpf: Test f_inode of an RCU-protected file Justin Suess
2026-10-23 21:20 ` [PATCH bpf-next v3 3/5] bpf: Add a __derives_ret kfunc argument annotation Justin Suess
2026-10-23 21:52 ` sashiko-bot
2026-10-23 21:20 ` [PATCH bpf-next v3 4/5] bpf: Bind the inode bpf_real_data_inode() returns to the file Justin Suess
2026-10-23 21:20 ` [PATCH bpf-next v3 5/5] selftests/bpf: Test __derives_ret return invalidation on argument release Justin Suess
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.