#!/usr/bin/env python3 """fnwatch4.py — 4 hardware watchpoints simultaneously: DR0/DR1: dwords of the FIR pointer slot ctx+0x540668 (catches loader of ptr) DR2 : kernel word bin43.re DR3 : scratch word bin43 RW=read/write (traps readers AND writers), LEN=4B.""" import ctypes import glob import hashlib import json import os import signal import struct import subprocess import sys import time DUR = float(os.environ.get('WATCH_DUR', '20')) PTRACE_CONT = 7 PTRACE_GETREGS = 12 PTRACE_SETREGS = 13 PTRACE_PEEKUSER = 3 PTRACE_POKEUSER = 6 PTRACE_SEIZE = 0x4206 PTRACE_INTERRUPT = 0x4207 PTRACE_O_TRACECLONE = 2 DR_BASE = 0x350 # u_debugreg[0] DR6_OFF = DR_BASE + 48 # 0x388 - wait: idx6=+48 -> that IS DR6 slot? no: # u_debugreg[0..7]: DR0@+0, DR1@+8, DR2@+16, DR3@+24, DR4@+32, DR5@+40, # DR6@+48, DR7@+56 DR7_OFF = DR_BASE + 56 DR7_VAL = 0xFFFF000F # L0-3 enabled, all RW=11, all LEN=11 (4B) libc = ctypes.CDLL('libc.so.6', use_errno=True) class UserRegs(ctypes.Structure): _fields_ = [(n, ctypes.c_ulonglong) for n in ( 'r15', 'r14', 'r13', 'r12', 'rbp', 'rbx', 'r11', 'r10', 'r9', 'r8', 'rax', 'rcx', 'rdx', 'rsi', 'rdi', 'orig_rax', 'rip', 'cs', 'eflags', 'rsp', 'ss', 'fs_base', 'gs_base', 'ds', 'es', 'fs', 'gs')] def pt(req, pid, addr=0, data=0): libc.ptrace.restype = ctypes.c_long r = libc.ptrace(req, pid, ctypes.c_void_p(addr), ctypes.c_void_p(data)) return None if r == -1 else r def find_host(): for p in glob.glob('/proc/[0-9]*'): pid = int(os.path.basename(p)) try: cmd = open(f'/proc/{pid}/cmdline', 'rb').read().replace(b'\0', b' ').decode('utf8', 'replace') maps = open(f'/proc/{pid}/maps').read() except Exception: continue if 'soothe2' in maps and 'reaper' not in cmd: return pid return None def main(): subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; " "rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1", shell=True) rpp = '/home/m/soothe-bt/dual_b1q_0.5.rpp' wav = rpp.replace('.rpp', '.wav') wt0 = os.path.getmtime(wav) if os.path.exists(wav) else 0 proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors', '-renderproject', rpp], stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT) host = None t0 = time.time() while time.time() - t0 < 30 and not host: host = find_host() time.sleep(0.002) if not host: print('NO HOST') return 1 fd = os.open(f'/proc/{host}/mem', os.O_RDONLY) def rd(a, n): try: return os.pread(fd, n, a) except OSError: return None ctx = firptr = scrptr = None vt = struct.pack('= 0: cand = a + j sb = rd(cand + 0x540870, 4) if sb and struct.unpack(' 100: ctx = cand firptr = struct.unpack(' 0.03: last_sweep = now for tid_s in glob.glob(f'/proc/{host}/task/*'): tid = int(os.path.basename(tid_s)) if tid not in seized: if pt(PTRACE_SEIZE, tid, 0, PTRACE_O_TRACECLONE) is not None: seized.add(tid) pt(PTRACE_INTERRUPT, tid) for _ in range(40): try: wpid, _st = os.waitpid(tid, os.WUNTRACED | os.WNOHANG) except ChildProcessError: break if wpid == tid: break time.sleep(0.001) if arm(tid): armed.add(tid) try: pt(PTRACE_CONT, tid, 0, 0) except OSError: pass try: pid, status = os.waitpid(-1, os.WSTOPPED | os.WNOHANG) except ChildProcessError: break if pid == 0: time.sleep(0.0005) continue sig = status >> 8 if os.WIFEXITED(status) or os.WIFSIGNALED(status): continue if os.WIFSTOPPED(pid) and sig == signal.SIGTRAP: regs = UserRegs() if pt(PTRACE_GETREGS, pid, 0, ctypes.addressof(regs)) is None: continue dr6v = pt(PTRACE_PEEKUSER, pid, DR_BASE + 48) rec = dict(rip=regs.rip, rsp=regs.rsp, tid=pid, dr6=dr6v, ret=struct.unpack(' wt0: main._wv = True print('wav fresh at %.2fs' % (time.time() - t0), flush=True) if not hasattr(main, '_wv'): main._wv = False fresh = main._wv print('hits=%d render_fresh=%s armed=%d/%d' % (len(hits), fresh, len(armed), len(seized))) json.dump(hits[:200], open('/tmp/opencode/fnwatch/hits4.json', 'w'), indent=1, default=str) proc.kill() return 0 if __name__ == '__main__': os.makedirs('/tmp/opencode/fnwatch', exist_ok=True) sys.exit(main())