Report native crashes to T5DE

The next start reports a native crash that ended the last run (IMVU's crash.log, this client's own only,
each once), with consent; checked on the build (native_crash).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-11-02 09:17:33 -08:00
1 parent 19a3fc4e80
commit 419303d0f6
3 files changed
+167 -1

No files matched your search

@@ -0,0 +1,97 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: dhkatz <[email protected]>
Date: Fri, 30 Oct 2026 08:16:59 -0700
Subject: [PATCH] Report native crashes to T5DE
A native crash (in IMVU's C++ code, or a renderer) is reported to T5DE the next time the client starts,
with the person's consent, like its Python errors: the exception and the crashing thread's stack
(each call's module and offset), from the crash log IMVU's crash handler leaves in %APPDATA%\IMVU, which
the client never read (its data is in T5DE's folder). Each run records its process ID, so only the
client's own crashes are sent, each once.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
T5de-Id: t5de-native-crashes
T5de-Name: Native Crash Reports
T5de-Group: core
T5de-Required: true
T5de-Depends: t5de-diagnostics
---
library/main/client.py | 50 +++++++++++++++++++++++++++++++++++++++
library/main/clientapp.py | 3 +++
2 files changed, 53 insertions(+)
diff --git a/library/main/client.py b/library/main/client.py
index 9cad8bcf9f775a3ad5b3fa0fbad9b4534e215918..e59ee3f55843e2967d1f5919bd2c17971af292e5 100644
--- a/library/main/client.py
+++ b/library/main/client.py
@@ -96,6 +96,56 @@ def t5deReport(excInfo, kind):
except Exception:
pass
+def t5deNativeCrash():
+ """Report the native crash that ended this client's last run, if there was one. IMVU's crash handler writes
+ %APPDATA%/IMVU/crash.log, which the client itself never reads (its data is in T5DE's folder). Only the exception
+ and the crashing thread's stack (module and offset) are sent. Each run records its process ID, so only this
+ client's own crashes are sent (not IMVU's, which writes there too), and each once."""
+ try:
+ import json, os, re, tempfile, time
+ import imvu.fs.windows
+ import imvu.version
+ record = os.path.join(imvu.fs.windows.getUserDirectory(), 't5de_last_run.json')
+ try:
+ with open(record) as f:
+ last = json.load(f)
+ except Exception:
+ last = {}
+ with open(record, 'w') as f:
+ json.dump({'pid': os.getpid(), 'start': time.time()}, f)
+ log = os.path.join(os.environ.get('APPDATA', ''), 'IMVU', 'crash.log')
+ if not last.get('pid') or not os.path.isfile(log) or os.path.getmtime(log) < last.get('start', 0):
+ return
+ with open(log) as f:
+ text = f.read(4000000)
+ pid = re.search(r'Process ID:\s*(\d+)', text)
+ if not pid or int(pid.group(1)) != last['pid']:
+ return
+ code = re.search(r'ExceptionCode:\s*(0x[0-9a-fA-F]+)', text)
+ code = code.group(1).lower() if code else 'unknown'
+ frames = []
+ crashing = text.split('CRASHING THREAD:', 1)
+ if len(crashing) == 2:
+ for line in re.split(r'\n\s*\n', crashing[1], 1)[0].splitlines():
+ m = re.match(r'\s*\(([^#!,()]+?)(?:#[0-9a-fA-F]+)?!([0-9a-fA-F]+)', line)
+ if m:
+ frames.append({'file': m.group(1), 'function': '+0x' + m.group(2).lower(), 'line': 0})
+ names = {'0xc0000005': 'access violation', '0xc00000fd': 'stack overflow', '0xc0000409': 'stack buffer overrun',
+ '0xe06d7363': 'C++ exception', '0x80000003': 'breakpoint', '0xc000001d': 'illegal instruction'}
+ # The log lists the outermost call first. The crash is named after the innermost module that isn't
+ # Windows' or the C runtime's (which only raise it).
+ system = re.compile(r'^(ntdll|kernelbase|kernel32|msvcr\d*|msvcp\d*|ucrtbase|vcruntime\d*)\.dll$', re.I)
+ own = [f['file'] for f in frames if not system.match(f['file'])]
+ where = own[-1] if own else (frames[-1]['file'] if frames else 'unknown module')
+ report = {'platform': 'native', 'type': 'NativeCrash', 'value': 'exception %s (%s) in %s' % (code, names.get(code, 'unknown'), where), 'frames': frames[:100], 'tags': {'kind': 'native', 'imvu': imvu.version.__version__}}
+ fd, path = tempfile.mkstemp(prefix='t5de-report-', suffix='.json')
+ os.write(fd, json.dumps(report))
+ os.close(fd)
+ t5deHelper('report', path)
+ except Exception:
+ pass
+
+
def t5deUpdateFile():
import os, tempfile
return os.path.join(tempfile.gettempdir(), 't5de-update.json')
diff --git a/library/main/clientapp.py b/library/main/clientapp.py
index 4a1778d5aaf11f5cc6502c30ec81ac46fbf5c654..9807b988b17cd867994fbcac8ff74f6f528aecf1 100644
--- a/library/main/clientapp.py
+++ b/library/main/clientapp.py
@@ -714,6 +714,9 @@ def reportUnrecoverableError(serviceProvider, userAccount, dialogFactory):
def runMain(argv=sys.argv):
from main import client
client.t5deTrack('launch')
+ # T5DE: a native crash ending the last run, to T5DE (with consent). Here, not at login: ClientApp binds
+ # maybeReportUncleanShutdownAfterLogin as a default argument, which a patch can't reach.
+ client.t5deNativeCrash()
return _t5de_original_runMain(argv)
+69
View File
@@ -339,5 +339,74 @@ def proxy():
return not wrong, '; '.join(wrong) or 'requests, errors, post_multipart, IMQ and fallback as expected'
# The shape of IMVU's native crash log (%APPDATA%\IMVU\crash.log), cut down: outermost call first.
CRASH_LOG = '''-------------------------------------------------
IMVU CRASH LOG
PROCESS INFO:
Process ID: 4242
Main thread ID: 0x0000307c
EXCEPTION:
ExceptionRecord: 0x00cfcea0
ExceptionCode: 0xc0000005 <unknown exception code!>
CRASHING THREAD: 0x0000307c()
Priority: 0
Stack:
(PYTHON27.DLL#7e2905e5022c4e4b8281138443d73afa3!00123bbd, pc=72a43bbd)
(SceneWindow.dll#4322532c99964c0aa698af609c8076181!000a9d7d, pc=718b9d7d)
(MSVCR100.dll#da7b3ed8d4af49efa87d61651bd1d6492!0002872d, pc=72bf872d)
THREAD: 0x00001bc8()
Stack:
(ntdll.dll#dbc4b3053c0f3189895b2be009a0d2a41!0006c6e1, pc=7757c6e1)
'''
def native_crash():
root = tempfile.mkdtemp()
try:
os.makedirs(os.path.join(root, 'IMVU'))
os.makedirs(os.path.join(root, 'T5DE'))
log = os.path.join(root, 'IMVU', 'crash.log')
with open(log, 'w') as f:
f.write(CRASH_LOG)
os.environ['APPDATA'] = root
imvu = types.ModuleType('imvu')
imvu.fs = types.ModuleType('imvu.fs')
imvu.fs.windows = types.ModuleType('imvu.fs.windows')
imvu.fs.windows.getUserDirectory = lambda: os.path.join(root, 'T5DE')
imvu.version = types.ModuleType('imvu.version')
imvu.version.__version__ = '554.8'
sys.modules.update({'imvu': imvu, 'imvu.fs': imvu.fs, 'imvu.fs.windows': imvu.fs.windows,
'imvu.version': imvu.version})
client = _no_proxy()
sent = []
client.t5deHelper = lambda *args: sent.append(json.load(open(args[1])))
record = os.path.join(root, 'T5DE', 't5de_last_run.json')
def last_run(pid):
with open(record, 'w') as f:
json.dump({'pid': pid, 'start': os.path.getmtime(log) - 60}, f)
last_run(4241) # another process's crash (IMVU's own client writes there too): not sent
client.t5deNativeCrash()
unsent = len(sent)
last_run(4242) # the last run's: sent once
client.t5deNativeCrash()
client.t5deNativeCrash()
want = {'platform': 'native', 'type': 'NativeCrash',
'value': 'exception 0xc0000005 (access violation) in SceneWindow.dll',
'frames': [{'file': 'PYTHON27.DLL', 'function': '+0x00123bbd', 'line': 0},
{'file': 'SceneWindow.dll', 'function': '+0x000a9d7d', 'line': 0},
{'file': 'MSVCR100.dll', 'function': '+0x0002872d', 'line': 0}],
'tags': {'kind': 'native', 'imvu': '554.8'}}
ok = unsent == 0 and sent == [want]
return ok, 'one report of the last run\'s crash' if ok else 'sent %r' % sent
finally:
shutil.rmtree(root, ignore_errors=True)
ok, detail = globals()[CHECK]()
print(json.dumps({'ok': ok, 'detail': detail}))
+1 -1
View File
@@ -23,7 +23,7 @@ def manifest(project):
@pytest.mark.parametrize("check", ["embeds_originals", "devicefingerprint", "prefs_key", "user_directory",
"network_original", "network_blocklist", "proxy"])
"network_original", "network_blocklist", "proxy", "native_crash"])
def test_spliced_library(client, run_py27, check):
install, built = client
result = run_py27("check_build.py", install / "library.zip", built / "library.zip", check)