Re: [PATCH] memcg: keep folio's objcg same as its node
From: Johannes Weiner
Date: Thu Aug 06 2026 - 11:52:43 EST
On Wed, Aug 05, 2026 at 11:18:30PM -0700, Shakeel Butt wrote:
> memcg_reparent_objcgs() has an inherent assumption that a folio's objcg
> is the objcg of the folio's node. Folio migration across nodes breaks
> that assumption: the new folio simply inherits the old folio's objcg
> while living on a different node.
>
> Once the assumption is broken, the reparenting of the folio's objcg and
> the reparenting of the folio's LRU list are no longer atomic.
> memcg_reparent_objcgs() handles one node per iteration and drops all the
> locks in between, so the objcg gets reparented in the iteration for the
> objcg's node while the LRU list gets spliced in the iteration for the
> folio's node. Any LRU operation on that folio in between resolves its
> lruvec through the objcg, and thus takes the lru_lock of the wrong
> memcg, not the lru_lock of the list the folio is actually on.
>
> Fix this by selecting the objcg by folio_nid() at charge time, and by
> re-deriving it for the destination node in mem_cgroup_migrate() and
> mem_cgroup_replace_folio().
Nice sleuthing.
> Reported-by: Karl Erik Hofseth <karl.e.hofseth@xxxxxxxxxx>
> Closes: https://lore.kernel.org/all/anMmd1ADrDVwMO6v@work/
> Fixes: f1cf8d2f36dc ("mm: memcontrol: eliminate the problem of dying memory cgroup for LRU folios")
> Cc: stable@xxxxxxxxxxxxxxx
> Signed-off-by: Shakeel Butt <shakeel.butt@xxxxxxxxx>
> ---
> mm/memcontrol.c | 33 +++++++++++++++++++++++++--------
> 1 file changed, 25 insertions(+), 8 deletions(-)
>
> diff --git a/mm/memcontrol.c b/mm/memcontrol.c
> index 3057396dda53..2e98788dc8bd 100644
> --- a/mm/memcontrol.c
> +++ b/mm/memcontrol.c
> @@ -2966,10 +2966,9 @@ struct mem_cgroup *mem_cgroup_from_virt(void *p)
> return folio_memcg_check(virt_to_folio(p));
> }
>
> -static struct obj_cgroup *__get_obj_cgroup_from_memcg(struct mem_cgroup *memcg)
> +static struct obj_cgroup *__get_obj_cgroup_from_memcg(struct mem_cgroup *memcg,
> + int nid)
> {
> - int nid = numa_node_id();
> -
> for (; memcg; memcg = parent_mem_cgroup(memcg)) {
> struct obj_cgroup *objcg = rcu_dereference(memcg->nodeinfo[nid]->objcg);
>
> @@ -2980,12 +2979,13 @@ static struct obj_cgroup *__get_obj_cgroup_from_memcg(struct mem_cgroup *memcg)
> return NULL;
> }
>
> -static inline struct obj_cgroup *get_obj_cgroup_from_memcg(struct mem_cgroup *memcg)
> +static inline struct obj_cgroup *get_obj_cgroup_from_memcg(struct mem_cgroup *memcg,
> + int nid)
> {
> struct obj_cgroup *objcg;
>
> rcu_read_lock();
> - objcg = __get_obj_cgroup_from_memcg(memcg);
> + objcg = __get_obj_cgroup_from_memcg(memcg, nid);
> rcu_read_unlock();
>
> return objcg;
> @@ -3029,7 +3029,7 @@ static struct obj_cgroup *current_objcg_update(void)
>
> rcu_read_lock();
> memcg = mem_cgroup_from_task(current);
> - objcg = __get_obj_cgroup_from_memcg(memcg);
> + objcg = __get_obj_cgroup_from_memcg(memcg, numa_node_id());
> rcu_read_unlock();
>
> /*
> @@ -5197,7 +5197,7 @@ static int charge_memcg(struct folio *folio, struct mem_cgroup *memcg,
> int ret = 0;
> struct obj_cgroup *objcg;
>
> - objcg = get_obj_cgroup_from_memcg(memcg);
> + objcg = get_obj_cgroup_from_memcg(memcg, folio_nid(folio));
> /* Do not account at the root objcg level. */
> if (!obj_cgroup_is_root(objcg))
> ret = try_charge_memcg(memcg, gfp, folio_nr_pages(folio));
> @@ -5431,6 +5431,7 @@ void mem_cgroup_replace_folio(struct folio *old, struct folio *new)
>
> rcu_read_lock();
> memcg = obj_cgroup_memcg(objcg);
> +
> /* Force-charge the new page. The old one will be freed soon */
> if (!obj_cgroup_is_root(objcg)) {
> page_counter_charge(&memcg->memory, nr_pages);
> @@ -5438,7 +5439,12 @@ void mem_cgroup_replace_folio(struct folio *old, struct folio *new)
> page_counter_charge(&memcg->memsw, nr_pages);
> }
>
> - obj_cgroup_get(objcg);
> + /* If replacing folio of different node, get objcg of that node. */
> + if (folio_nid(old) != folio_nid(new))
> + objcg = __get_obj_cgroup_from_memcg(memcg, folio_nid(new));
> + else
> + obj_cgroup_get(objcg);
> +
> commit_charge(new, objcg);
> memcg1_commit_charge(new, memcg);
> rcu_read_unlock();
> @@ -5478,6 +5484,17 @@ void mem_cgroup_migrate(struct folio *old, struct folio *new)
> if (!objcg)
> return;
>
> + /* If migrating to different node, get objcg of that node. */
> + if (folio_nid(old) != folio_nid(new)) {
> + struct obj_cgroup *old_objcg = objcg;
> +
> + rcu_read_lock();
> + objcg = __get_obj_cgroup_from_memcg(obj_cgroup_memcg(old_objcg),
> + folio_nid(new));
> + rcu_read_unlock();
> + obj_cgroup_put(old_objcg);
> + }
> +
> /* Transfer the charge and the objcg ref */
> commit_charge(new, objcg);
Just noticed Sashiko pointed this out too, but you put old_objcg while
old->memcg_data still references it. I don't see how that could lead
to a UAF but it's fragile.
Man, this is hairy. I missed the implications when I suggested to make
the objcg per node for simplifcation elsewhere. Now locality actually
matters -_-
A comment describing the race with reparenting would be very helpful.