mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Pu Lehui <pulehui@huawei.com>
To: <bot+bpf-ci@kernel.org>, <pulehui@huaweicloud.com>,
	<bpf@vger.kernel.org>, <linux-kernel@vger.kernel.org>
Cc: <ast@kernel.org>, <daniel@iogearbox.net>, <andrii@kernel.org>,
	<eddyz87@gmail.com>, <memxor@gmail.com>, <martin.lau@linux.dev>,
	<yonghong.song@linux.dev>, <song@kernel.org>, <jolsa@kernel.org>,
	<emil@etsalapatis.com>, <ihor.solodrai@linux.dev>,
	<martin.lau@kernel.org>, <clm@meta.com>
Subject: Re: [PATCH bpf] bpf: Fix UAF due to concurrent consumption of waiting_for_gp_ttrace
Date: Thu, 27 Aug 2026 15:43:11 +0800	[thread overview]
Message-ID: <7466dc22-6b03-4584-8289-02e7186129bb@huawei.com> (raw)
In-Reply-To: <df5a55306918f6a83693a91db252f1e653602f35144e6ad189d502acb8b19ed0@mail.kernel.org>


Thanks very much!

The review comments on bpf ci look excellent, and I will extract the 
valuable information from them.

On 2026/8/26 19:16, bot+bpf-ci@kernel.org wrote:
>>      alloc_bulk() runs in an IRQ context (serving as an implicit normal RCU
>>      read-side critical section), an RCU Tasks Trace GP cannot complete while
>>      alloc_bulk() is accessing the list.
> 
> Is this accurate on CONFIG_PREEMPT_RT? Looking at init_refill_work() in
> kernel/bpf/memalloc.c, it uses init_irq_work() without IRQ_WORK_INIT_HARD,
> so on RT the irq_work goes to lazy_list and is executed by the preemptible
> per-CPU 'irq_work/N' kthread, not in hardirq context. The allocator's own
> comment says "In RT irq_work runs in per-cpu kthread."
> 
> The conclusion still holds because run_irq_workd() wraps
> irq_work_run_list(&lazy_list) in guard(rcu)(), making it an explicit RCU
> read-side critical section on RT. So alloc_bulk() is protected by RCU, but
> the stated reason ('IRQ context') is only true on !RT.
> 
> Could the commit message clarify that the RCU-reader property comes from
> hardirq on !RT and explicit rcu_read_lock() in run_irq_workd() on RT?
> 
>>      Thus, the callback __free_rcu cannot run concurrently, ruling out
>>      missing RCU read-side locks as the cause.
> 
> Can __free_rcu really not run concurrently with alloc_bulk()? The race
> diagram two paragraphs later shows exactly that happening:
> 
> CPU0                                           CPU1
>                                                 __free_rcu (RCU Tasks Trace callback)
> alloc_bulk (irq context)
>    llist_del_first(&c->waiting_for_gp_ttrace)
>      entry = smp_load_acquire(&head->first);
>                                                 free_all(llist_del_all(&c->waiting_for_gp_ttrace))
>      next = READ_ONCE(entry->next); <-- trigger UAF
> 
> Looking at the guarantees from 57b23c0f612d, the RCU Tasks Trace GP
> contains at least one synchronize_rcu(), which waits for alloc_bulk()
> invocations that started before that internal synchronize_rcu(). But a
> fresh alloc_bulk() entered after the GP expired is not waited for, and
> __free_rcu can run while that fresh invocation is in flight.
> 
> So what's actually true is narrower: a ttrace GP cannot expire while an
> alloc_bulk() read section that began before the GP's internal
> synchronize_rcu() is still in flight. A fresh alloc_bulk() after the GP
> expired can race with __free_rcu().
> 
> Could the explanation be more precise about which alloc_bulk() invocations
> are protected and which can race?
> 
>> diff --git a/kernel/bpf/memalloc.c b/kernel/bpf/memalloc.c
>> index e9662db7198f..fb1e733bfb82 100644
>> --- a/kernel/bpf/memalloc.c
>> +++ b/kernel/bpf/memalloc.c
>> @@ -110,7 +110,9 @@ struct bpf_mem_cache {
>>   	struct llist_node *free_by_rcu_tail;
>>   	struct llist_head waiting_for_gp;
>>   	struct llist_node *waiting_for_gp_tail;
>> +	struct llist_node *waiting_for_reclaim_gp;
>>   	struct rcu_head rcu;
>> +	struct rcu_head rcu_reclaim;
>>   	atomic_t call_rcu_in_progress;
>>   	struct llist_head free_llist_extra_rcu;
> 
> [ ... ]
> 
>> @@ -276,12 +278,28 @@ static int free_all(struct bpf_mem_cache *c, struct llist_node *llnode, bool per
>>   	return cnt;
>>   }
>>
>> +static void __free_final_rcu(struct rcu_head *head)
>> +{
>> +	struct bpf_mem_cache *c = container_of(head, struct bpf_mem_cache, rcu);
>> +	struct llist_node *llnode = c->waiting_for_reclaim_gp;
>> +
>> +	c->waiting_for_reclaim_gp = NULL;
>> +	free_all(c, llnode, !!c->percpu_size);
>> +	atomic_set(&c->call_rcu_ttrace_in_progress, 0);
>> +}
>> +
>>   static void __free_rcu(struct rcu_head *head)
>>   {
>>   	struct bpf_mem_cache *c = container_of(head, struct bpf_mem_cache, rcu_ttrace);
>> +	struct llist_node *llnode = llist_del_all(&c->waiting_for_gp_ttrace);
>>
>> -	free_all(c, llist_del_all(&c->waiting_for_gp_ttrace), !!c->percpu_size);
>> -	atomic_set(&c->call_rcu_ttrace_in_progress, 0);
>> +	if (!llnode) {
>> +		atomic_set(&c->call_rcu_ttrace_in_progress, 0);
>> +		return;
>> +	}
>> +
>> +	c->waiting_for_reclaim_gp = llnode;
>> +	call_rcu(&c->rcu_reclaim, __free_final_rcu);
>                            ^^^^^^^^^^^^^^^^^^^^^^^^^
> 
> Does free_mem_alloc() wait for this new callback? Looking at the teardown
> path:
> 
> kernel/bpf/memalloc.c:free_mem_alloc() {
>      /*
>       * Trace grace period implies RCU grace period, so all __free_rcu
>       * don't need extra call_rcu() (and thus extra rcu_barrier() here).
>       */
>      rcu_barrier();               /* wait for __free_by_rcu */
>      rcu_barrier_tasks_trace();   /* wait for __free_rcu */
>      free_mem_alloc_no_barrier(ma);
>          -> free_percpu(ma->cache) / free_percpu(ma->caches)
> }
> 
> The barriers run in that order: normal RCU first, then tasks-trace. If
> __free_rcu() executes during rcu_barrier_tasks_trace() and queues
> call_rcu(&c->rcu_reclaim, __free_final_rcu), there's no rcu_barrier()
> left to wait for it. So free_percpu() runs while __free_final_rcu() is
> still queued on the now-freed per-cpu memory.
> 
> Concrete sequence:
> 1. do_call_rcu_ttrace() queues call_rcu_tasks_trace(&c->rcu_ttrace,
>     __free_rcu)
> 2. Map destroyed, destroy path computes rcu_in_progress >= 1 from
>     atomic_read(&c->call_rcu_ttrace_in_progress), takes deferred path
> 3. free_mem_alloc(): rcu_barrier() at 716 completes, rcu_barrier_tasks_trace()
>     at 717 waits for __free_rcu() which runs and queues call_rcu(&c->rcu_reclaim,
>     __free_final_rcu), barrier returns with normal-RCU callback newly queued
> 4. free_mem_alloc_no_barrier() frees the per-cpu memory holding c
> 5. When normal grace period ends, __free_final_rcu() runs on freed memory
> 
> Also, drain_mem_cache() drains seven lists but not waiting_for_reclaim_gp,
> and check_mem_cache() WARN_ON_ONCE()s on those seven but never inspects
> waiting_for_reclaim_gp, so the leak is silent.
> 
> Does this need an rcu_barrier() after rcu_barrier_tasks_trace() (or the
> two swapped plus an added one)? And should the comment at lines 706-715
> be updated since it now says the opposite of what the code does?
> 
> Also note check_mem_cache() should probably check waiting_for_reclaim_gp.
> 
> The same llist_del_first-vs-llist_del_all hazard that this patch fixes
> for waiting_for_gp_ttrace also exists on free_by_rcu_ttrace. Looking at
> kernel/bpf/memalloc.c:
> 
> alloc_bulk() does:
>      obj = llist_del_first(&c->free_by_rcu_ttrace);
> 
> while do_call_rcu_ttrace() does:
>      llnode = llist_del_all(&c->free_by_rcu_ttrace);
>      ...
>      free_all(c, llnode, percpu);  /* draining path, immediate free */
> 
> and drain_mem_cache() does:
>      free_all(llist_del_all(&c->free_by_rcu_ttrace), ...);
> 
> include/linux/llist.h's concurrency table marks the del_first x del_all
> cell 'L' (lock is needed) and the kerneldoc on llist_del_first says "If
> multiple consumers are needed, please use llist_del_all or use lock
> between consumers."
> 
> The non-draining path in do_call_rcu_ttrace() is incidentally covered by
> the new extra GP because nodes end up in waiting_for_gp_ttrace, but the
> draining direct-free and drain_mem_cache()'s free_all() still free without
> any RCU deferral.
> 
> Does free_by_rcu_ttrace need the same treatment?
> 
> While the nodes sit in c->waiting_for_reclaim_gp they are invisible to
> alloc_bulk()'s reuse loop, so for one extra normal RCU GP the allocator
> must kmalloc() instead of reusing. This partially undoes the optimization
> being fixed.
> 
> Is this expected? Should the commit message mention it?
> 
> The commit message says "Syzkaller repeatedly triggered" but carries no
> Reported-by:, Closes: or Link: tag, and the splat is from a private 6.6.0+
> tree rather than upstream syzbot.
> 
> Also, Hou Tao reported this exact hazard during review of 04fabf00b4d3 in
> July 2023 (lore.kernel.org/rcu/957dd5cd-0855-1197-7045-4cb1590bd753@huaweicloud.com).
> Alexei's reply was that the probability was "lower than a bit flip due to
> cosmic rays."
> 
> Could citing that thread make the analysis more credible and give proper
> credit to Hou Tao?
> 
> 
> ---
> AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
> See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
> 
> CI run summary: https://github.com/kernel-patches/bpf/actions/runs/32959494022

  reply	other threads:[~2026-08-27  7:43 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-26 10:36 Pu Lehui
2026-08-26 11:16 ` bot+bpf-ci
2026-08-27  7:43   ` Pu Lehui [this message]
2026-08-26 12:28 ` Hou Tao
2026-08-27  7:40   ` Pu Lehui
2026-08-26 16:13 ` [syzbot ci] " syzbot ci

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=7466dc22-6b03-4584-8289-02e7186129bb@huawei.com \
    --to=pulehui@huawei.com \
    --cc=andrii@kernel.org \
    --cc=ast@kernel.org \
    --cc=bot+bpf-ci@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=clm@meta.com \
    --cc=daniel@iogearbox.net \
    --cc=eddyz87@gmail.com \
    --cc=emil@etsalapatis.com \
    --cc=ihor.solodrai@linux.dev \
    --cc=jolsa@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=martin.lau@kernel.org \
    --cc=martin.lau@linux.dev \
    --cc=memxor@gmail.com \
    --cc=pulehui@huaweicloud.com \
    --cc=song@kernel.org \
    --cc=yonghong.song@linux.dev \
    /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

all inboxes | Powered by JetHome®