ExpandedCrashLogs: fix writeLine call convention (restores minidump generation) - #77
Open
thesammy58 wants to merge 2 commits into
Open
ExpandedCrashLogs: fix writeLine call convention (restores minidump generation)#77thesammy58 wants to merge 2 commits into
thesammy58 wants to merge 2 commits into
Conversation
Fixes both the crash inside the hook and the missing native minidumps. This is the root cause; the SEH hardening in the following commit is not what restores dump generation. The game's crash-log `writeLine` (vtable slot 0x20) takes two arguments, not one. Every call-site in TS3.exe pushes two and performs no stack clean-up afterwards, so it is a two-argument __thiscall that returns with `ret 8` (0x004d6042, 0x004d6d61 and 0x004d61f0 on the EA build). By contrast `openSection` at 0x18 and `closeSection` at 0x1C really do take one argument -- 0x004d6024 and 0x004d6055 push one and likewise do not clean up -- so those call-sites were already correct. Calling `writeLine` through the one-argument `AcceptString` signature made it pop four bytes more than we pushed. ESP was left skewed for the rest of `WriteS3SSSectionsInCrashLog`, whose epilogue restores ebx/esi/edi with `pop`, so they came back from the wrong stack slots. The hook keeps `this` in esi across that call and never reloads it, which produced the observed `this->vtable == 0x8BF18B56`, which is a value that decodes as x86 code bytes, i.e. a stale .text address recovered from a wrong slot. /GS does not catch this: the cookie is read via [ebp-0x1c], which is unaffected by ESP drift. The resulting fault killed the process before the game's exception handler reached the minidump writer: 0x004d6f6c call 0x004d6cb0 ; write the xcpt...txt sections <- here 0x004d6f86 call 0x004d6ad0 ; DbgHelp!MiniDumpWriteDump Verified against a repeatable third-party startup crash with an identical code path on every run: with this patch built from upstream and no other change, the crash produces both the expanded xcpt...txt and an xcpt...mdmp; without it, the .mdmp is absent. Post-mortem capture of the unfixed build faults at `Sims3SettingsSetter+0x491c3` with the game's handler on the stack directly below, five instructions short of the minidump call. Still unverified: the flag's meaning. The game passes 1 for ordinary content lines, including the identical "<An exception was encountered...>" message at 0x004d6d61 that this patch mirrors, and 0 for two lines in the section written by 0x004d61f0. 1 is used here, and the S3SS section renders the same way the game's own sections do.
Simply strengthening, not a 'fix'. The preceding commit is what restores minidump generation. Verified: a build carrying only the writeLine arity fix, with this hook left exactly as upstream, still produces native minidumps. The reason to harden this particular call anyway is that it is unusually expensive to get wrong. The call-site we replace (0x004d6ebc on the EA build) is reached with the game's own SEH try-level already reset to -1 at 0x004d6eb1, so nothing above us catches anything, and the handler that called us goes on to write the native minidump in a later call at 0x004d6f86. Any exception escaping here therefore costs the .dmp as well as the two finalizing virtual calls at 0x004d6ec1/0x004d6ec5. One bad dereference in a diagnostic patch like this one ironically takes the rest of the diagnostics with it. The guard covers the whole dispatch rather than only the `this->vtable->unknown5` read. Unpatched, a fault in the chained callee is fatal in exactly the same place and loses the .dmp too, so returning normally is strictly more diagnostics than dying; it costs the tail of the section chain and buys back both the finalized log and the minidump. The trade-off is noted inline; narrow the scope to just the `unknown5` read if that is ever unwanted. Note this is a backstop, not a guarantee: __except catches faults, so it covers a bad read or a jump to unmapped memory, but not a jump into valid-but-wrong code. That's why the preceding arity fix, not this, is the actual repair.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two commits against the
ExpandedCrashLogspatch. Harry is aware of both issues and okayed a PR (with AI-assisted coding for the fix). He wasn't available to weigh in on specifics, so the judgement calls below are mine and I'm happy to be overruled on any of them. Also, feel free to condense the comments if you want (or I can).TL;DR - two problems I experienced turned out to be one bug.
The bug
writeLine— vtable slot0x20on the crash-log object — takes two arguments, not one. Every call site inTS3.exepushes two and performs no stack cleanup afterwards, so it's a two-argument__thiscallending inret 8. At0x004d6042on the EA build (1.69.47.024017):The patch called it through the one-argument
AcceptStringsignature at three sites, so each call popped four bytes more than were pushed.openSection(0x18) andcloseSection(0x1C) genuinely are one-argument.0x004d6024and0x004d6055push one and likewise don't clean up, so those were already right.Why it produced two different symptoms
ESP was left 8 bytes high for the rest of
WriteS3SSSectionsInCrashLog, whose epilogue restoresebx/esi/ediwithpop(ESP-relative, not EBP-relative) so they came back from the wrong slots.HookedEndOfExceptionReportSectionskeepsthisinesiacross that call and never reloads it. That's howthis->vtableended up as0x8BF18B56: a value that decodes as x86 code bytes, i.e. a stale.textaddress pulled from the wrong stack slot./GSdoesn't catch it, because the cookie is read via[ebp-0x1c]and is unaffected by the drift.The resulting fault happens at try-level −1. The game drops it at
0x004d6eb1, immediately before our call site inside the first of two sequential calls in the exception handler:So the process dies before the minidump is written. The "no minidumps while this patch is enabled" issue was never a separate hooking bug, it turns out it's a downstream consequence of the first one.
It's also invisible from inside the crash log: the header is written from the original exception record before our hook runs, and the game swaps its own filter out at
0x004d6f52, so the second fault is never logged. The missing.mdmpis the only symptom. It can't be seen under a live debugger at all either, since the top-level filter isn't called when one is attached. Postmortem capture and debugging was the only way to catch it.Evidence
Sims3SettingsSetter+0x491c3, withTS3+0xd6f71andKERNELBASE!UnhandledExceptionFilterdirectly beneath it, five instructions short of the minidump call.eax = 0x8BF18B56, matching a Visual Studio capture from days earlier exactly, so the corruption is deterministic.About the second commit
The SEH guard is not what fixes the minidumps. I've kept it because the site is unusually expensive to get wrong: it runs outside any game
__tryand gates the minidump write, so one bad dereference in a diagnostic patch takes the rest of the diagnostics down with it.I scoped it to the whole dispatch rather than just the
unknown5read. Unpatched, a fault in the chained callee is fatal in the same place and loses the minidump too, so returning normally is strictly more diagnostics than dying.It's also worth mentioning that it's a backstop, not a guarantee.
__exceptcatches faults, so it covers a bad read or a jump to unmapped memory, but not a jump into valid-but-wrong code. I saw both variants: one capture read the garbage pointer successfully and thenjmp'd to it.Caveats
writeLine's second argument means. The game passes1for ordinary content lines everywhere it writes one, including the identical<An exception was encountered …>message at0x004d6d61that this patch mirrors, and0for two lines in the section at0x004d61f0. I used1here, and the rendered section matches the game's own.