Skip to content

Instantly share code, notes, and snippets.

@osandov
Created October 27, 2025 22:53
Show Gist options
  • Select an option

  • Save osandov/10e45e45afa29b11e0c7209247afc00b to your computer and use it in GitHub Desktop.

Select an option

Save osandov/10e45e45afa29b11e0c7209247afc00b to your computer and use it in GitHub Desktop.
#!/usr/bin/env -S drgn --main-symbols
import contextlib
import ctypes
import errno
import os
from pathlib import Path
import re
import subprocess
import tempfile
import time
from drgn import cast
from drgn.helpers.linux.fs import fget
from drgn.helpers.linux.pid import find_task
from drgn.helpers.linux.rbtree import rbtree_inorder_for_each_entry
_syscall = ctypes.CDLL(None, use_errno=True).syscall
_syscall.restype = ctypes.c_long
class struct_iovec(ctypes.Structure):
_fields_ = (
("iov_base", ctypes.c_void_p),
("iov_len", ctypes.c_size_t),
)
def read_gpas(console_log):
while True:
gpas = set()
try:
f = console_log.open("r")
except FileNotFoundError:
pass
else:
with f:
for line in f:
match = re.search(f"GPA=(0x[0-9a-f]+)", line)
if match:
gpas.add(int(match.group(1), 16))
elif "POKESTRESS RUNNING" in line:
return sorted(gpas)
time.sleep(1)
def find_kvm_vm_fd(pid):
for fd_link in Path(f"/proc/{pid}/fd").iterdir():
try:
target = fd_link.readlink()
except FileNotFoundError:
continue
if str(target) == "anon_inode:kvm-vm":
return int(fd_link.name)
raise LookupError("kvm-vm fd not found")
def gpas_to_hva_ranges(kvm, gpas):
slots = kvm.memslots[0].read_()
idx = slots.node_idx.value_()
mappings = []
PAGE_SHIFT = prog["PAGE_SHIFT"]
for slot in rbtree_inorder_for_each_entry(
"struct kvm_memory_slot",
slots.gfn_tree.address_of_(),
f"gfn_node[{idx}]"
):
gpa = (slot.base_gfn << PAGE_SHIFT).value_()
hva = slot.userspace_addr.value_()
length = (slot.npages << PAGE_SHIFT).value_()
mappings.append((gpa, hva, length))
SZ_4K = 4096
SZ_2M = 2 * 1024 * 1024
hva_ranges = set()
for gpa in gpas:
for map_gpa, map_hva, map_length in mappings:
gpa &= ~(SZ_4K - 1) # Align to a page.
# To allow for THP, if the full 2MB range containing the address is
# within the mapping, add the whole 2MB range.
gpa_2M = gpa & ~(SZ_2M - 1)
if map_gpa <= gpa_2M and gpa_2M + SZ_2M <= map_gpa + map_length:
hva = map_hva + (gpa_2M - map_gpa)
hva_ranges.add((hva, SZ_2M))
break
elif map_gpa <= gpa and gpa + SZ_4K <= map_gpa + map_length:
hva = map_hva + (gpa - map_gpa)
hva_ranges.add((hva, SZ_4K))
break
else:
raise LookupError(f"HVA for GPA {hex(gpa)} not found")
return sorted(hva_ranges)
def pageout_loop(pidfd, hva_ranges):
SYS_process_madvise = ctypes.c_long(440)
ctypes_pidfd = ctypes.c_int(pidfd)
iovecs = (struct_iovec * len(hva_ranges))()
for (address, length), iovec in zip(hva_ranges, iovecs):
iovec.iov_base = address
iovec.iov_len = length
n = ctypes.c_size_t(len(hva_ranges))
MADV_PAGEOUT = ctypes.c_int(21)
flags = ctypes.c_uint(0)
while True:
ret = _syscall(
SYS_process_madvise, ctypes_pidfd, iovecs, n, MADV_PAGEOUT, flags
)
if ret < 0:
errnum = ctypes.get_errno()
if errnum == errno.ESRCH:
break
raise OSError(errnum, os.strerror(errnum))
def repro(vmlinuz, initramfs):
with contextlib.ExitStack() as exit_stack:
temp_dir = Path(exit_stack.enter_context(tempfile.TemporaryDirectory()))
console_log = temp_dir / "console.log"
proc = exit_stack.enter_context(
subprocess.Popen(
[
"qemu-system-x86_64",
"-cpu", "host", "-enable-kvm",
"-nodefaults", "-display", "none",
"-chardev", f"stdio,mux=on,id=char0,logfile={console_log}",
"-mon", "chardev=char0,mode=readline",
"-serial", "chardev:char0",
"-smp", "4",
"-m", "1G",
"-no-reboot",
"-kernel", vmlinuz, "-initrd", initramfs,
"-append", "console=ttyS0,115200 panic=-1 rdinit=/bin/sh",
]
)
)
exit_stack.callback(proc.kill)
pid = proc.pid
pidfd = os.pidfd_open(pid)
exit_stack.callback(os.close, pidfd)
gpas = read_gpas(console_log)
fd = find_kvm_vm_fd(pid)
file = fget(find_task(pid), fd)
kvm = cast("struct kvm *", file.private_data)
hva_ranges = gpas_to_hva_ranges(kvm, gpas)
pageout_loop(pidfd, hva_ranges)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("vmlinuz")
parser.add_argument("initramfs")
args = parser.parse_args()
repro(args.vmlinuz, args.initramfs)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment