Files
soothe2-re/scripts/fnexec.py
T

227 lines
8.0 KiB
Python

#!/usr/bin/env python3
"""fnexec.py — hardware EXEC breakpoints on 529fe0 entry and FIR-loop head.
Answers definitively whether the decoded mask chain executes during render."""
import ctypes
import glob
import os
import signal
import struct
import numpy as np
import subprocess
import sys
import time
DUR = float(os.environ.get('EXEC_DUR', '20'))
PTRACE_CONT = 7
PTRACE_GETREGS = 12
PTRACE_SETREGS = 13
PTRACE_PEEKUSER = 3
PTRACE_POKEUSER = 6
PTRACE_SINGLESTEP = 9
PTRACE_SEIZE = 0x4206
PTRACE_INTERRUPT = 0x4207
PTRACE_O_TRACECLONE = 2
DR_BASE = 0x350
DR7_OFF = DR_BASE + 56
# DR0=exec 529fe0, DR1=exec 52b550; RW=00 LEN=00 both; L0,L1 enabled
DR7_VAL = 0x00000003
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)
wav = '/home/m/soothe-bt/dual_b1q_0.5.wav'
wt0 = os.path.getmtime(wav) if os.path.exists(wav) else 0
proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors',
'-renderproject', '/home/m/soothe-bt/dual_b1q_0.5.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
print('host %d at %.2fs' % (host, time.time() - t0), flush=True)
# EARLY arm: exec watch on DESIGN bodies (inherited by future clones)
fd0 = os.open(f'/proc/{host}/mem', os.O_RDONLY)
pt(PTRACE_SEIZE, host, 0, PTRACE_O_TRACECLONE)
pt(PTRACE_INTERRUPT, host)
for _ in range(60):
try:
wpid, _st = os.waitpid(host, os.WUNTRACED | os.WNOHANG)
except ChildProcessError:
break
if wpid == host:
break
time.sleep(0.001)
pt(PTRACE_POKEUSER, host, DR_BASE + 0, 0x1802A24C0)
pt(PTRACE_POKEUSER, host, DR_BASE + 8, 0x1802FA420)
pt(PTRACE_POKEUSER, host, DR7_OFF, 0x3)
ok0 = pt(PTRACE_PEEKUSER, host, DR7_OFF)
print('early arm dr7=%#x' % (ok0 or 0), flush=True)
pt(PTRACE_CONT, host, 0, 0)
# phase B: seize ALL tids NOW and arm exec watches
def arm(tid):
pt(PTRACE_POKEUSER, tid, DR_BASE + 0, 0x180529FE0)
pt(PTRACE_POKEUSER, tid, DR_BASE + 8, 0x18052B550)
pt(PTRACE_POKEUSER, tid, DR7_OFF, DR7_VAL)
v = pt(PTRACE_PEEKUSER, tid, DR7_OFF)
return v is not None and (v & ~0x400) == DR7_VAL
seized = {host}
armed = set()
for tid_s in glob.glob(f'/proc/{host}/task/*') or [host]:
tid = int(os.path.basename(tid_s))
pt(PTRACE_SEIZE, tid, 0, PTRACE_O_TRACECLONE)
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
print('armed %d/%d tids' % (len(armed), len(seized)), flush=True)
n0 = n1 = 0
samples = []
t_end = time.time() + DUR
last_sweep = 0.0
fresh_t = None
while time.time() < t_end:
now = time.time()
if now - last_sweep > 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.0004)
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
rip = regs.rip - (1 if False else 0)
if rip not in (0x180529FE0, 0x18052B550, 0x1802A24C0, 0x1802FA420):
pt(PTRACE_CONT, pid, 0, 0)
continue
if rip == 0x180529FE0:
n0 += 1
elif rip == 0x18052B550:
n1 += 1
rec = dict(which='design' if rip in (0x1802A24C0, 0x1802FA420)
else ('529fe0' if hit529 else 'loop'),
rip=rip, rcx=regs.rcx, rdx=regs.rdx,
cnt=regs.r8 & 0xffffffff,
t=round(time.time() - t0, 3), tid=pid)
# dump band curve (rcx) and scratch (rdx) for design hits
if rec['which'] == 'design':
def rdarr(ptr, n=2049):
try:
b = os.pread(fd, n * 4, ptr)
return np.frombuffer(b, dtype='<f4').astype(np.float32).tolist()
except OSError:
return []
rec['in'] = rdarr(regs.rcx)
rec['out'] = rdarr(regs.rdx)
print('DESIGN hit tid=%d rcx=%#x rdx=%#x cnt=%#x in[85]=%g'
% (pid, regs.rcx, regs.rdx, rec['cnt'],
rec['in'][85] if rec['in'] else -1), flush=True)
samples.append(rec)
if len(samples) > 400:
break
# pass exec trap: RF flag suppresses next report
pt(PTRACE_POKEUSER, pid, DR_BASE + 48, 0xFFFF0FF0)
regs.eflags |= 0x10000
pt(PTRACE_SETREGS, pid, 0, ctypes.addressof(regs))
pt(PTRACE_CONT, pid, 0, 0)
elif os.WIFSTOPPED(pid):
pt(PTRACE_CONT, pid, 0, sig if 0 < sig < 32 else 0)
if fresh_t is None and os.path.exists(wav) and os.path.getmtime(wav) > wt0:
fresh_t = time.time() - t0
print('wav fresh at %.2fs' % fresh_t, flush=True)
fresh = fresh_t is not None
print('hits: 529fe0=%d loop=%d render_fresh=%s armed=%d/%d'
% (n0, n1, fresh, len(armed), len(seized)))
import json
json.dump(samples[:300], open('/tmp/opencode/fnexec/hits.json', 'w'),
indent=1, default=str)
proc.kill()
return 0
def rdsp(sp, pid):
return None
if __name__ == '__main__':
os.makedirs('/tmp/opencode/fnexec', exist_ok=True)
sys.exit(main())