[PATCH v3 6/7] mm/page_owner: add bpf_page_owner_get_memcg_info() kfunc

From: Zhen Ni

Date: Wed Oct 21 2026 - 07:29:29 EST


Expose per-page memcg information to BPF programs, so that a bpf_iter
over page_owner can aggregate and filter allocations by memcg.
Attributing an allocation to a specific cgroup needs its full cgroup
path, not just the memcg name.

Split memcg info collection out of print_page_owner_memcg() into
get_page_memcg_info(), which fills struct memcg_info and, when given a
caller-provided buffer, writes the full cgroup path via cgroup_path().
print_page_owner_memcg() now consumes the struct; this introduces no
functional change to the debugfs output.

The helper takes a single READ_ONCE snapshot within one RCU read-side
critical section, so the flags and cgroup path reflect a consistent
view of the page. cgroup_path() returns the path relative to the cgroup
hierarchy root, so memcg attribution works on both cgroup v1 and v2.

Because scan->pfn is selected by a lockless scan, the kfunc re-resolves
it with pfn_to_online_page() under get_online_mems() to avoid accessing
a hot-removed section. If the PFN is offline or removed, the kfunc
returns -ENODATA instead of dereferencing memcg_data.

Signed-off-by: Zhen Ni <zhen.ni@xxxxxxxxxxxx>
---
Changes in v3:
- Check the page_owner_inited static key.
- Resolve the page with pfn_to_online_page() under get_online_mems().

Changes in v2:
- bpf_page_owner_get_memcg_info() now resolves the page with
pfn_to_online_page() for an offline or removed pfn.
- Buffer/info args renamed to info__uninit/buf__uninit/buf__sz.

v2: https://lore.kernel.org/linux-mm/20261013122125.843451-6-zhen.ni@xxxxxxxxxxxx/
v1: https://lore.kernel.org/linux-mm/20261009112308.240769-6-zhen.ni@xxxxxxxxxxxx/
---
kernel/bpf/helpers.c | 1 +
mm/page_owner.c | 85 ++++++++++++++++++++++++++++++++++----------
mm/page_owner.h | 9 +++++
mm/page_owner_iter.c | 48 +++++++++++++++++++++++++
4 files changed, 124 insertions(+), 19 deletions(-)

diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
index 0ba836f77957..bba9eeb076c3 100644
--- a/kernel/bpf/helpers.c
+++ b/kernel/bpf/helpers.c
@@ -4954,6 +4954,7 @@ BTF_ID_FLAGS(func, bpf_iter_page_owner_next,
BTF_ID_FLAGS(func, bpf_iter_page_owner_destroy,
KF_ITER_DESTROY | KF_SLEEPABLE | KF_PERFMON)
BTF_ID_FLAGS(func, bpf_page_owner_stack_snprint, KF_SLEEPABLE | KF_PERFMON)
+BTF_ID_FLAGS(func, bpf_page_owner_get_memcg_info, KF_SLEEPABLE | KF_PERFMON)
#endif
BTF_ID_FLAGS(func, __bpf_trap)
BTF_ID_FLAGS(func, bpf_strcmp, KF_PERFMON);
diff --git a/mm/page_owner.c b/mm/page_owner.c
index 8bc15bcdc24e..7470b5d002be 100644
--- a/mm/page_owner.c
+++ b/mm/page_owner.c
@@ -538,16 +538,22 @@ void pagetypeinfo_showmixedcount_print(struct seq_file *m,

#ifdef CONFIG_MEMCG
/*
- * Looking for memcg information and print it out
+ * Collect memcg information of @page into @info; when @buf and a
+ * nonzero @size are given, also copy the full cgroup path of the
+ * charged memcg into @buf.
+ *
+ * Return: 0 if @page is charged to an attributable memcg; -ENODATA
+ * otherwise.
*/
-static inline int print_page_owner_memcg(char *kbuf, size_t count, int ret,
- struct page *page)
+int get_page_memcg_info(struct page *page, struct memcg_info *info,
+ char *buf, int size)
{
unsigned long memcg_data;
struct obj_cgroup *objcg;
struct mem_cgroup *memcg;
- bool online;
- char name[80];
+ int ret = -ENODATA;
+
+ *info = (struct memcg_info){};

rcu_read_lock();
memcg_data = READ_ONCE(page->memcg_data);
@@ -555,31 +561,68 @@ static inline int print_page_owner_memcg(char *kbuf, size_t count, int ret,
goto out_unlock;

if (memcg_data & MEMCG_DATA_OBJEXTS) {
- ret += scnprintf(kbuf + ret, count - ret,
- "Slab cache page\n");
+ info->is_slab = true;
goto out_unlock;
}

objcg = (void *)(memcg_data & ~OBJEXTS_FLAGS_MASK);
- memcg = objcg ? obj_cgroup_memcg(objcg) : NULL;
+ if (!objcg)
+ goto out_unlock;
+
+ memcg = obj_cgroup_memcg(objcg);
if (!memcg)
goto out_unlock;

- online = css_is_online(&memcg->css);
- cgroup_name(memcg->css.cgroup, name, sizeof(name));
- ret += scnprintf(kbuf + ret, count - ret,
- "Charged %sto %smemcg %s\n",
- (memcg_data & MEMCG_DATA_KMEM) ? "(via objcg) " : "",
- online ? "" : "offline ",
- name);
+ info->is_kmem = (memcg_data & MEMCG_DATA_KMEM) != 0;
+ info->is_online = css_is_online(&memcg->css);
+ cgroup_name(memcg->css.cgroup, info->name, sizeof(info->name));
+
+ if (buf && size)
+ cgroup_path(memcg->css.cgroup, buf, size);
+ ret = 0;
out_unlock:
rcu_read_unlock();

+ if (ret < 0 && buf && size)
+ buf[0] = '\0';
+
+ return ret;
+}
+
+/*
+ * Print memcg information from memcg_info
+ */
+static inline int print_page_owner_memcg(char *kbuf, size_t count, int ret,
+ const struct memcg_info *info)
+{
+ if (!info)
+ return ret;
+
+ if (info->is_slab)
+ ret += scnprintf(kbuf + ret, count - ret,
+ "Slab cache page\n");
+
+ if (info->name[0])
+ ret += scnprintf(kbuf + ret, count - ret,
+ "Charged %sto %smemcg %s\n",
+ info->is_kmem ? "(via objcg) " : "",
+ info->is_online ? "" : "offline ",
+ info->name);
+
return ret;
}
#else
+int get_page_memcg_info(struct page *page, struct memcg_info *info,
+ char *buf, int size)
+{
+ *info = (struct memcg_info){};
+ if (buf && size)
+ buf[0] = '\0';
+ return -EOPNOTSUPP;
+}
+
static inline int print_page_owner_memcg(char *kbuf, size_t count, int ret,
- struct page *page)
+ const struct memcg_info *info)
{
return ret;
}
@@ -589,7 +632,8 @@ static ssize_t
print_page_owner(char __user *buf, size_t count, unsigned long pfn,
struct page *page, struct page_owner *page_owner,
depot_stack_handle_t handle,
- struct page_owner_filter_state *state)
+ struct page_owner_filter_state *state,
+ const struct memcg_info *memcg_info)
{
int ret, pageblock_mt, page_mt;
char *kbuf;
@@ -639,7 +683,7 @@ print_page_owner(char __user *buf, size_t count, unsigned long pfn,
migrate_reason_names[page_owner->last_migrate_reason]);
}

- ret = print_page_owner_memcg(kbuf, count, ret, page);
+ ret = print_page_owner_memcg(kbuf, count, ret, memcg_info);

ret += snprintf(kbuf + ret, count - ret, "\n");
if (ret >= count)
@@ -822,6 +866,7 @@ static ssize_t
read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
struct page_owner_scan scan;
+ struct memcg_info memcg_info;
struct page_owner_filter_state *state = file->private_data;

if (!static_branch_unlikely(&page_owner_inited))
@@ -855,8 +900,10 @@ read_page_owner(struct file *file, char __user *buf, size_t count, loff_t *ppos)
/* Record the next PFN to read in the file offset */
*ppos = scan.pfn + 1;

+ get_page_memcg_info(page, &memcg_info, NULL, 0);
ret = print_page_owner(buf, count, scan.pfn, page,
- &scan.po_snap, scan.po_snap.handle, state);
+ &scan.po_snap, scan.po_snap.handle, state,
+ &memcg_info);
put_online_mems();
return ret;

