-
Notifications
You must be signed in to change notification settings - Fork 3
Patcher
Garry's Mod has a few known bugs in its physics code that can crash or freeze the server.
The plugin ships with small fixes for these and applies them automatically when it loads, so there's nothing to set up.
The patch system is a compiled-in registry with byte verification and drift tracking: every fix is tied to the exact game code it repairs.
If a Garry's Mod update changes that code, the fix simply doesn't apply - the plugin never writes over code it doesn't recognize, so a fix that's no longer valid can't cause new problems.
Patches are compiled for both Linux x86 and x64 servers.
They only exist in server builds, never the client.
Tip
Every crash report's header shows how many fixes were applied versus skipped (e.g. 3 applied, 1 drifted), so you always know what's actually running.
See Reading Reports.
crashcapture.patches() -- list every fix and its state
crashcapture.patch("gm.phys.contact_stale_core", false) -- turn one off
crashcapture.patch("gm.phys.contact_stale_core") -- turn it back onYour choice is saved to crashes/patches.txt and remembered on the next start.
CRASHCAPTURE_PATCHES=0 turns every fix off, regardless of the file.
crashcapture.patch() returns three values:
-
ok- whether the change was accepted. -
status- what happens next:-
"queued"- takes effect shortly, at the next frame boundary. -
"restart"- applies on the next server start (most fixes, because rewriting code while it's running is unsafe). -
"unknown"- an invalid id. -
"blocked"- patches are turned off.
-
-
saved- whether your choice was written to disk.-
falsemeans it only lasts this session, usually because the reports folder isn't writable.
-
Each patch targets one code site, resolved symbol-first with an IDA-style signature fallback (see Signatures). Before anything is written:
- The site is located (by symbol if the build has one, otherwise by scanning for the expected bytes).
- The bytes at the site are verified against the expected pattern.
- Only on an exact match is the fix written.
Depending on the outcome, a patch ends up in one of these states:
| State | Meaning |
|---|---|
disabled |
Off via config, the state file, or its default. |
unresolved |
The site's symbol/signature found nothing in this build. |
drifted |
The site was found, but the expected bytes differ - the engine code probably changed (often meaning it was fixed upstream). |
failed |
The memory protection or the write itself was refused. |
unsupported |
The patch kind is not implemented in this build. |
applied |
Resolved, verified, written. |
A drifted or unresolved fix is skipped, never forced, so an outdated fix can't corrupt a newer engine.
Patches are compiled in as a CCPatch table, following fields:
-
id- stable, namespaced id used bycrashcapture.patch(), e.g."gm.phys.mindist_null_edge". -
upstream- tracking note for when the bug gets fixed upstream. -
note- one-line description of what the fix does. -
kind-CC_PATCH_BYTES(replace bytes in place),CC_PATCH_DATA,CC_PATCH_DETOUR(redirect to a C++ handler, keeping the original reachable through a trampoline), orCC_PATCH_VFUNC. -
site- a signature target: module, symbol (tried first), IDA pattern fallback, and resolve steps. -
offset- the patched window issite + offset. -
expect/mask/bytes/len- up to 24 bytes: what must be there (expect,maskmarks which positions must match), and what replaces it. -
detour/trampoline- forCC_PATCH_DETOUR: the C++ handler and the out-pointer receiving the original address. -
default_on- whether the fix ships enabled. -
hot_safe- the site is unreachable from non-game threads, so a live toggle cannot tear it.- Most physics fixes are not hot safe and therefore apply on restart.
-
keep_wildcards- leave mask-wildcard positions holding their original byte.
Example of a bytes patch, shortened from the real registry:
static const CCPatch kPatches[] = {
{
"gm.phys.oo_collision_hash_index",
"gmod IVP OO-watcher collision-hash sign-extension",
"read the 16-bit collision-hash slot with movzx, not movsx (an index >= 0x8000 went negative)",
CC_PATCH_BYTES,
{"patch.oo_hash_index", "vphysics", NULL,
"0F BF 38 89 FE 66 83 FF FF",
{{CC_STEP_END, 0, 0}}},
0,
{0x0F,0xBF,0x38,0x89,0xFE,0x66,0x83,0xFF,0xFF},
{1,1,1,1,1,1,1,1,1},
{0x0F,0xB7,0x38,0x89,0xFE,0x66,0x83,0xFF,0xFF},
9,
NULL, NULL,
true, false, false,
},
};The registry is registered and applied at load:
CrashCapture::Patch::Register(kPatches, ARRAYSIZE(kPatches));
CrashCapture::Patch::Init();The main entry points of the Patch namespace:
Patch::Register(const CCPatch* patches, int count): voidPatch::Init(): voidPatch::Queue(const char* id, bool on, bool* persisted): CCPatchTogglePatch::Enabled(const char* id): boolPatch::Count(): intPatch::GetInfo(int index, CCPatchInfo* out): boolPatch::LoadStateFile(): voidPatch::WriteStateFile(): bool
These fixes are compiled in and applied automatically. The list below is the current registry.
| id | default | arch | what it fixes |
|---|---|---|---|
gm.phys.contact_stale_core |
on | x86, x64 | Stops a crash when physics objects are destroyed while still in use (skips reset_freeze_check_values when core->environment is null during teardown). |
gm.phys.mindist_null_edge |
on | x86, x64 | Stops a crash when a physics contact record points at removed geometry or a NaN-position object. |
gm.phys.mindist_stale_ff |
on | x86, x64 | Stops a crash when a retained mindist references a stale or NaN-position object (p_minimize_FF). |
gm.phys.friction_stale_mindist |
on | x86, x64 | Stops a crash when managed friction is generated from a stale or NaN-position object. |
gm.phys.oo_collision_hash_index |
on | x86, x64 | Stops a crash from a sign-extension bug reading the collision-hash slot index (movsx -> movzx; an index >= 0x8000 went negative). |
gm.phys.oo_collision_hash_swap |
on | x86, x64 | Second movsx -> movzx site on the collision-hash swap-path probe. |
gm.phys.watcher_stale_mindist |
on | x86 only | Stops a crash when a physics record is removed twice (x86 only: its x64 prologue can't be safely detoured). |
gm.phys.ovtree_hash_remove |
on | x86, x64 | Stops a crash when an object is removed from a physics list twice. |
gm.phys.vhash_remove_null |
on | x86, x64 |
IVP_VHash::remove_elem returns early on an absent key instead of raising the not-found fatal. |
gm.phys.minlist_walk_bound_a |
on | x86, x64 | Stops a freeze where physics scheduling gets stuck in a loop (out-of-range add() chain link treated as end-of-chain, first walk site). |
gm.phys.minlist_walk_bound_b |
on | x86, x64 | Same fix, second insertion-walk link check. |
gm.phys.minlist_skip_list |
off | x86, x64 | Disables the physics min-list skip-list (long-jump) optimization outright. |
gm.phys.minlist_replace |
on | x86, x64 | Replaces IVP_U_Min_List::add with a corrected copy of the stock algorithm. |
gm.phys.ctrl_remove_absent |
on | x86, x64 | Stops a crash when a constraint is removed from a physics object that has already been torn down. |
gm.phys.coc_absent_bail |
on | x86, x64 |
ctrl_remove_absent controller removal returns when the controller is absent from the sim unit's list instead of reading before the list array. |
gm.phys.vhash_store_remove_bound |
opt-in | x86 | Bounds IVP_VHash_Store::remove_elem: its find loop has no null-slot break, so an absent key walks off the array. |
gm.phys.friction_hash_init_size |
on | x86 | Creates the per-core friction hash with 16 initial slots instead of 2, cutting rehash+re-add churn as contacts accumulate. |
When an engine update changes the code a fix is tied to, you'll get a notice in the console, and every crash report shows the applied/skipped counts so you always know what's actually running.
Getting started
Usage
Features
For module developers