Fix dispatcher crash/deadlock, instruction analyzer, cache equality, task leak, and redirection protection

- MainThreadDispatcher: guard DispatchHook with try/catch so exceptions never escape to native caller; drain and fault pending work on Dispose; synchronize Execute/ExecuteAsync/Dispose against race/dispose.
- InstructionAnalyzer: require ModRM 0xEC for 0x83/0x81 sub-esp/rsp forms, rejecting unsafe RIP-relative or memory forms.
- PatternScannerCache: implement value equality on CacheKey so repeated scans actually hit cache.
- BackgroundTaskExecutor: add remote allocations to the free list immediately after VirtualAllocEx, before any write that could fail and leak.
- redirect: capture and restore original page protection in Apply/Remove instead of leaving target RWX.
- Regression tests for all six fixes.

Tests: 198 passing, 4 integration/interactive skipped.
This commit is contained in:
kbe
2026-07-22 00:32:19 +02:00
parent 3f0bea6bd4
commit 44a368de9c
9 changed files with 301 additions and 43 deletions
+34 -11
View File
@@ -117,28 +117,39 @@ public sealed class Detour : IDisposable
"Failed to write the detour trampoline into the target process.");
}
// Make the target page writable if necessary, then write the detour jump.
// Make the target page writable if necessary, then write the detour jump,
// restoring the original protection regardless of success or failure.
if (!NativeMethods.VirtualProtectEx(
_memory.Handle,
Target,
preserveLength,
MemoryProtectionType.ExecuteReadWrite,
out _))
out MemoryProtectionType oldProtect))
{
int error = Marshal.GetLastPInvokeError();
throw new InvalidOperationException(
$"Failed to change target memory protection: error {error}");
}
written = _memory.WriteBytes(Target, hookJump);
if (written != hookJump.Length)
try
{
throw new InvalidOperationException("Failed to write detour jump to target.");
}
written = _memory.WriteBytes(Target, hookJump);
if (written != hookJump.Length)
throw new InvalidOperationException("Failed to write detour jump to target.");
Trampoline = trampoline;
Original = Marshal.GetDelegateForFunctionPointer(Trampoline, Hook.GetType());
IsApplied = true;
Trampoline = trampoline;
Original = Marshal.GetDelegateForFunctionPointer(Trampoline, Hook.GetType());
IsApplied = true;
}
finally
{
NativeMethods.VirtualProtectEx(
_memory.Handle,
Target,
preserveLength,
oldProtect,
out _);
}
}
catch
{
@@ -164,9 +175,21 @@ public sealed class Detour : IDisposable
Target,
OverwrittenBytes.Length,
MemoryProtectionType.ExecuteReadWrite,
out _);
out MemoryProtectionType oldProtect);
_memory.WriteBytes(Target, OverwrittenBytes);
try
{
_memory.WriteBytes(Target, OverwrittenBytes);
}
finally
{
NativeMethods.VirtualProtectEx(
_memory.Handle,
Target,
OverwrittenBytes.Length,
oldProtect,
out _);
}
}
if (Trampoline != IntPtr.Zero)
+4 -4
View File
@@ -53,12 +53,12 @@ internal static class PrologueDecoder
return i + 2;
}
// sub r/m32/64, imm8.
if (op == 0x83 && bytes.Length > i + 2)
// sub esp/rsp, imm8 — register-direct ModRM 0xEC only.
if (op == 0x83 && bytes.Length > i + 2 && bytes[i + 1] == 0xEC)
return i + 3;
// sub r/m32/64, imm32.
if (op == 0x81 && bytes.Length > i + 5)
// sub esp/rsp, imm32 — register-direct ModRM 0xEC only.
if (op == 0x81 && bytes.Length > i + 5 && bytes[i + 1] == 0xEC)
return i + 6;
return -1;