Re: [PATCH] mm/memcg: fix UAF in drain_all_stock() async work during offline

From: Johannes Weiner

Date: Thu Aug 27 2026 - 13:15:17 EST


On Thu, Aug 27, 2026 at 12:42:11PM -0400, Rik van Riel wrote:
> drain_all_stock() queues drain work on remote CPUs via
> schedule_drain_work() -> queue_work_on(memcg_wq) and returns
> immediately without waiting. The worker, drain_local_memcg_stock()
> / drain_local_obj_stock(), dereferences per-CPU stock caches with
> READ_ONCE(stock->cached[i]) and does css_put() / obj_cgroup_put().
>
> mem_cgroup_css_offline() calls drain_all_stock(memcg) to
> optimize reclamation latency, but never flushes memcg_wq. If
> that races with cgroup removal, free can happen while workers
> are still pending, causing UAF. The drain work could also have
> been queued by somebody else before offline started (e.g. high
> throttling), not just by the offline path itself.
>
> Timeline illustrating the race:
>
> CPU0 (rmdir + offline) CPU1 (charge cache holder)
> ------------------------- ----------------------------
> cgroup_rmdir()
> cgroup_destroy_locked()
> kill_css_sync()
> ...
>
> refill_stock(victim)
> css_get(victim)
> WRITE_ONCE(cached[i]=victim)

I'm really confused. CPU1 acquires a ref for the cached[i] pointer ^

> percpu_ref kill confirmed, css_killed_ref_fn() called
>
> css_killed_work_fn() [offline_wq]
> mem_cgroup_css_offline(victim)
> drain_all_stock(victim)
> is_memcg_drain_needed()
> READ_ONCE(cached) -> victim
> queue_work_on(CPU1, memcg_wq, work)
> // no flush!
> mem_cgroup_private_id_put()
> css_put() -> refcnt may hit 0

So how can it hit 0 here?

> [RCU GP]
> css_free_rwork_fn()
> mem_cgroup_free(victim)
> // victim struct freed

This won't run until we hit zero...

>
> // worker delayed by scheduler/
> // WQ concurrency
> drain_local_memcg_stock()
> old = READ_ONCE(cached[i])
> // UAF: old == freed victim
> memcg_uncharge(old)
> css_put(&old->css)

...which is here.