Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Ye Liu" <ye.liu@linux.dev>, "Andrew Morton" <akpm@linux-foundation.org>
Cc: "SeongJae Park" <sj@kernel.org>,
	"Stephen Brennan" <stephen.s.brennan@oracle.com>,
	linux-mm@kvack.org, "Ye Liu" <liuye@kylinos.cn>
Subject: Re: [PATCH] tools/mm: rewrite show_page_info.py with modular design and new features
Date: Tue, 20 Oct 2026 03:11:53 +0000	[thread overview]
Message-ID: <sashiko-outbox-178431@kernel.org> (raw)
In-Reply-To: <20261019-show_page_info_refactor-v1-1-42518e2d7de7@linux.dev>

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Medium] tools/mm: broken page_type bitfield decoding in _page_type()
- [Medium] tools/mm: incorrect rmap resolution in get_rmap_info() and _rmap_fields()
- [Medium] tools/mm: broken swap cache detection in _page_fields() and _swapcache_info()
- [Medium] tools/mm: unmasked upper PTE bits and non-present PTE decoding in _walk_page_table()
- [Low] tools/mm: wrong slab stride and tail page access in _scan_page_types()
--

commit 16444b1496af4eed07be39cdba3cef7d0140f1da
Author: Ye Liu <liuye@kylinos.cn>

tools/mm: rewrite show_page_info.py with modular design and new features

This commit rewrites tools/mm/show_page_info.py into modular field-collector
functions, adds multiple input modes (--pfn, --page, --user, --selftest), and
introduces rmap, PTE walk, NUMA node/zone, page type, and memcg inspection
features.

> diff --git a/tools/mm/show_page_info.py b/tools/mm/show_page_info.py
> index c46d8ea283d7d..eea1f8b8fa9fc 100644
> --- a/tools/mm/show_page_info.py
> +++ b/tools/mm/show_page_info.py

[ ... ]

> +def _vma_owner(vma, folio_index, page_size):
> +    """Extract (pid, comm, vm_start, vm_end, mm_ptr, vaddr) from a VMA."""
> +    mm_ptr = vma.vm_mm
> +    owner = mm_ptr.read_().owner.read_()
> +    vaddr = vma.vm_start.value_() + \
> +        (folio_index - vma.vm_pgoff.value_()) * page_size
> +    return (owner.pid.value_(), owner.comm.string_().decode(),
> +            vma.vm_start.value_(), vma.vm_end.value_(), mm_ptr, vaddr)
> +
> +
> +def get_rmap_info(page):
> +    """Find processes that map this page via reverse mapping."""
> +    results = []
> +    head = compound_head(page) if PageCompound(page) else page
> +    mapping = head.mapping.value_()
> +    if mapping == 0:
> +        return results
> +
> +    page_size = prog["PAGE_SIZE"].value_()
> +    try:
> +        folio_index = head.__folio_index.value_()
> +    except (AttributeError, FaultError):
> +        folio_index = 0
> +
> +    try:
> +        if mapping & 0x1:
> +            # Anonymous page: walk anon_vma interval tree.
> +            # Note: this returns all VMAs sharing the anon_vma, which may
> +            # include VMAs that don't actually map this page. The PTE walk
> +            # distinguishes actual mappings (shows "not present" otherwise).
> +            anon_vma = Object(prog, "struct anon_vma *", mapping & ~3)
> +            for avc in rbtree_inorder_for_each_entry(
> +                "struct anon_vma_chain", anon_vma.rb_root.rb_root, "rb"):
> +                try:
> +                    results.append(
> +                        _vma_owner(avc.vma.read_(), folio_index, page_size))
> +                except FaultError:
> +                    continue
> +        else:
> +            addr_space = Object(prog, "struct address_space *", mapping & ~3)
> +            for vma in rbtree_inorder_for_each_entry(
> +                "struct vm_area_struct", addr_space.i_mmap.rb_root,
> +                "shared.rb"):
> +                try:
> +                    vma_pgoff = vma.vm_pgoff.value_()
> +                    vma_pages = (vma.vm_end.value_() -
> +                                 vma.vm_start.value_()) >> 12
> +                    if vma_pgoff <= folio_index < vma_pgoff + vma_pages:
> +                        results.append(
> +                            _vma_owner(vma, folio_index, page_size))
> +                except FaultError:
> +                    continue
> +    except (FaultError, AttributeError):
> +        pass
> +
> +    return results

[Severity: Medium]
Are there a few issues in how get_rmap_info(), _vma_owner(), and
_rmap_fields() resolve reverse mappings?

