BPF List
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Justin Suess" <utilityemal77@gmail.com>
Cc: bpf@vger.kernel.org
Subject: Re: [PATCH bpf-next v3 1/5] bpf: Don't apply the trusted field lists to an RCU-protected parent
Date: Fri, 23 Oct 2026 21:58:13 +0000	[thread overview]
Message-ID: <sashiko-outbox-183266@kernel.org> (raw)
In-Reply-To: <20261023212041.884996-2-utilityemal77@gmail.com>

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

  reply	other threads:[~2026-10-23 21:58 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 [this message]
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

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=sashiko-outbox-183266@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=utilityemal77@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