[RFC PATCH 05/45] mm: vmstat: restore per-migratetype free counts in /proc/pagetypeinfo

Rik van Riel
2026-04-30T20:22:49Z

The SPB rework moved free pages off zone->free_area[] and onto the
per-superpageblock free lists at zone->superpageblocks[i].free_area[].
pagetypeinfo_showfree_print() was still walking the now-empty zone-level
free lists, so /proc/pagetypeinfo's "Free pages count per migrate type
at order" table read as all zeros.

Walk every SPB in the zone, accumulating counts per (migratetype, order)
into stack-allocated 2-D arrays, then emit one line per migratetype.
zone->lock is dropped between SPBs (matching the original printer's
unlock/cond_resched/lock pattern) to bound time under the lock. The
100000-per-cell cap is retained -- it is now cumulative across all SPBs
in the zone, which is the same effective semantic as before since the
old free_area was already per-zone.

Concurrent memory hotplug can swap zone->superpageblocks under us during
a lock drop; the counts may then be inconsistent, but no UAF is possible
since sb is re-derefed each iteration. Acceptable for a debug-only
interface.

Signed-off-by: Rik van Riel <riel@surriel.com>
Assisted-by: Claude:claude-opus-4.7 syzkaller
---
mm/vmstat.c | 66 ++++++++++++++++++++++++++++++-----------------------
1 file changed, 38 insertions(+), 28 deletions(-)

diff --git a/mm/vmstat.c b/mm/vmstat.c
index 86b14b0f77b5..7de08ab61b9d 100644
--- a/mm/vmstat.c
+++ b/mm/vmstat.c
@@ -1572,41 +1572,51 @@ static int frag_show(struct seq_file *m, void *arg)
static void pagetypeinfo_showfree_print(struct seq_file *m,
pg_data_t *pgdat, struct zone *zone)
{
+ unsigned long counts[MIGRATE_TYPES][NR_PAGE_ORDERS] = { };
+ bool overflow[MIGRATE_TYPES][NR_PAGE_ORDERS] = { };
+ unsigned long sb_idx, nr_sbs = zone->nr_superpageblocks;
int order, mtype;

+ /*
+ * Free pages live on per-superpageblock free lists. Walk the SPBs,
+ * accumulating per (migratetype, order) counts. The 100000 cap per
+ * cell limits time under zone->lock; this is a debugging interface,
+ * knowing there is "a lot" of one size is sufficient. zone->lock is
+ * dropped between SPBs, so concurrent memory hotplug may produce
+ * inconsistent counts -- acceptable for a debug-only interface.
+ */
+ for (sb_idx = 0; sb_idx < nr_sbs; sb_idx++) {
+ struct superpageblock *sb = &zone->superpageblocks[sb_idx];
+
+ for (order = 0; order < NR_PAGE_ORDERS; order++) {
+ struct free_area *area = &sb->free_area[order];
+ struct list_head *curr;
+
+ for (mtype = 0; mtype < MIGRATE_TYPES; mtype++) {
+ if (overflow[mtype][order])
+ continue;
+ list_for_each(curr, &area->free_list[mtype]) {
+ if (++counts[mtype][order] >= 100000) {
+ overflow[mtype][order] = true;
+ break;
+ }
+ }
+ }
+ }
+ spin_unlock_irq(&zone->lock);
+ cond_resched();
+ spin_lock_irq(&zone->lock);
+ }
+
for (mtype = 0; mtype < MIGRATE_TYPES; mtype++) {
seq_printf(m, "Node %4d, zone %8s, type %12s ",
pgdat->node_id,
zone->name,
migratetype_names[mtype]);
- for (order = 0; order < NR_PAGE_ORDERS; ++order) {
- unsigned long freecount = 0;
- struct free_area *area;
- struct list_head *curr;
- bool overflow = false;
-
- area = &(zone->free_area[order]);
-
- list_for_each(curr, &area->free_list[mtype]) {
- /*
- * Cap the free_list iteration because it might
- * be really large and we are under a spinlock
- * so a long time spent here could trigger a
- * hard lockup detector. Anyway this is a
- * debugging tool so knowing there is a handful
- * of pages of this order should be more than
- * sufficient.
- */
- if (++freecount >= 100000) {
- overflow = true;
- break;
- }
- }
- seq_printf(m, "%s%6lu ", overflow ? ">" : "", freecount);
- spin_unlock_irq(&zone->lock);
- cond_resched();
- spin_lock_irq(&zone->lock);
- }
+ for (order = 0; order < NR_PAGE_ORDERS; order++)
+ seq_printf(m, "%s%6lu ",
+ overflow[mtype][order] ? ">" : "",
+ counts[mtype][order]);
seq_putc(m, '\n');
}
}
--
2.52.0