First, for KSM pages, FOLIO_MAPPING_KSM (0x3) sets both FOLIO_MAPPING_ANON
(0x1) and FOLIO_MAPPING_ANON_KSM (0x2) in head.mapping, where mapping & ~3
points to a struct ksm_stable_node rather than a struct anon_vma. Checking
only mapping & 0x1 causes get_rmap_info() to cast a ksm_stable_node pointer
to struct anon_vma * and walk it as an anon_vma rbtree.

Second, in _vma_owner(), vaddr is calculated using vma.vm_pgoff:

    vaddr = vma.vm_start.value_() + \
        (folio_index - vma.vm_pgoff.value_()) * page_size

For anonymous pages, folio->index (__folio_index) is relative to
vma_start_anon_pgoff(vma) ((vma->__vm_anon_pgoff_hi << 32) |
vma->__vm_anon_pgoff_lo), not vma->vm_pgoff. For COW anonymous pages in
MAP_PRIVATE file-backed VMAs (such as writable data/bss segments of ELF
binaries), vma->vm_pgoff is the file offset while folio_index is vaddr >>
PAGE_SHIFT, producing a bogus vaddr outside the VMA. In addition, the
anonymous branch in get_rmap_info() does not check whether folio_index falls
within the VMA's anonymous pgoff range, so for split or adjacent VMAs sharing
an anon_vma, _vma_owner() computes a vaddr outside avc.vma, and vma_find(mm,
vaddr) in _rmap_fields() resolves a different VMA instead of avc.vma.

Third, show_page_state() passes head instead of page to _rmap_fields(head,
fields, ...), and get_rmap_info() uses head.__folio_index without adding the
tail-page offset (page_to_pfn(page) - page_to_pfn(head)). When inspecting a
tail page of a compound page, _rmap_fields() computes vaddr and walks the
page tables for the head page instead of the target subpage, and misses VMAs
that only map a subrange of a large folio starting after head.__folio_index.
Also, the file-backed branch hardcodes >> 12 when computing vma_pages instead
of dividing by page_size.

[ ... ]

> +def _page_type(page):
> +    """Decode page->page_type field via enum pagetype discovery."""
> +    try:
> +        pt = page.page_type.value_()
> +    except AttributeError:
> +        return None
> +    try:
> +        enum_type = prog.constant("PGTY_buddy").type_
> +        for name, value in enum_type.enumerators:
> +            if value == pt:
> +                return name[5:]  # strip PGTY_ prefix
> +    except (LookupError, AttributeError):
> +        pass
> +    return None

[Severity: Medium]
Does _page_type() ever match real typed pages, or does it misclassify normal
mapped pages instead?

In include/linux/page-flags.h, enum pagetype defines 8-bit constants
PGTY_buddy (0xf0) through PGTY_mapcount_underflow (0xff), and FOLIO_TYPE_OPS
stores the type in bits 24..31 of the 32-bit page->page_type field:

    enum pagetype {
        /* 0x00-0x7f are positive numbers, ie mapcount */
        /* Reserve 0x80-0xef for mapcount overflow. */
        PGTY_buddy              = 0xf0,
        ...
        PGTY_mapcount_underflow = 0xff
    };

    static inline bool page_type_has_type(int page_type)
    {
        return page_type < (PGTY_mapcount_underflow << 24);
    }