diff --git a/mm/page_owner.h b/mm/page_owner.h
index 934bc9f84d6f..65598415af73 100644
--- a/mm/page_owner.h
+++ b/mm/page_owner.h
@@ -41,7 +41,16 @@ struct page_owner_scan {
int nid;
};

+struct memcg_info {
+ char name[80];
+ bool is_slab;
+ bool is_kmem;
+ bool is_online;
+};
+
bool page_owner_next_eligible(struct page_owner_scan *scan);
+int get_page_memcg_info(struct page *page, struct memcg_info *info,
+ char *buf, int size);

/*
* mm/page_owner_iter.c
diff --git a/mm/page_owner_iter.c b/mm/page_owner_iter.c
index b7e0ce1dfa80..a649e1207e24 100644
--- a/mm/page_owner_iter.c
+++ b/mm/page_owner_iter.c
@@ -181,4 +181,52 @@ __bpf_kfunc int bpf_page_owner_stack_snprint(struct page_owner_scan *scan,
return len;
}

+/**
+ * bpf_page_owner_get_memcg_info - Report the memcg charged for the current page
+ * @scan: page_owner scan handle.
+ * @info__uninit: output buffer for the memcg_info.
+ * @buf__uninit: optional output buffer for the full cgroup path.
+ * @buf__sz: size of @buf__uninit.
+ *
+ * Fills @info__uninit with the memcg charged for the page backing
+ * @scan->pfn. If @buf__uninit and @buf__sz are provided, the full
+ * cgroup path is also copied out, so a BPF program can attribute a
+ * page_owner allocation to a specific cgroup.
+ *
+ * @scan->pfn is resolved to a struct page under get_online_mems(),
+ * so the page cannot be accessed after its memory section is offlined
+ * or removed.
+ *
+ * Returns:
+ * * 0 on success
+ * * -ENODATA if the page is not online or not charged to a memcg
+ * * -EOPNOTSUPP if built without CONFIG_MEMCG
+ */
+__bpf_kfunc int bpf_page_owner_get_memcg_info(struct page_owner_scan *scan,
+ struct memcg_info *info__uninit,
+ char *buf__uninit, u32 buf__sz)
+{
+ struct page *page;
+ int ret = -ENODATA;
+
+ if (!static_branch_unlikely(&page_owner_inited))
+ goto nodata;
+
+ get_online_mems();
+ page = pfn_to_online_page(scan->pfn);
+ if (!page) {
+ put_online_mems();
+ goto nodata;
+ }
+ ret = get_page_memcg_info(page, info__uninit, buf__uninit, buf__sz);
+ put_online_mems();
+ return ret;
+
+nodata:
+ *info__uninit = (struct memcg_info){};
+ if (buf__uninit && buf__sz)
+ buf__uninit[0] = '\0';
+ return ret;
+}
+
__bpf_kfunc_end_defs();
--
2.20.1