-
-
Save osandov/10e45e45afa29b11e0c7209247afc00b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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