Home / Alt manpages / proc_pid_pagemap(5)

  • proc_pid_pagemap(5)
  • File format
  • linux

Read Linux Virtual Page Mappings with /proc/pid/pagemap

Every virtual page a process touches has an 8-byte entry in /proc/pid/pagemap that says whether it is resident, swapped, file-backed or soft-dirty. This guide reads one entry, decodes the bits, and changes nothing on the way. Allow about 15 minutes if Python 3 is already installed.

It follows the local proc_pid_pagemap(5) page from Linux man-pages 6.7, on a machine currently running Linux 6.8.0. The file has existed since Linux 2.6.25, but newer flag meanings are versioned below. You need a shell, Python 3 for the worked probe, and a process you are allowed to inspect.

1. Check the interface and page size

Confirm the kernel exposes the file, and record the system page size, with an ordinary shell check:

$ test -r /proc/self/pagemap && echo 'pagemap is readable'
pagemap is readable
$ getconf PAGESIZE
4096

The first command checks the current shell's view of the file. The page is present only when the kernel was built with CONFIG_PROC_PAGE_MONITOR, so a missing file is a kernel configuration or procfs problem, not a malformed offset.

Checkpoint

Keep the page-size value. Every pagemap entry is 8 bytes, and entry n starts at byte offset n * 8, where n is the virtual address divided by the page size.

2. Probe one page without changing process state

The Python program below allocates one anonymous page, writes a byte so it is actually backed, finds that page's virtual address, and reads exactly one 64-bit entry from /proc/self/pagemap. It runs against its own process, so there is no cross-process permission surprise in this basic example.

$ python3 - <<'PY'
import ctypes
import mmap
import os
import struct

page_size = os.sysconf("SC_PAGESIZE")
page = mmap.mmap(-1, page_size, prot=mmap.PROT_READ | mmap.PROT_WRITE)
page[0] = 0x41
address = ctypes.addressof(ctypes.c_char.from_buffer(page))
entry_offset = (address // page_size) * 8

with open("/proc/self/pagemap", "rb") as pagemap:
    raw = os.pread(pagemap.fileno(), 8, entry_offset)

if len(raw) != 8:
    raise RuntimeError(f"short pagemap read: {len(raw)} bytes")

entry, = struct.unpack("Q", raw)
print(f"virtual address: 0x{address:x}")
print(f"raw entry:      0x{entry:016x}")
print(f"present:        {(entry & (1 << 63)) != 0}")
print(f"swapped:        {(entry & (1 << 62)) != 0}")
print(f"file/shared:    {(entry & (1 << 61)) != 0}")
print(f"soft-dirty:     {(entry & (1 << 55)) != 0}")
print(f"exclusive:      {(entry & (1 << 56)) != 0}")
print(f"PFN field:      {entry & ((1 << 55) - 1)}")
PY
virtual address: 0x7f2...
raw entry:      0x8180000000000000
present:        True
swapped:        False
file/shared:    False
soft-dirty:     True
exclusive:      True
PFN field:      0

The address and raw value vary between runs; the shape that matters is a successful eight-byte read with present: True. A zero PFN here is normal for an unprivileged process on modern kernels: since Linux 4.2, the kernel zeroes the PFN field unless the reader has CAP_SYS_ADMIN. Do not treat a zero PFN as proof the page is unmapped when bit 63 says it is present.

There is no need for sudo on this self-check. Adding it changes the security context and will not make the output a more stable reference value.

3. Decode the entry fields

The local manpage describes one 64-bit value per virtual page. Read the fields from the high bits down.

BitsMeaningUse in a check
63Page is present in RAMPrimary resident-page test
62Page is in swapDistinguishes swapped storage from RAM
61File-mapped or shared anonymous pageQualifies the mapping type
57Write-protected through userfaultfdRelevant only when userfaultfd is part of the design
56Page is exclusively mappedUseful, but not a universal ownership guarantee
55PTE is soft-dirtyShows the soft-dirty state of the page table entry
54-0PFN if present, or swap encoding if swappedOnly useful as a PFN when access is permitted

Bits 60-58 are zero in this manpage's description. The meanings are not all from the same kernel release: bit 61 is documented since Linux 3.5, bit 56 since 4.2, bit 57 since 5.14, and the soft-dirty bit since 3.11. Treat an unknown bit layout as a version question, not a reason to guess.

If bit 63 is clear and bit 62 is also clear, the page is not currently present and is not represented as swapped either. That usually means the virtual address is unmapped, or the entry is otherwise empty. Check the address against the process's memory map before drawing a stronger conclusion.

4. Limit reads to mapped ranges

A large pagemap file (/proc/PID/pagemap) is sparse from the point of view of useful work, so do not scan every possible virtual address. First read /proc/PID/maps, pick the ranges relevant to your question, and seek only to the corresponding entry offsets.

$ PID=12345
$ sed -n '1,8p' "/proc/$PID/maps"
55b2c0000000-55b2c0021000 r--p 00000000 08:01 123456 /usr/bin/example
55b2c0021000-55b2c0049000 r-xp 00021000 08:01 123456 /usr/bin/example
...
$ getconf PAGESIZE
4096
  • Offset formula. For a range beginning at virtual address start, the first entry sits at byte offset (start / page_size) * 8. Round or iterate at page boundaries, and stop at the range end.
  • Races are normal. A map can disappear while you inspect a live process. Handle short reads and changing mappings as ordinary races, and re-read maps if a consistent snapshot matters.
  • Permission is a separate gate. Access to another process is governed by a PTRACE_MODE_READ_FSCREDS check. A readable /proc/$PID/maps does not guarantee /proc/$PID/pagemap will be readable too. Use a process you own or are authorised to inspect, and do not work around a denial by weakening procfs or ptrace protections.

Interpret failures without damaging evidence

  • Absent file. The interface is unavailable for that kernel.
  • Permission error. Points to the ptrace access check or file permissions.
  • Short read. Means the offset is beyond the available file, or the process's mapping changed. Record the PID, kernel version, page size and target address before retrying.

Security boundary

The PFN is sensitive information. The kernel restriction exists because physical-frame information can assist attacks such as Rowhammer, so avoid making a privileged reader a default troubleshooting step. If a controlled diagnostic genuinely needs PFNs, document why CAP_SYS_ADMIN is required, run the smallest possible read, and remove the extra capability afterwards according to your deployment process. There is no undo for a PFN disclosure.

Do not write to the file. The worked examples only read, and the allocated Python page is released when the process exits. If you started a long-running diagnostic, stop it normally with Ctrl-C; it has no persistent configuration to undo.

Done means

  • Confirmed the interface. /proc/self/pagemap is present and readable, and the host page size is known.
  • Ran a self-contained probe. It read one eight-byte entry after touching a real page.
  • Decoded the bits correctly. Bits 63, 62, 61, 57, 56 and 55 were interpreted using the installed manpage's version notes.
  • Treated PFN zeroing as expected. Unprivileged readers seeing a zero PFN is normal modern behaviour.
  • Scoped any wider search. A multi-page investigation starts from /proc/PID/maps and handles permission failures and mapping races.
  • Left everything as found. No procfs setting, process memory, service or persistent file was changed.