# Stack scan v7: read thread stack in small chunks, skip unreadable pages # (v6 failed reading upward from rsp when the thread's stack was shallow and # the 0x4000 chunk crossed the committed top). Also dumps full registers for # the window-owner thread so the wait handle/params can be read. # Usage: python stackscan.py --bridge | --probe [secs] import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time sys.stdout.reconfigure(encoding='utf-8', errors='replace') MODE = sys.argv[1] if len(sys.argv) > 1 else "--bridge" SECS = int(sys.argv[2]) if len(sys.argv) > 2 else 15 SHM_NAME = "SonicForge_DAW_IPC_VERIFY" SIZE = 32768 BRIDGE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe" PROBE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\gui_probe.exe" NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3" LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stack_sample.log" if MODE == "--bridge" else "stack_probe.log") k32 = ctypes.windll.kernel32 k32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, w.DWORD, w.DWORD, w.DWORD, ctypes.c_char_p] k32.CreateFileMappingA.restype = ctypes.c_void_p k32.MapViewOfFile.argtypes = [ctypes.c_void_p, w.DWORD, w.DWORD, w.DWORD, ctypes.c_size_t] k32.MapViewOfFile.restype = ctypes.c_void_p INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value hMap = k32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, 0x04, 0, SIZE, SHM_NAME.encode()) ptr = k32.MapViewOfFile(hMap, 0xF001F, 0, 0, 0) def wait_log(pattern, timeout): end = time.time() + timeout while time.time() < end: try: with open(LOG, 'r', encoding='utf-8', errors='replace') as f: if pattern in f.read(): return True except FileNotFoundError: pass time.sleep(0.2) return False if MODE == "--bridge": extra = dict(os.environ) for k in list(extra): if k.startswith("SF_"): del extra[k] extra.update({"SF_SHM_NAME": SHM_NAME, "SF_SAMPLE_RATE": "48000", "SF_BLOCK_SIZE": "256", "SF_ONCE": "1", "SF_AUTOGUI": "0"}) proc = subprocess.Popen([BRIDGE, "--shm", SHM_NAME], env=extra, stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT) marker = "isPlatformTypeSupported=0" wait_after = 3 wnd_class = "SonicForge_Native_VST3_Class" else: proc = subprocess.Popen([PROBE, NEXUS, "bridge_exact11", str(SECS)], stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT) marker = "openGUI: attached=0" wait_after = 2 wnd_class = "SonicForge_Native_VST3_Class" print("pid", proc.pid, "mode", MODE) if not wait_log(marker, 120): print("TIMEOUT waiting for", marker) sys.exit(1) time.sleep(wait_after) pid = proc.pid PROCESS_QUERY_INFORMATION = 0x0400 PROCESS_VM_READ = 0x0010 k32.OpenProcess.argtypes = [w.DWORD, w.BOOL, w.DWORD] k32.OpenProcess.restype = ctypes.c_void_p hProc = k32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid) if not hProc: print("OpenProcess failed", ctypes.get_last_error()) sys.exit(1) psapi = ctypes.windll.psapi class MODULEINFO(ctypes.Structure): _fields_ = [("lpBaseOfDll", ctypes.c_void_p), ("SizeOfImage", w.DWORD), ("pad", w.DWORD), ("EntryPoint", ctypes.c_void_p)] psapi.EnumProcessModulesEx.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), w.DWORD, ctypes.POINTER(w.DWORD), w.DWORD] psapi.GetModuleBaseNameA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p, w.DWORD] psapi.GetModuleInformation.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(MODULEINFO), w.DWORD] mods = [] buf = (ctypes.c_void_p * 1024)() needed = w.DWORD(0) if psapi.EnumProcessModulesEx(hProc, buf, ctypes.sizeof(buf), ctypes.byref(needed), 3): n = needed.value // ctypes.sizeof(ctypes.c_void_p) for i in range(min(n, 1024)): base = buf[i] name = ctypes.create_string_buffer(260) psapi.GetModuleBaseNameA(hProc, base, name, 260) info = MODULEINFO() psapi.GetModuleInformation(hProc, base, ctypes.byref(info), ctypes.sizeof(info)) mods.append((base, info.SizeOfImage, name.value.decode('utf-8', 'replace'))) mods.sort(key=lambda m: m[0]) def modname(addr): if addr == 0: return "null" for b, s, n in mods: if b <= addr < b + s: return "%s+%x" % (n, addr - b) return None # --- find the target window + its owning thread (main) --- user32 = ctypes.windll.user32 user32.EnumWindows.argtypes = [ctypes.c_void_p, ctypes.c_void_p] user32.GetWindowThreadProcessId.argtypes = [ctypes.c_void_p, ctypes.POINTER(w.DWORD)] user32.GetClassNameA.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int] WNDENUMPROC = ctypes.WINFUNCTYPE(w.BOOL, ctypes.c_void_p, ctypes.c_void_p) found = {"hwnd": None, "tid": None} @WNDENUMPROC def enumcb(hwnd, lparam): wpid = w.DWORD(0) tid = user32.GetWindowThreadProcessId(hwnd, ctypes.byref(wpid)) if wpid.value != pid: return True cls = ctypes.create_string_buffer(256) user32.GetClassNameA(hwnd, cls, 256) if cls.value.decode('utf-8', 'replace') == wnd_class: found["hwnd"] = hwnd found["tid"] = tid return False return True user32.EnumWindows(enumcb, None) print("target window hwnd=%s tid=%s" % (found["hwnd"], found["tid"])) class THREADENTRY32(ctypes.Structure): _fields_ = [("dwSize", w.DWORD), ("cntUsage", w.DWORD), ("th32ThreadID", w.DWORD), ("th32OwnerProcessID", w.DWORD), ("tpBasePri", ctypes.c_long), ("tpDeltaPri", ctypes.c_long), ("dwFlags", w.DWORD)] TH32CS_SNAPTHREAD = 0x4 THREAD_SUSPEND_RESUME = 0x0002 THREAD_GET_CONTEXT = 0x0008 THREAD_QUERY_INFORMATION = 0x0040 CONTEXT_CTRL_INT_SEG = 0x100001 | 0x2 | 0x20 class CONTEXT64(ctypes.Structure): _fields_ = [ ("P1Home", ctypes.c_uint64), ("P2Home", ctypes.c_uint64), ("P3Home", ctypes.c_uint64), ("P4Home", ctypes.c_uint64), ("P5Home", ctypes.c_uint64), ("P6Home", ctypes.c_uint64), ("ContextFlags", w.DWORD), ("MxCsr", w.DWORD), ("SegCs", w.WORD), ("SegDs", w.WORD), ("SegEs", w.WORD), ("SegFs", w.WORD), ("SegGs", w.WORD), ("SegSs", w.WORD), ("EFlags", w.DWORD), ("Dr0", ctypes.c_uint64), ("Dr1", ctypes.c_uint64), ("Dr2", ctypes.c_uint64), ("Dr3", ctypes.c_uint64), ("Dr6", ctypes.c_uint64), ("Dr7", ctypes.c_uint64), ("Rax", ctypes.c_uint64), ("Rcx", ctypes.c_uint64), ("Rdx", ctypes.c_uint64), ("Rbx", ctypes.c_uint64), ("Rsp", ctypes.c_uint64), ("Rbp", ctypes.c_uint64), ("Rsi", ctypes.c_uint64), ("Rdi", ctypes.c_uint64), ("R8", ctypes.c_uint64), ("R9", ctypes.c_uint64), ("R10", ctypes.c_uint64), ("R11", ctypes.c_uint64), ("R12", ctypes.c_uint64), ("R13", ctypes.c_uint64), ("R14", ctypes.c_uint64), ("R15", ctypes.c_uint64), ("Rip", ctypes.c_uint64), ] k32.CreateToolhelp32Snapshot.argtypes = [w.DWORD, w.DWORD] k32.CreateToolhelp32Snapshot.restype = ctypes.c_void_p snap = k32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) te = THREADENTRY32(); te.dwSize = ctypes.sizeof(THREADENTRY32) threads = [] ok = k32.Thread32First(snap, ctypes.byref(te)) while ok: if te.th32OwnerProcessID == pid: threads.append(te.th32ThreadID) ok = k32.Thread32Next(snap, ctypes.byref(te)) INTEREST = ("Nexus", "daw_", "USER32", "ntdll", "win32u", "KERNELBASE", "ole32", "combase", "uxtheme", "d2d1", "d3d11", "MSCTF", "IMM32", "SHELL32") def read_mem(addr, size): """ReadProcessMemory with proper argtypes; returns bytes or None.""" chunk = ctypes.create_string_buffer(size) read = ctypes.c_size_t(0) k32.ReadProcessMemory.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)] if not k32.ReadProcessMemory(hProc, ctypes.c_void_p(addr), chunk, size, ctypes.byref(read)): return None return chunk.raw[:read.value] def scan_stack(tid, depth=0x20000): k32.OpenThread.argtypes = [w.DWORD, w.BOOL, w.DWORD] k32.OpenThread.restype = ctypes.c_void_p hT = k32.OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, False, tid) if not hT: return None, [], None k32.SuspendThread(hT) ctx = CONTEXT64(); ctx.ContextFlags = CONTEXT_CTRL_INT_SEG r = k32.GetThreadContext(hT, ctypes.byref(ctx)) k32.ResumeThread(hT) k32.CloseHandle(hT) if not r: return None, [], None rsp = ctx.Rsp chunks = [] fails = [] # read downward a bit first (trap frame may sit below rsp), then upward for base in (rsp - 0x1000, rsp, rsp + 0x1000, rsp + 0x2000): if base > rsp + depth: break d = read_mem(base, 0x1000) if d is None: fails.append(hex(base)) else: chunks.append((base, d)) data = b"".join(d for _, d in chunks) seen = [] for addr, d in chunks: for i in range(0, len(d) - 8, 8): val = struct.unpack_from('= rsp] or stack for a, mn in shown[-60:]: print(" ", "%s @%x" % (mn, a)) print("--- log tail ---") try: with open(LOG, 'rb') as f: f.seek(max(0, os.path.getsize(LOG) - 1000)) print(f.read().decode('utf-8', 'replace')) except FileNotFoundError: pass