Read Linux Virtual Page Mappings with /proc/pid/pagemap
You will inspect one process virtual page, read its 64-bit pagemap entry, and decode the bits that say whether the page is resident, swapped, file-backed or soft-dirty. The method is read-only. It does not alter the target process or its memory. Allow about 15 minutes if Python 3 is already installed.
The route
Jump straight to the step you need, or tick off Done means at the end.
This guide 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 that you are allowed to inspect.
1. Check the interface and page size
Start by confirming that the kernel exposes the file and record the system page size. Use an ordinary shell for this 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. A missing file is therefore 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 following Python program allocates one anonymous page, writes a byte so the page is actually backed, finds that page's virtual address, and reads exactly one 64-bit entry. It opens /proc/self/pagemap for the Python process itself, so there is no cross-process permission surprise in the 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 useful shape is a successful eight-byte read with present: True. The zero PFN shown 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 interpret a zero PFN as proof that the page is unmapped when bit 63 says it is present.
There is no need for sudo for this self-check. Adding it may change the security context and will not make the output a 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 first:
| Bits | Meaning | Use in a check |
|---|---|---|
| 63 | Page is present in RAM | Primary resident-page test |
| 62 | Page is in swap | Distinguishes swapped storage from RAM |
| 61 | File-mapped or shared anonymous page | Qualifies the mapping type |
| 57 | Write-protected through userfaultfd | Relevant only when userfaultfd is part of the design |
| 56 | Page is exclusively mapped | Useful, but not a universal ownership guarantee |
| 55 | PTE is soft-dirty | Shows the soft-dirty state of the page table entry |
| 54-0 | PFN if present, or swap encoding if swapped | Only 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 as 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. 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 is sparse from the point of view of useful work. Do not scan every possible virtual address. First read /proc/PID/maps, select 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
For a range beginning at virtual address start, the first entry is at byte offset (start / page_size) * 8. Round or iterate at page boundaries, and stop at the range end. A map can disappear while you inspect a live process, so handle short reads and changing mappings as ordinary races. Re-read maps if a consistent snapshot matters.
Access to another process is governed by a PTRACE_MODE_READ_FSCREDS check. A readable /proc/$PID/maps does not guarantee that /proc/$PID/pagemap will be readable. Use a target process you own or are authorised to inspect. Do not work around a denial by weakening procfs or ptrace protections.
5. Interpret failures without damaging evidence
An absent /proc/PID/pagemap means the interface is unavailable for that kernel. A permission error points to the ptrace access check or file permissions. A short read can mean that the offset is beyond the available file or that the process's mapping changed. Record the PID, kernel version, page size and target address before retrying.
The PFN is sensitive information: the kernel restriction exists because physical-frame information can assist attacks such as Rowhammer. Avoid making a privileged reader a default troubleshooting step. If a controlled diagnostic genuinely requires PFNs, document why CAP_SYS_ADMIN is needed, run the smallest possible read, and remove the extra capability afterwards according to your deployment process. There is no undo operation for a PFN disclosure, so treat that decision as a security boundary.
Do not write to the file. The worked examples use only reads, and they leave the allocated Python page to be released when the process exits. If you added a long-running diagnostic, stop that diagnostic normally with Ctrl-C; it has no persistent configuration to undo.
Done means
/proc/self/pagemapis present and readable, and the host page size is known.- A self-contained probe read one eight-byte entry after touching a real page.
- Bits 63, 62, 61, 57, 56 and 55 were interpreted using the installed manpage's version notes.
- PFN zeroing for unprivileged readers was treated as expected modern behaviour.
- Any multi-page investigation starts from
/proc/PID/mapsand handles permission failures and mapping races. - No procfs setting, process memory, service or persistent file was changed.