Files
whitemagic/WhiteMagic/Thread/ThreadFactory.cs
T
kbe 1169fdb994 Address review findings for thread control and process discovery
- Drop the false WOW64 claim from GetContext32/SetContext32 docs and guard them for 32-bit targets only.\n- Make FrozenThread dispose the thread handles it owns; make Freeze(predicate) dispose filtered-out threads.\n- Pass the already-validated handle through GetThreadById instead of opening a second one.\n- Add no-progress guard to MemoryBase.EnumerateRegions.\n- Dispose unmatched Process candidates in ApplicationFinder.OpenProcess.\n- Clean up RemoteThreadExecutor allocation formatting.
2026-07-22 17:18:48 +02:00

265 lines
8.0 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using WhiteMagic.Native;
namespace WhiteMagic.Thread;
/// <summary>
/// Enumerates and selects threads belonging to the target process.
/// </summary>
public sealed class ThreadFactory
{
private readonly MemoryBase _memory;
/// <summary>Creates a factory bound to the target process represented by <paramref name="memory"/>.</summary>
public ThreadFactory(MemoryBase memory)
{
_memory = memory ?? throw new ArgumentNullException(nameof(memory));
}
/// <summary>
/// Enumerates every thread that belongs to the target process.
/// </summary>
public IEnumerable<RemoteThread> Enumerate()
{
foreach (int threadId in CollectThreadIds())
{
SafeMemoryHandle handle = NativeMethods.OpenThread(
ThreadAccess.SuspendResume |
ThreadAccess.GetContext |
ThreadAccess.SetContext |
ThreadAccess.QueryInformation,
false,
threadId);
if (handle.IsInvalid)
continue;
yield return new RemoteThread(_memory, threadId, handle);
}
}
private int[] CollectThreadIds()
{
using SafeMemoryHandle snapshot = NativeMethods.CreateToolhelp32Snapshot(SnapshotFlags.Thread, 0);
if (snapshot.IsInvalid)
{
int error = Marshal.GetLastPInvokeError();
throw new InvalidOperationException($"CreateToolhelp32Snapshot failed: error {error}.");
}
var entry = new ThreadEntry32
{
dwSize = (uint)Marshal.SizeOf<ThreadEntry32>()
};
var ids = new List<int>();
if (!NativeMethods.Thread32First(snapshot, ref entry))
{
int error = Marshal.GetLastPInvokeError();
if (error == 18 || error == 259) // ERROR_NO_MORE_FILES / ERROR_NO_MORE_ITEMS
return ids.ToArray();
throw new InvalidOperationException($"Thread32First failed: error {error}.");
}
do
{
if (entry.th32OwnerProcessID == (uint)_memory.ProcessId)
ids.Add((int)entry.th32ThreadID);
}
while (NativeMethods.Thread32Next(snapshot, ref entry));
return ids.ToArray();
}
/// <summary>
/// Returns the thread with the specified operating-system identifier if it belongs
/// to the target process.
/// </summary>
/// <exception cref="InvalidOperationException">The thread does not belong to the target process.</exception>
public RemoteThread GetThreadById(int threadId)
{
if (threadId <= 0)
throw new ArgumentException("Thread ID must be positive.", nameof(threadId));
const ThreadAccess requiredAccess =
ThreadAccess.SuspendResume |
ThreadAccess.GetContext |
ThreadAccess.SetContext |
ThreadAccess.QueryInformation;
SafeMemoryHandle handle = NativeMethods.OpenThread(requiredAccess, false, threadId);
if (handle.IsInvalid)
{
int error = Marshal.GetLastPInvokeError();
throw new InvalidOperationException($"OpenThread failed for thread {threadId}: error {error}.");
}
try
{
var info = new ThreadBasicInformation();
int status = NativeMethods.NtQueryInformationThread(
handle,
0,
ref info,
(uint)Marshal.SizeOf<ThreadBasicInformation>(),
out _);
if (status < 0)
{
throw new InvalidOperationException(
$"NtQueryInformationThread failed for thread {threadId} (NTSTATUS {status:X8}).");
}
if ((uint)(nint)info.ClientId.UniqueProcess != (uint)_memory.ProcessId)
{
throw new InvalidOperationException(
$"Thread {threadId} does not belong to process {_memory.ProcessId}.");
}
// Ownership of the validated handle transfers to the RemoteThread.
return new RemoteThread(_memory, threadId, handle);
}
catch
{
handle.Dispose();
throw;
}
}
/// <summary>
/// Returns the earliest-created thread of the target process.
/// </summary>
public RemoteThread MainThread
{
get
{
RemoteThread? earliest = null;
long earliestTime = long.MaxValue;
foreach (RemoteThread thread in Enumerate())
{
long creationTime = GetCreationTime(thread.Id);
if (creationTime < earliestTime)
{
earliestTime = creationTime;
earliest?.Dispose();
earliest = thread;
}
else
{
thread.Dispose();
}
}
if (earliest is null)
{
throw new InvalidOperationException(
$"Process {_memory.ProcessId} has no observable threads.");
}
return earliest;
}
}
/// <summary>
/// Suspends the supplied threads and returns a disposable scope that resumes exactly
/// those threads when disposed, including when an exception escapes the guarded body.
/// </summary>
/// <remarks>
/// Do not freeze the target's threads while executing target code through a remote
/// thread or main-thread pump; doing so can deadlock because the frozen thread is the
/// one responsible for running the code.
/// </remarks>
public FrozenThread Freeze(IEnumerable<RemoteThread> threads)
{
ArgumentNullException.ThrowIfNull(threads);
var suspended = new List<RemoteThread>();
try
{
foreach (RemoteThread thread in threads)
{
thread.Suspend();
suspended.Add(thread);
}
return new FrozenThread(suspended);
}
catch
{
foreach (RemoteThread thread in suspended)
{
try
{
thread.Resume();
}
catch
{
// Best-effort unwind.
}
}
throw;
}
}
/// <summary>
/// Suspends all target threads selected by <paramref name="predicate"/>.
/// </summary>
public FrozenThread Freeze(Func<RemoteThread, bool> predicate)
{
ArgumentNullException.ThrowIfNull(predicate);
var selected = new List<RemoteThread>();
try
{
foreach (RemoteThread thread in Enumerate())
{
try
{
if (predicate(thread))
selected.Add(thread);
else
thread.Dispose();
}
catch
{
thread.Dispose();
throw;
}
}
return Freeze(selected);
}
catch
{
foreach (RemoteThread thread in selected)
thread.Dispose();
throw;
}
}
private long GetCreationTime(int threadId)
{
using SafeMemoryHandle handle = NativeMethods.OpenThread(ThreadAccess.QueryInformation, false, threadId);
if (handle.IsInvalid)
{
int error = Marshal.GetLastPInvokeError();
throw new InvalidOperationException($"OpenThread failed for thread {threadId}: error {error}.");
}
if (!NativeMethods.GetThreadTimes(handle, out long creationTime, out _, out _, out _))
{
int error = Marshal.GetLastPInvokeError();
throw new InvalidOperationException($"GetThreadTimes failed for thread {threadId}: error {error}.");
}
return creationTime;
}
}