fix(core): make function-call patches survive a re-resolve - #24
Open
Rushaway wants to merge 1 commit into
Open
Conversation
The anchor pattern added in #14 ends on the CALL rel32 that the patch NOPs out, and that call site is one of the sites the occurrence loop overwrites. The patch therefore erases its own signature: every later resolve in the same srcds process returns functionAddress = 0, logs "Could not find patch signature" and, through bSuccess, makes SDK_OnLoad() call SDK_OnUnload() and return false -- taking every other fix in the extension down with it. That is the failure from #13 that #14 set out to fix, now deterministic. When the pattern is not found, search again with the trailing five bytes replaced by NOPs. A match there means the patch is already applied, so log it and skip instead of failing the load. Also in the same code path: - guard ofs >= range before "pPatch->range - ofs". range is int and ofs is uintptr_t, so the subtraction is unsigned and wraps to ~4 GB as soon as a match lands in the last bytes of the range, after which the scan runs past the function and NOPs call sites in unrelated engine code - break instead of continue when a patch signature is not found. ofs is unchanged, so continue re-ran the identical search and logged the identical error occurrences (100) times - report which step failed instead of leaving functionAddress at 0 silently - use a constant 5 for a CALL rel32 instead of strlen() on pPatch, which is binary data, not a string. A 0x00 patch byte would truncate PatchLen for both the write and the restore - clear pPatch->pRestore after freeing the list so the head does not dangle - read the rel32 as int32_t so the arithmetic also holds on 64-bit (#20) Closes #23 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Fixes #23.
The regression
The anchor pattern introduced in #14 ends on the
CALL rel32the patch NOPs out:That trailing
E8 rel32is what identifies the callee — and it is also one of the call sites the occurrence loop overwrites with90 90 90 90 90. The patch erases its own signature. Once applied, the pattern can never match again in that process, so every later resolve yieldsfunctionAddress = 0→→
bSuccess = false→SDK_OnLoad()callsSDK_OnUnload()and returnsfalse→ the whole extension fails to load, losing all ~19 patches, the detours and the hooks. With #10/#11 gone, packet spam makes the engine floodConMsg/Msgagain, which is the server lag we were chasing.This is #13's failure, now deterministic rather than intermittent.
The fix
When the pattern is not found, search again with the trailing five bytes replaced by NOPs. A match there means the patch is already applied → log and skip, instead of failing the load of everything else.
FindFunctionAddressByPattern(StartAddr, MaxSize)becomesFindFunctionAddressByCall(CallAddr): it takes the call address directly instead of re-deriving it from the mask length, which also removes the unchecked assumption thatstrlen(pPatchPattern) == sizeof(pPatchSignature) - 1.Also fixed in the same code path
pPatch->range - ofswrapsrangeisint,ofsisuintptr_t→ unsigned subtraction.FindFunctionCall()can return a match atBaseAddr + MaxSize - 1, makingofs=range + 4; the next scan then covers0xfffffffcbytes and NOPs call sites in unrelated engine code. Guarded withif (ofs >= (uintptr_t)pPatch->range) break;continue→breakofsis unchanged, socontinuere-ran the identical search: a failing #10/#11 logged 100 identical errors and did 100 full-range scansCALL rel32strlen()on binary dataPatchLenfor a function-call patch is now the constant5.strlen(pPatch)only returned 5 by luck; a0x00patch byte would truncate it for both the write and the restore inSDK_OnUnload()pPatch->pRestoreint32_t Offsetinstead ofuint32_t offsetread through anint32_t *, so the arithmetic also holds for #20No patch signature, range or occurrence count is changed — behaviour on a clean engine image is identical.
Testing
No CS:S server here, so I verified the logic offline:
FindPattern,FindFunctionCall,FindFunctionAddressByCall,FindPatchAnchorCalland the oldFindFunctionAddressByPatternwere extracted verbatim fromsrc/extension.cppinto a standalone harness, built-m32(i386, as srcds runs), and driven with patch #10's real signature and mask against a synthetic function containing severalCALL rel32sites:Still needs a run on a real server to confirm the patches apply as before on a clean load and that the lag is gone. I have not been able to test the
already appliedpath against a live engine.Not addressed here
Discussed in #23 — a single stale signature still makes
SDK_OnLoad()returnfalseand takes the entire extension down. Anoptional/requiredflag per patch would turn that into a warning for non-critical patches, but it is a behaviour change that deserves its own PR.🤖 Generated with Claude Code