[PATCH] mm/memcg: fix NULL nodeinfo[] dereference on late-onlined nodes

From: Prakash Gupta

Date: Mon Aug 10 2026 - 01:46:48 EST


memcg->nodeinfo[] entries are allocated only for nodes present at
css_alloc time. When a node is onlined after a memcg is created its
nodeinfo[] slot remains NULL. Two call sites dereference these slots
unconditionally:

lruvec_stat_mod_folio() calls mem_cgroup_lruvec() which reads
memcg->nodeinfo[pgdat->node_id] without a NULL check. On a system
where a node is onlined after the memcg is created, any folio stat
update for that node crashes with a NULL pointer dereference:

Unable to handle kernel paging request at virtual address ffffffbebf7e1908
pc : lruvec_stat_mod_folio+0xf0/0x444 [6.18.21-android17-5]
lr : lruvec_stat_mod_folio+0x88/0x444
Call trace:
lruvec_stat_mod_folio+0xf0/0x444
folio_add_new_anon_rmap+0xac/0x2b8
do_wp_page+0x768/0xc80
handle_mm_fault+0x37c/0x8c4
do_page_fault+0x140/0xa1c
do_mem_abort+0x54/0x74
el0_da+0x48/0x8c
el0t_64_sync_handler+0x20/0x130
el0t_64_sync+0x1c4/0x1c8

__invalidate_reclaim_iterators() iterates for_each_node() and reads
from->nodeinfo[nid]->iter without checking for NULL. for_each_node()
visits all possible nodes, so this is reachable whenever a node is
onlined after the memcg was created.

Fix lruvec_stat_mod_folio() by checking nodeinfo[pgdat->node_id]
directly and falling back to mod_node_page_state() when NULL, mirroring
the existing !memcg early-return path. The fallback must not go through
mod_lruvec_state() since that calls mod_memcg_lruvec_state() which uses
container_of() to recover the mem_cgroup_per_node from the lruvec
pointer; passing &pgdat->__lruvec there produces a garbage pointer.
When nodeinfo[nid] is NULL the memcg has no per-node accounting
structure for that node, so node-level accounting is correct.

Fix __invalidate_reclaim_iterators() by skipping NULL nodeinfo[] slots.

Also switch lruvec_stat_mod_folio() to folio_memcg_check() which uses
READ_ONCE() to safely read folio->memcg_data in an unlocked context.

Fixes: 6c77b607ee26 ("mm: kill lock|unlock_page_memcg()")
Cc: stable@xxxxxxxxxxxxxxx
Assisted-by: pi:claude-sonnet-4-5
Signed-off-by: Prakash Gupta <prakash.gupta@xxxxxxxxxxxxxxxx>
---
mm/memcontrol.c | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)

diff --git a/mm/memcontrol.c b/mm/memcontrol.c
index 6dc4888a90f3..2a6f02956b89 100644
--- a/mm/memcontrol.c
+++ b/mm/memcontrol.c
@@ -977,11 +977,12 @@ void lruvec_stat_mod_folio(struct folio *folio, enum node_stat_item idx,
int val)
{
struct mem_cgroup *memcg;
+ struct mem_cgroup_per_node *mz;
pg_data_t *pgdat = folio_pgdat(folio);
struct lruvec *lruvec;

rcu_read_lock();
- memcg = folio_memcg(folio);
+ memcg = folio_memcg_check(folio);
/* Untracked pages have no memcg, no lruvec. Update only the node */
if (!memcg) {
rcu_read_unlock();
@@ -989,7 +990,18 @@ void lruvec_stat_mod_folio(struct folio *folio, enum node_stat_item idx,
return;
}

- lruvec = mem_cgroup_lruvec(memcg, pgdat);
+ mz = memcg->nodeinfo[pgdat->node_id];
+ if (unlikely(!mz)) {
+ /*
+ * nodeinfo[nid] is NULL when a node is onlined after the
+ * memcg was created. Fall back to node-level accounting.
+ */
+ rcu_read_unlock();
+ mod_node_page_state(pgdat, idx, val);
+ return;
+ }
+
+ lruvec = &mz->lruvec;
mod_lruvec_state(lruvec, idx, val);
rcu_read_unlock();
}
@@ -1320,6 +1332,8 @@ static void __invalidate_reclaim_iterators(struct mem_cgroup *from,

for_each_node(nid) {
mz = from->nodeinfo[nid];
+ if (!mz)
+ continue;
iter = &mz->iter;
cmpxchg(&iter->position, dead_memcg, NULL);
}

---
base-commit: 075b74841bd0065a3bda3440873c747938e69b68
change-id: 20260808-memcg-nodeinfo-null-guard-dce5f63a2349

Best regards,
--
Prakash Gupta <prakash.gupta@xxxxxxxxxxxxxxxx>