Because _page_type() compares the unshifted 32-bit pt directly against value
(0xf0..0xff), real typed pages (whose page_type is 0xf0000000..0xf8ffffff,
such as 0xf5000000 for PGTY_slab in the commit message's Tests 5-7) never
match. Conversely, because page->page_type shares a union with atomic_t
_mapcount, any normal user-mapped page whose _mapcount happens to be between
0xf0 and 0xff (240..255) will match value == pt and be falsely reported as a
typed page.

Also, even if pt is shifted right by 24 bits, PGTY_mapcount_underflow (0xff)
is an enumerator in enum pagetype and will match normal unmapped pages where
page_type is 0xffffffff (_mapcount == -1) unless excluded, and _base_fields()
passes page rather than head to _page_type(), whereas tail pages of compound
typed folios have page_type == UINT_MAX.

[ ... ]

> +def _walk_page_table(mm, vaddr):
> +    """Walk 4-level page table. Returns list of (level, entry_pa, entry_val).
> +
> +    Note: only supports x86_64 4-level paging (48-bit VA). Does not support
> +    5-level paging (LA57, 57-bit VA) where an additional P4D level exists.
> +    """
> +    page_mask = ~(prog["PAGE_SIZE"].value_() - 1)
> +    entries = []
> +    try:
> +        def _read(table_va, idx):
> +            return prog.read_word(table_va + idx * 8)
> +
> +        base_va = mm.pgd.value_()
> +
> +        # PGD
> +        pgd_idx = (vaddr >> 39) & 0x1FF
> +        pgd_val = _read(base_va, pgd_idx)
> +        pgd_pa = virt_to_phys(prog, base_va).value_() + pgd_idx * 8
> +        entries.append(("PGD", pgd_pa, pgd_val))
> +        if not (pgd_val & 1):
> +            return entries
> +
> +        # PUD
> +        pud_table_pa = pgd_val & page_mask
> +        pud_idx = (vaddr >> 30) & 0x1FF
> +        pud_val = _read(phys_to_virt(prog, pud_table_pa).value_(), pud_idx)
> +        entries.append(("PUD", pud_table_pa + pud_idx * 8, pud_val))
> +        if not (pud_val & 1):
> +            return entries
> +        if pud_val & (1 << 7):
> +            return entries  # 1GB huge page
> +
> +        # PMD
> +        pmd_table_pa = pud_val & page_mask
> +        pmd_idx = (vaddr >> 21) & 0x1FF
> +        pmd_val = _read(phys_to_virt(prog, pmd_table_pa).value_(), pmd_idx)
> +        entries.append(("PMD", pmd_table_pa + pmd_idx * 8, pmd_val))
> +        if not (pmd_val & 1):
> +            return entries
> +        if pmd_val & (1 << 7):
> +            return entries  # 2MB huge page
> +
> +        # PTE
> +        pte_table_pa = pmd_val & page_mask
> +        pte_idx = (vaddr >> 12) & 0x1FF
> +        pte_val = _read(phys_to_virt(prog, pte_table_pa).value_(), pte_idx)
> +        entries.append(("PTE", pte_table_pa + pte_idx * 8, pte_val))
> +    except FaultError:
> +        pass
> +    return entries
> +
> +
> +def _pte_fields(mm, addr, fields):
> +    """Collect page table entries via page table walk."""
> +    if prog.platform.arch != Architecture.X86_64:
> +        return
> +    entries = _walk_page_table(mm, addr)
> +    for i, (level, pa, val) in enumerate(entries):
> +        if not val:
> +            fields[level] = "not present"
> +            continue
> +        if i == len(entries) - 1:
> +            # Final entry (PTE or huge page): show decoded flags
> +            flags = '|'.join(n for n, m in _PTE_FLAGS if val & m)
> +            fields[level] = f"{hex(pa)} => {hex(val)} ({flags})"
> +        else:
> +            # Intermediate level: just address => value
> +            fields[level] = f"{hex(pa)} => {hex(val)}"

[Severity: Medium]
In _walk_page_table(), won't page_mask = ~(prog["PAGE_SIZE"].value_() - 1)
leave upper bits (such as bit 63 NX or bits 52..62) set in pud_table_pa,
pmd_table_pa, and pte_table_pa?

Because Python integers have arbitrary precision, ~(4096 - 1) evaluates to
-4096, where all bits >= 12 are 1. If an intermediate PGD, PUD, or PMD entry
has NX (bit 63) or software bits set, masking with page_mask retains those
high bits and passes an invalid physical address to phys_to_virt(), raising
FaultError and aborting the page table walk.

Also, in both _pte_fields() and _rmap_fields(), the loop checks if not val:
to detect non-present entries. When an entry is non-zero but does not have
the PRESENT bit (bit 0) set (such as a swap PTE, migration entry, or
PROT_NONE entry), val is non-zero, so the code decodes swap/migration bits
against _PTE_FLAGS as if it were a present hardware PTE.

[ ... ]

> +def _swapcache_info(head):
> +    """Get swap entry value if page is in swap cache."""
> +    # drgn does not provide a PageSwapCache() helper, so we check the
> +    # decoded page flags string for PG_swapcache.
> +    flags_str = str(decode_page_flags(head))
> +    if "PG_swapcache" not in flags_str:
> +        return None
> +    try:
> +        return hex(head.private.value_())
> +    except (AttributeError, FaultError):
> +        return "Unknown"
> +
> +
> +def _page_fields(head, fields):
> +    """Collect non-slab page fields (refcount, mapping, anon/file, etc.)."""
> +    mapping = head.mapping.value_()
> +    fields["Page Mapping"] = hex(mapping)
> +
> +    if mapping == 0:
> +        fields["Page Anon/File"] = "Unmapped"
> +        refcount = head._refcount.counter.value_()
> +        if refcount > 0:
> +            fields["Page Refcount"] = refcount
> +            fields["Page Mapcount"] = head._mapcount.counter.value_()
> +        return
> +
> +    fields["Page Refcount"] = head._refcount.counter.value_()
> +    fields["Page Mapcount"] = head._mapcount.counter.value_()
> +
> +    lru = _lru_type(head)
> +    if lru:
> +        fields["LRU"] = lru
> +
> +    swap = _swapcache_info(head)
> +    if swap:
> +        fields["Swap Entry"] = swap

[Severity: Medium]
Can _page_fields() and _swapcache_info() miss actual swap-cache pages while
misidentifying non-swapbacked file pages as swap-cache pages?

In _page_fields(), returning early when mapping == 0 skips both _lru_type()
and _swapcache_info(). However, shmem folios in the swap cache and unmapped
anonymous swap-cache folios have mapping == NULL (0), so their LRU state and
Swap Entry will never be shown.

Meanwhile, in include/linux/page-flags.h, PG_swapcache is an alias for
PG_owner_priv_1 (the same bit number as PG_checked), and
folio_test_swapcache() requires both folio_test_swapbacked(folio) and
PG_swapcache. Because drgn's decode_page_flags() emits all enum pageflags
names for each set bit, any non-swapbacked filesystem page with
PG_owner_priv_1 / PG_checked set has "PG_swapcache" in decode_page_flags().
Without checking PageSwapBacked(head) in _swapcache_info(), filesystem
head.private values will be reported as Swap Entry.

[ ... ]

> +    # Show VMA and PTE details for first 2 mappings (--pfn/--page mode only)
> +    if not show_detail:
> +        return
> +    detail = min(2, len(rmap))
> +    for i in range(detail):
> +        pid, comm, s, e, mm, vaddr = rmap[i]
> +        prefix = f"Rmap {pid} " if detail > 1 else ""
> +        try:
> +            vma = vma_find(mm, vaddr)
> +            fields[f"{prefix}VMA"] = hex(vma.value_())

[Severity: Medium]
What happens in _rmap_fields() if the first two rmap entries belong to the
same process (same pid)? Since prefix only includes pid, won't the second
iteration overwrite the first entry's keys in fields?

[ ... ]

> +def _scan_page_types():
> +    """Scan all pages and categorize by type. Returns {type: page} dict."""
> +    from drgn.helpers.linux.mm import for_each_page
> +
> +    objexts_flag = prog.constant("MEMCG_DATA_OBJEXTS").value_()
> +    mask = prog.constant('__NR_MEMCG_DATA_FLAGS').value_() - 1
> +    base_stride = prog.type("struct slabobj_ext").size
> +    stride = base_stride * 2 if _slab_obj_ext_has_codetag() else base_stride
> +
> +    found = {}
> +    needed = ["anon", "file", "compound_anon", "compound_file",
> +              "slab_multi", "slab_single", "slab_no_objcg", "uncharged"]
> +
> +    for page in for_each_page(prog):
> +        try:
> +            head = compound_head(page) if PageCompound(page) else page
> +
> +            if PageSlab(head):
> +                if page.memcg_data.read_() & objexts_flag:
> +                    try:
> +                        slab = cast("struct slab *", head)
> +                        if slab.obj_exts_needs_objcg.value_():
> +                            n = _count_distinct_memcg(page, stride, mask)

[Severity: Low]
In _scan_page_types(), shouldn't the PageSlab(head) block use head rather
than page when reading memcg_data and calling _count_distinct_memcg(), and
compute the stride per slab via _slab_obj_ext_stride(slab)?

For tail pages of compound slabs, memcg_data and slab.objects live on head,
not page. Also, computing a single fixed stride outside the loop skips the
slab.obj_exts_in_object check that _slab_obj_ext_stride(slab) performs when
SLAB_OBJ_EXT_IN_OBJ is used.

[ ... ]

> +            if page.memcg_data.read_() == 0:
> +                if "uncharged" not in found:
> +                    found["uncharged"] = page
> +                continue

[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261019-show_page_info_refactor-v1-1-42518e2d7de7@linux.dev?part=1


      reply	other threads:[~2026-10-20  3:12 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-19  3:10 [PATCH] tools/mm: rewrite show_page_info.py with modular design and new features Ye Liu
2026-10-20  3:11 ` sashiko-bot [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-178431@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=akpm@linux-foundation.org \
    --cc=linux-mm@kvack.org \
    --cc=liuye@kylinos.cn \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=sj@kernel.org \
    --cc=stephen.s.brennan@oracle.com \
    --cc=ye.liu@linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox