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
@@ -150,6 +150,19 @@ public sealed class RemoteThreadExecutorTests
Assert.Equal(Environment.Is64BitProcess, reader.Is64Bit);
}
[Fact]
public void Execute_releases_allocated_remote_memory_on_write_failure()
{
using var reader = new WriteFailingMemoryBase();
var executor = new RemoteThreadExecutor(reader);
// The executor will allocate a remote call stub; our reader then refuses every
// WriteBytes call. The allocation made before the failure must still be freed.
// The write failure must surface as an InvalidOperationException, not hang or crash.
Assert.Throws<InvalidOperationException>(() =>
executor.Execute<int>(new IntPtr(0x123456789ABCDEF0L), CallConvention.Stdcall));
}
private static int RunPayload(byte[] payload, CallConvention convention, params object?[] args)
{
const nint pageSize = 4096;
@@ -206,4 +219,26 @@ public sealed class RemoteThreadExecutorTests
{
}
}
/// <summary>
/// A fake reader whose WriteBytes always returns zero, forcing the executor down
/// the failure path after it has allocated remote memory.
/// </summary>
private sealed class WriteFailingMemoryBase : MemoryBase
{
public override IntPtr ImageBase => IntPtr.Zero;
public override SafeMemoryHandle Handle { get; } = new SafeMemoryHandle(new IntPtr(-1));
public override bool Is64Bit => Environment.Is64BitProcess;
public override int ProcessId => Environment.ProcessId;
public override byte[] ReadBytes(IntPtr address, int count, bool isRelative = false)
=> throw new NotSupportedException();
public override int WriteBytes(IntPtr address, ReadOnlySpan<byte> bytes, bool isRelative = false)
=> 0;
public override void Dispose()
{
}
}
}