23i: hardware watchpoint infrastructure working (u_debugreg base 0x350, LEN8 rejected -> use LEN4, DR6 reset documented); zero hits on kernel word — consumer may read a copy or builds early; fnwatch.py committed

This commit is contained in:
2026-08-23 23:31:26 +03:00
parent 05caf1eb44
commit 25b736b2bb
4 changed files with 511 additions and 0 deletions
+10
View File
@@ -2412,3 +2412,13 @@ t_{-24,-12,0,+24}.rpp (тон@1000, бин 85): scr85 = 1.0071 / 0.2276 /
2. Проверить 0x18052e1c0/0x180534580 дизасмом (уже лежат окна? нет — снять).
3. После опознания дубля: bp на его выход → [rsp]=адрес применения → декод
масштаба 1.805.
### 23i (короткое): HW-watchpoint заведён, попаданий нет
Отладочные регистры через POKEUSER: база u_debugreg = **0x350** (не 0x340/0x380;
DR7 @0x388; LEN=10(8B) отвергается ядром — использовать LEN=11(4B),
DR7=L0|R/W=11|LEN=11 = 0xF0001; сброс DR6 = poke 0xFFFF0FF0 @DR0+48).
scripts/fnwatch.py: watch на слово кернела (bin43.re) от момента создания
экземпляра до конца хоста — **0 попаданий** при подтверждённом армировании.
Гипотезы: потребитель читает КОПИЮ кернела; либо строится однократно до
армирования; либо выравнивание доступа мимо слова. Следующий шаг: watch
нескольких слов + старт с рождения хоста + контроль факта рендера (wav mtime).
+69
View File
@@ -0,0 +1,69 @@
; hot_52e1c0
/tmp/slice.bin: file format binary
Disassembly of section .data:
000000018052e1c0 <.data>:
18052e1c0: 05 e8 fa 2e ad add $0xad2efae8,%eax
18052e1c5: ff 85 f6 48 89 1f incl 0x1f8948f6(%rbp)
18052e1cb: 0f 48 f3 cmovs %ebx,%esi
18052e1ce: 89 77 08 mov %esi,0x8(%rdi)
18052e1d1: 85 f6 test %esi,%esi
18052e1d3: 74 2f je 0x18052e204
18052e1d5: 8d 0c b5 00 00 00 00 lea 0x0(,%rsi,4),%ecx
18052e1dc: e8 9f 2e ad ff call 0x180001080
18052e1e1: 48 89 07 mov %rax,(%rdi)
18052e1e4: 39 5f 08 cmp %ebx,0x8(%rdi)
18052e1e7: 7e 1b jle 0x18052e204
18052e1e9: 48 8b cb mov %rbx,%rcx
18052e1ec: 0f 1f 40 00 nopl 0x0(%rax)
18052e1f0: 48 8b 07 mov (%rdi),%rax
18052e1f3: 48 8d 49 04 lea 0x4(%rcx),%rcx
18052e1f7: ff c3 inc %ebx
18052e1f9: f3 0f 11 74 01 fc movss %xmm6,-0x4(%rcx,%rax,1)
18052e1ff: 3b 5f 08 cmp 0x8(%rdi),%ebx
18052e202: 7c ec jl 0x18052e1f0
18052e204: 48 8b 5c 24 40 mov 0x40(%rsp),%rbx
18052e209: 48 8b 74 24 48 mov 0x48(%rsp),%rsi
18052e20e: 0f 28 74 24 20 movaps 0x20(%rsp),%xmm6
18052e213: 48 83 c4 30 add $0x30,%rsp
18052e217: 5f pop %rdi
18052e218: c3 ret
18052e219: cc int3
18052e21a: cc int3
18052e21b: cc int3
18052e21c: cc int3
18052e21d: cc int3
18052e21e: cc int3
18052e21f: cc int3
18052e220: 40 53 rex push %rbx
18052e222: 48 83 ec 20 sub $0x20,%rsp
18052e226: 83 79 08 00 cmpl $0x0,0x8(%rcx)
18052e22a: 48 8b d9 mov %rcx,%rbx
18052e22d: 7e 1b jle 0x18052e24a
18052e22f: 48 8b 09 mov (%rcx),%rcx
18052e232: 48 85 c9 test %rcx,%rcx
18052e235: 74 05 je 0x18052e23c
18052e237: e8 84 2e ad ff call 0x1800010c0
18052e23c: 33 c0 xor %eax,%eax
18052e23e: 48 89 03 mov %rax,(%rbx)
18052e241: 89 43 08 mov %eax,0x8(%rbx)
18052e244: 48 83 c4 20 add $0x20,%rsp
18052e248: 5b pop %rbx
18052e249: c3 ret
18052e24a: 33 c0 xor %eax,%eax
18052e24c: 48 89 01 mov %rax,(%rcx)
18052e24f: 89 41 08 mov %eax,0x8(%rcx)
18052e252: 48 83 c4 20 add $0x20,%rsp
18052e256: 5b pop %rbx
18052e257: c3 ret
18052e258: cc int3
18052e259: cc int3
18052e25a: cc int3
18052e25b: cc int3
18052e25c: cc int3
18052e25d: cc int3
18052e25e: cc int3
18052e25f: cc int3
+198
View File
@@ -0,0 +1,198 @@
/tmp/slice.bin: file format binary
Disassembly of section .data:
0000000180534580 <.data>:
180534580: 48 89 1f mov %rbx,(%rdi)
180534583: 0f 48 f3 cmovs %ebx,%esi
180534586: 89 77 08 mov %esi,0x8(%rdi)
180534589: 85 f6 test %esi,%esi
18053458b: 74 26 je 0x1805345b3
18053458d: 8b ce mov %esi,%ecx
18053458f: e8 ec ca ac ff call 0x180001080
180534594: 48 89 07 mov %rax,(%rdi)
180534597: 39 5f 08 cmp %ebx,0x8(%rdi)
18053459a: 7e 17 jle 0x1805345b3
18053459c: 48 8b cb mov %rbx,%rcx
18053459f: 90 nop
1805345a0: 48 8b 07 mov (%rdi),%rax
1805345a3: 48 8d 49 01 lea 0x1(%rcx),%rcx
1805345a7: ff c3 inc %ebx
1805345a9: c6 44 01 ff 00 movb $0x0,-0x1(%rcx,%rax,1)
1805345ae: 3b 5f 08 cmp 0x8(%rdi),%ebx
1805345b1: 7c ed jl 0x1805345a0
1805345b3: 48 8b 5c 24 30 mov 0x30(%rsp),%rbx
1805345b8: 48 8b 74 24 38 mov 0x38(%rsp),%rsi
1805345bd: 48 83 c4 20 add $0x20,%rsp
1805345c1: 5f pop %rdi
1805345c2: c3 ret
1805345c3: cc int3
1805345c4: cc int3
1805345c5: cc int3
1805345c6: cc int3
1805345c7: cc int3
1805345c8: cc int3
1805345c9: cc int3
1805345ca: cc int3
1805345cb: cc int3
1805345cc: cc int3
1805345cd: cc int3
1805345ce: cc int3
1805345cf: cc int3
1805345d0: 40 53 rex push %rbx
1805345d2: 48 83 ec 40 sub $0x40,%rsp
1805345d6: 48 c7 44 24 30 fe ff movq $0xfffffffffffffffe,0x30(%rsp)
1805345dd: ff ff
1805345df: 48 8b d9 mov %rcx,%rbx
1805345e2: 48 8d 05 c7 ce f3 ff lea -0xc3139(%rip),%rax # 0x1804714b0
1805345e9: 48 89 44 24 20 mov %rax,0x20(%rsp)
1805345ee: 4c 8d 0d 8b 00 00 00 lea 0x8b(%rip),%r9 # 0x180534680
1805345f5: ba 80 00 00 00 mov $0x80,%edx
1805345fa: 44 8d 42 90 lea -0x70(%rdx),%r8d
1805345fe: e8 01 d6 bf 00 call 0x181131c04
180534603: 90 nop
180534604: c7 83 00 08 00 00 08 movl $0x8,0x800(%rbx)
18053460b: 00 00 00
18053460e: 48 c7 83 04 08 00 00 movq $0x447a0000,0x804(%rbx)
180534615: 00 00 7a 44
180534619: c7 83 0c 08 00 00 f4 movl $0x3f34fdf4,0x80c(%rbx)
180534620: fd 34 3f
180534623: c6 83 10 08 00 00 00 movb $0x0,0x810(%rbx)
18053462a: c7 83 14 08 00 00 01 movl $0x1,0x814(%rbx)
180534631: 00 00 00
180534634: 33 c0 xor %eax,%eax
180534636: 48 89 83 18 08 00 00 mov %rax,0x818(%rbx)
18053463d: 48 89 83 20 08 00 00 mov %rax,0x820(%rbx)
180534644: 48 8b c3 mov %rbx,%rax
180534647: 48 83 c4 40 add $0x40,%rsp
18053464b: 5b pop %rbx
18053464c: c3 ret
18053464d: cc int3
18053464e: cc int3
18053464f: cc int3
180534650: 48 83 ec 38 sub $0x38,%rsp
180534654: 48 c7 44 24 20 fe ff movq $0xfffffffffffffffe,0x20(%rsp)
18053465b: ff ff
18053465d: 4c 8d 0d 4c ce f3 ff lea -0xc31b4(%rip),%r9 # 0x1804714b0
180534664: ba 80 00 00 00 mov $0x80,%edx
180534669: 44 8d 42 90 lea -0x70(%rdx),%r8d
18053466d: e8 aa d0 bf 00 call 0x18113171c
180534672: 90 nop
180534673: 48 83 c4 38 add $0x38,%rsp
180534677: c3 ret
180534678: cc int3
180534679: cc int3
18053467a: cc int3
18053467b: cc int3
18053467c: cc int3
18053467d: cc int3
18053467e: cc int3
18053467f: cc int3
180534680: 48 83 ec 28 sub $0x28,%rsp
180534684: e8 a7 00 00 00 call 0x180534730
180534689: 4c 8b 19 mov (%rcx),%r11
18053468c: 48 ba 00 00 00 00 00 movabs $0x3ff0000000000000,%rdx
180534693: 00 f0 3f
180534696: 49 89 13 mov %rdx,(%r11)
180534699: 48 8b 41 08 mov 0x8(%rcx),%rax
18053469d: 48 89 10 mov %rdx,(%rax)
1805346a0: 48 8b c1 mov %rcx,%rax
1805346a3: 48 83 c4 28 add $0x28,%rsp
1805346a7: c3 ret
1805346a8: cc int3
1805346a9: cc int3
1805346aa: cc int3
1805346ab: cc int3
1805346ac: cc int3
1805346ad: cc int3
1805346ae: cc int3
1805346af: cc int3
1805346b0: 48 89 5c 24 08 mov %rbx,0x8(%rsp)
1805346b5: 48 89 74 24 10 mov %rsi,0x10(%rsp)
1805346ba: 57 push %rdi
1805346bb: 48 83 ec 20 sub $0x20,%rsp
1805346bf: 48 8b d9 mov %rcx,%rbx
1805346c2: 8b f2 mov %edx,%esi
1805346c4: 48 83 c1 60 add $0x60,%rcx
1805346c8: e8 a3 11 d7 00 call 0x1812a5870
1805346cd: 48 8d 05 64 7f f7 01 lea 0x1f77f64(%rip),%rax # 0x1824ac638
1805346d4: 48 89 03 mov %rax,(%rbx)
1805346d7: 48 8b 4b 48 mov 0x48(%rbx),%rcx
1805346db: c7 43 54 00 00 00 00 movl $0x0,0x54(%rbx)
1805346e2: ff 15 c0 6b 67 01 call *0x1676bc0(%rip) # 0x181bab2a8
1805346e8: 48 8b 7b 08 mov 0x8(%rbx),%rdi
1805346ec: 48 85 ff test %rdi,%rdi
1805346ef: 74 17 je 0x180534708
1805346f1: 48 8b 4f 18 mov 0x18(%rdi),%rcx
1805346f5: ff 15 ad 6b 67 01 call *0x1676bad(%rip) # 0x181bab2a8
1805346fb: ba 28 01 00 00 mov $0x128,%edx
180534700: 48 8b cf mov %rdi,%rcx
180534703: e8 ec c9 bf 00 call 0x1811310f4
180534708: 40 f6 c6 01 test $0x1,%sil
18053470c: 74 0d je 0x18053471b
18053470e: ba e0 00 00 00 mov $0xe0,%edx
180534713: 48 8b cb mov %rbx,%rcx
180534716: e8 d9 c9 bf 00 call 0x1811310f4
18053471b: 48 8b 74 24 38 mov 0x38(%rsp),%rsi
180534720: 48 8b c3 mov %rbx,%rax
180534723: 48 8b 5c 24 30 mov 0x30(%rsp),%rbx
180534728: 48 83 c4 20 add $0x20,%rsp
18053472c: 5f pop %rdi
18053472d: c3 ret
18053472e: cc int3
18053472f: cc int3
180534730: 4c 8d 41 18 lea 0x18(%rcx),%r8
180534734: c7 41 10 03 00 00 00 movl $0x3,0x10(%rcx)
18053473b: 49 8b c0 mov %r8,%rax
18053473e: 4c 89 01 mov %r8,(%rcx)
180534741: 0f 57 c9 xorps %xmm1,%xmm1
180534744: c6 41 78 01 movb $0x1,0x78(%rcx)
180534748: 48 83 c0 10 add $0x10,%rax
18053474c: f2 0f 11 4c 24 08 movsd %xmm1,0x8(%rsp)
180534752: f2 0f 10 44 24 08 movsd 0x8(%rsp),%xmm0
180534758: 48 8d 51 30 lea 0x30(%rcx),%rdx
18053475c: 48 89 51 08 mov %rdx,0x8(%rcx)
180534760: 66 0f c6 c0 00 shufpd $0x0,%xmm0,%xmm0
180534765: 41 f6 c0 0f test $0xf,%r8b
180534769: 75 06 jne 0x180534771
18053476b: 41 0f 11 00 movups %xmm0,(%r8)
18053476f: eb 0f jmp 0x180534780
180534771: 41 0f 11 00 movups %xmm0,(%r8)
180534775: 66 66 66 0f 1f 84 00 data16 data16 nopw 0x0(%rax,%rax,1)
18053477c: 00 00 00 00
180534780: 45 33 c9 xor %r9d,%r9d
180534783: f2 0f 11 4c 24 08 movsd %xmm1,0x8(%rsp)
180534789: f2 0f 10 44 24 08 movsd 0x8(%rsp),%xmm0
18053478f: 4c 89 08 mov %r9,(%rax)
180534792: 48 8b c2 mov %rdx,%rax
180534795: 48 83 c0 10 add $0x10,%rax
180534799: 66 0f c6 c0 00 shufpd $0x0,%xmm0,%xmm0
18053479e: f6 c2 0f test $0xf,%dl
1805347a1: 75 05 jne 0x1805347a8
1805347a3: 0f 11 02 movups %xmm0,(%rdx)
1805347a6: eb 08 jmp 0x1805347b0
1805347a8: 0f 11 02 movups %xmm0,(%rdx)
1805347ab: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
1805347b0: 4c 89 08 mov %r9,(%rax)
1805347b3: 49 ba 00 00 00 00 00 movabs $0x3ff0000000000000,%r10
1805347ba: 00 f0 3f
1805347bd: 4d 89 10 mov %r10,(%r8)
1805347c0: 4c 8d 41 48 lea 0x48(%rcx),%r8
1805347c4: 49 8b c0 mov %r8,%rax
1805347c7: f2 0f 11 4c 24 08 movsd %xmm1,0x8(%rsp)
1805347cd: f2 0f 10 44 24 08 movsd 0x8(%rsp),%xmm0
1805347d3: 48 83 c0 10 add $0x10,%rax
1805347d7: 4c 89 12 mov %r10,(%rdx)
1805347da: 66 0f c6 c0 00 shufpd $0x0,%xmm0,%xmm0
1805347df: 41 f6 c0 0f test $0xf,%r8b
1805347e3: 75 06 jne 0x1805347eb
1805347e5: 41 0f 11 00 movups %xmm0,(%r8)
1805347e9: eb 05 jmp 0x1805347f0
1805347eb: 41 0f 11 00 movups %xmm0,(%r8)
1805347ef: 90 nop
1805347f0: 48 8d 51 60 lea 0x60(%rcx),%rdx
1805347f4: 4c 89 08 mov %r9,(%rax)
1805347f7: 48 8b c2 mov %rdx,%rax
1805347fa: f2 0f 11 4c 24 08 movsd %xmm1,0x8(%rsp)
+234
View File
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
"""fnwatch.py — hardware data-watchpoint (DR0 RW) on the live FIR kernel word.
Catches whoever READS the built kernel during offline render -> consumer RIP."""
import ctypes
import glob
import os
import signal
import struct
import subprocess
import sys
import time
DUR = float(os.environ.get('WATCH_DUR', '20'))
BIN = '/home/m/re-tools/soothe_mem.bin'
BASE = 0x180000000
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
DR0_OFF = 0x350 # empirically verified debugreg base
DR7_OFF = DR0_OFF + 56 # 0x388
DR7_VAL = 0x000F0001 # L0=1, R/W0=11 (rd/wr), LEN0=11 (4 bytes)
libc = ctypes.CDLL('libc.so.6', use_errno=True)
_mod = open(BIN, 'rb').read()
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)
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
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
# find ctx & FIR ptr while running (poll till instance exists)
ctx = firptr = None
vt = struct.pack('<Q', 0x1824AC210)
while time.time() - t0 < 25 and firptr is None:
for line in open(f'/proc/{host}/maps'):
parts = line.split()
if 'rw' not in parts[1]:
continue
lo, hi = (int(x, 16) for x in parts[0].split('-'))
CH = 16 * 1024 * 1024
a = lo
while a < hi and firptr is None:
d = rd(a, min(CH + 4096, hi - a))
if not d:
break
j = d.find(vt)
while j >= 0:
cand = a + j
sb = rd(cand + 0x540870, 4)
if sb and struct.unpack('<f', sb)[0] > 100:
ctx = cand
qb = rd(cand + 0x540668, 8)
firptr = struct.unpack('<Q', qb)[0]
break
j = d.find(vt, j + 1)
a += CH
if firptr is None:
print('NO CTX/FIR')
return 1
print('host %d ctx %#x fir %#x at %.2fs' % (host, ctx, firptr, time.time() - t0), flush=True)
seized = {host}
armed = set()
pt(PTRACE_SEIZE, host, 0, PTRACE_O_TRACECLONE)
pt(PTRACE_INTERRUPT, host)
for _ in range(60):
try:
pid, st = os.waitpid(host, os.WUNTRACED | os.WNOHANG)
except ChildProcessError:
break
if pid == host:
break
time.sleep(0.001)
pt(PTRACE_POKEUSER, host, DR0_OFF, 0x18052b8bb)
pt(PTRACE_POKEUSER, host, DR7_OFF, DR7_VAL)
armed.add(host)
print('placeholder armed', flush=True)
def arm_dbg(tid):
pass_off=(0x350,)
for off in pass_off:
r = pt(PTRACE_POKEUSER, tid, off, 0xdeadbeef if off == DR0_OFF else DR7_VAL)
v = pt(PTRACE_PEEKUSER, tid, off)
print(' dbg poke %#x -> r=%s read=%#x' % (off, 'ok' if r is not None else 'EIO', v or 0), flush=True)
def arm(tid):
pt(PTRACE_POKEUSER, tid, DR0_OFF, firptr + 43 * 8) # bin43 re (hot word)
pt(PTRACE_POKEUSER, tid, DR7_OFF, DR7_VAL)
v = pt(PTRACE_PEEKUSER, tid, DR7_OFF)
# bit10 of DR7 reads as always-1 (RA1)
return v is not None and (v & ~0x400) == (DR7_VAL & ~0x400)
# stop host briefly to verify arming works at all
pt(PTRACE_INTERRUPT, host)
for _ in range(50):
try:
pid, st = os.waitpid(host, os.WUNTRACED | os.WNOHANG)
except ChildProcessError:
break
if pid == host:
break
time.sleep(0.002)
arm_dbg(host)
ok = arm(host)
print('arm check (stopped):', ok, flush=True)
pt(PTRACE_CONT, host, 0, 0)
if ok:
armed.add(host)
hits = []
t_end = time.time() + DUR
last_sweep = 0.0
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, 0) is not None:
seized.add(tid)
if tid not in armed:
pt(PTRACE_INTERRUPT, tid)
for _ in range(40):
try:
pid, st = os.waitpid(tid, os.WUNTRACED | os.WNOHANG)
except ChildProcessError:
break
if pid == 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
rec = dict(rip=regs.rip, rsp=regs.rsp, tid=pid,
ret=struct.unpack('<Q', rd(regs.rsp, 8) or b'\0' * 8)[0])
hits.append(rec)
if len(hits) <= 15:
print('WATCH HIT rip=%#x ret=%#x' % (rec['rip'], rec['ret']), flush=True)
# pass trap: clear DR6 (poke 0xffff0ff0), single-step, continue
pt(PTRACE_POKEUSER, pid, DR0_OFF + 48, 0xFFFF0FF0) # reset DR6
regs.eflags |= 0x100
pt(PTRACE_SETREGS, pid, 0, ctypes.addressof(regs))
pt(PTRACE_SINGLESTEP, pid, 0, 0)
try:
os.waitpid(pid, os.WUNTRACED)
except ChildProcessError:
pass
pt(PTRACE_CONT, pid, 0, 0)
elif os.WIFSTOPPED(pid):
pt(PTRACE_CONT, pid, 0, sig if 0 < sig < 32 else 0)
print('total watch hits:', len(hits))
import json
json.dump(hits, open('/tmp/opencode/fnwatch/hits.json', 'w'), indent=1, default=str)
proc.kill()
return 0
if __name__ == '__main__':
os.makedirs('/tmp/opencode/fnwatch', exist_ok=True)
sys.exit(main())