Adds RemoteThread, ThreadFactory (enumeration, main-thread selection, get-by-id), and FrozenThread scoped freeze. Supports suspend/resume, 32/64-bit context round-trip, TEB query, and reverse-order resume on dispose. Closes section 2 of add-thread-region-finder.
131 lines
3.6 KiB
C#
131 lines
3.6 KiB
C#
using System.Threading;
|
|
using Thread = System.Threading.Thread;
|
|
using WhiteMagic;
|
|
using WhiteMagic.Native;
|
|
using WhiteMagic.Thread;
|
|
using Xunit;
|
|
|
|
namespace WhiteMagicTest.Thread;
|
|
|
|
/// <summary>
|
|
/// Tests for <see cref="RemoteThread"/> open/suspend/resume and context round-trip.
|
|
/// </summary>
|
|
public sealed class RemoteThreadTests
|
|
{
|
|
[Fact]
|
|
public void Open_by_id_succeeds_for_current_thread()
|
|
{
|
|
using var magic = Magic.OpenInProcess();
|
|
int currentId = (int)NativeMethods.GetCurrentThreadId();
|
|
|
|
using var thread = new RemoteThread(magic.Memory, currentId);
|
|
Assert.Equal(currentId, thread.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public void Suspend_returns_prior_count_and_stops_worker()
|
|
{
|
|
using var magic = Magic.OpenInProcess();
|
|
using var cts = new CancellationTokenSource();
|
|
var started = new ManualResetEventSlim(false);
|
|
int osThreadId = 0;
|
|
|
|
var worker = new System.Threading.Thread(() =>
|
|
{
|
|
osThreadId = (int)NativeMethods.GetCurrentThreadId();
|
|
started.Set();
|
|
while (!cts.IsCancellationRequested)
|
|
System.Threading.Thread.Sleep(10);
|
|
});
|
|
|
|
worker.Start();
|
|
started.Wait();
|
|
|
|
try
|
|
{
|
|
using var thread = new RemoteThread(magic.Memory, osThreadId);
|
|
|
|
uint prior = thread.Suspend();
|
|
Assert.True(prior < 0xFFFFFFFF);
|
|
|
|
cts.Cancel();
|
|
// Worker cannot observe cancellation while suspended.
|
|
Assert.False(worker.Join(100));
|
|
|
|
thread.Resume();
|
|
Assert.True(worker.Join(1000));
|
|
}
|
|
finally
|
|
{
|
|
if (worker.IsAlive)
|
|
{
|
|
cts.Cancel();
|
|
using var thread = new RemoteThread(magic.Memory, osThreadId);
|
|
thread.Resume();
|
|
worker.Join(1000);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Resume_restarts_a_suspended_worker()
|
|
{
|
|
using var magic = Magic.OpenInProcess();
|
|
using var cts = new CancellationTokenSource();
|
|
var started = new ManualResetEventSlim(false);
|
|
var resumed = new ManualResetEventSlim(false);
|
|
int osThreadId = 0;
|
|
|
|
var worker = new System.Threading.Thread(() =>
|
|
{
|
|
osThreadId = (int)NativeMethods.GetCurrentThreadId();
|
|
started.Set();
|
|
while (!cts.IsCancellationRequested)
|
|
{
|
|
resumed.Set();
|
|
System.Threading.Thread.Sleep(10);
|
|
}
|
|
});
|
|
|
|
worker.Start();
|
|
started.Wait();
|
|
|
|
try
|
|
{
|
|
using var thread = new RemoteThread(magic.Memory, osThreadId);
|
|
thread.Suspend();
|
|
resumed.Reset();
|
|
|
|
uint prior = thread.Resume();
|
|
Assert.True(prior < 0xFFFFFFFF);
|
|
|
|
// Worker must reach the resumed flag again.
|
|
Assert.True(resumed.Wait(1000));
|
|
cts.Cancel();
|
|
Assert.True(worker.Join(1000));
|
|
}
|
|
finally
|
|
{
|
|
if (worker.IsAlive)
|
|
{
|
|
cts.Cancel();
|
|
using var thread = new RemoteThread(magic.Memory, osThreadId);
|
|
thread.Resume();
|
|
worker.Join(1000);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void GetTeb_returns_managed_teb_for_thread()
|
|
{
|
|
using var magic = Magic.OpenInProcess();
|
|
int currentId = (int)NativeMethods.GetCurrentThreadId();
|
|
|
|
using var thread = new RemoteThread(magic.Memory, currentId);
|
|
using var teb = thread.GetTeb();
|
|
|
|
Assert.NotEqual(IntPtr.Zero, teb.ReadTebAddress());
|
|
}
|
|
}
|