diff --git a/WhiteMagic/Magic.cs b/WhiteMagic/Magic.cs
index d3990d3..0081b38 100644
--- a/WhiteMagic/Magic.cs
+++ b/WhiteMagic/Magic.cs
@@ -1,7 +1,12 @@
+using System.Collections.Generic;
using System.Diagnostics;
using Process = System.Diagnostics.Process;
using WhiteMagic.Execution;
using WhiteMagic.Hooking;
+using WhiteMagic.Memory;
+using WhiteMagic.ProcessDiscovery;
+using WhiteMagic.Thread;
+using WhiteMagic.Windows;
namespace WhiteMagic;
@@ -24,6 +29,21 @@ public sealed class Magic : IDisposable
/// Inline-detour manager (in-process only).
public DetourManager DetourManager => Memory.DetourManager;
+ ///
+ /// Returns the memory region that contains .
+ ///
+ public MemoryRegion QueryRegion(IntPtr address) => Memory.QueryRegion(address);
+
+ ///
+ /// Enumerates the committed and reserved regions of the target process address space.
+ ///
+ public IEnumerable Regions => Memory.EnumerateRegions();
+
+ ///
+ /// Factory for discovering and operating on the target process's threads.
+ ///
+ public ThreadFactory Threads => new ThreadFactory(Memory);
+
private Magic(MemoryBase memory)
{
Memory = memory;
@@ -31,11 +51,38 @@ public sealed class Magic : IDisposable
}
/// Opens an external process for reading, writing, and execution.
- public static Magic Open(System.Diagnostics.Process process)
+ public static Magic Open(Process process)
{
return new Magic(new ExternalReader(process));
}
+ ///
+ /// Opens a target process by its image name. Throws if zero or more than one match.
+ ///
+ public static Magic Open(string processName)
+ {
+ using Process process = ApplicationFinder.OpenProcess(processName);
+ return Open(process);
+ }
+
+ ///
+ /// Opens the process that owns the top-level window with the specified title.
+ ///
+ public static Magic OpenByWindowTitle(string title)
+ {
+ using Process process = ApplicationFinder.OpenByWindowTitle(title);
+ return Open(process);
+ }
+
+ ///
+ /// Opens the process that owns the specified window handle.
+ ///
+ public static Magic OpenByWindowHandle(IntPtr handle)
+ {
+ using Process process = ApplicationFinder.OpenByWindowHandle(handle);
+ return Open(process);
+ }
+
/// Creates an in-process session for the current process.
public static Magic OpenInProcess()
{
diff --git a/WhiteMagic/Process/ApplicationFinder.cs b/WhiteMagic/Process/ApplicationFinder.cs
new file mode 100644
index 0000000..d8a97a4
--- /dev/null
+++ b/WhiteMagic/Process/ApplicationFinder.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.InteropServices;
+using WhiteMagic.Native;
+using WhiteMagic.Windows;
+
+namespace WhiteMagic.ProcessDiscovery;
+
+///
+/// Discovers running processes by name, window title, or window handle so they can be
+/// attached through a session.
+///
+public static class ApplicationFinder
+{
+ ///
+ /// Enumerates processes whose image name matches
+ /// (extension optional).
+ ///
+ public static IEnumerable Enumerate(string processName)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(processName);
+
+ return Process.GetProcessesByName(GetNameWithoutExtension(processName));
+ }
+
+ ///
+ /// Returns the unique process whose image name matches .
+ ///
+ /// Zero or multiple processes match.
+ public static Process OpenProcess(string processName)
+ {
+ Process[] candidates = Enumerate(processName).ToArray();
+
+ if (candidates.Length == 0)
+ {
+ throw new InvalidOperationException(
+ $"No process named '{processName}' was found.");
+ }
+
+ if (candidates.Length > 1)
+ {
+ throw new InvalidOperationException(
+ $"Process name '{processName}' is ambiguous ({candidates.Length} matches): " +
+ string.Join(", ", candidates.Select(p => $"{p.ProcessName}:{p.Id}")));
+ }
+
+ return candidates[0];
+ }
+
+ ///
+ /// Enumerates processes that own a top-level window whose title equals
+ /// .
+ ///
+ public static IEnumerable FindByWindowTitle(string title)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(title);
+
+ var seen = new HashSet();
+ foreach (RemoteWindow window in WindowFactory.GetWindows())
+ {
+ if (!string.Equals(window.Text, title, StringComparison.Ordinal))
+ continue;
+
+ uint pid = window.ProcessId;
+ if (pid == 0 || !seen.Add((int)pid))
+ continue;
+
+ Process? process;
+ try
+ {
+ process = global::System.Diagnostics.Process.GetProcessById((int)pid);
+ }
+ catch
+ {
+ continue;
+ }
+
+ yield return process;
+ }
+ }
+
+ ///
+ /// Returns the unique process that owns a top-level window titled .
+ ///
+ /// Zero or multiple windows match.
+ public static Process OpenByWindowTitle(string title)
+ {
+ Process[] candidates = FindByWindowTitle(title).ToArray();
+
+ if (candidates.Length == 0)
+ {
+ throw new InvalidOperationException(
+ $"No top-level window titled '{title}' was found.");
+ }
+
+ if (candidates.Length > 1)
+ {
+ throw new InvalidOperationException(
+ $"Window title '{title}' is ambiguous ({candidates.Length} matches): " +
+ string.Join(", ", candidates.Select(p => $"{p.ProcessName}:{p.Id}")));
+ }
+
+ return candidates[0];
+ }
+
+ ///
+ /// Returns the process that owns the specified window handle.
+ ///
+ public static Process OpenByWindowHandle(IntPtr handle)
+ {
+ if (handle == IntPtr.Zero)
+ throw new ArgumentException("Window handle cannot be zero.", nameof(handle));
+
+ uint tid = NativeMethods.GetWindowThreadProcessId(handle, out uint processId);
+ if (tid == 0 || processId == 0)
+ {
+ int error = Marshal.GetLastPInvokeError();
+ throw new InvalidOperationException(
+ $"GetWindowThreadProcessId failed for handle {handle:X}: error {error}.");
+ }
+
+ try
+ {
+ return global::System.Diagnostics.Process.GetProcessById((int)processId);
+ }
+ catch (ArgumentException)
+ {
+ throw new InvalidOperationException(
+ $"Process {processId} owning window {handle:X} is no longer running.");
+ }
+ }
+
+ private static string GetNameWithoutExtension(string name)
+ {
+ if (name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
+ return name[..^4];
+
+ return name;
+ }
+}
diff --git a/WhiteMagicTest/MagicFacadeTests.cs b/WhiteMagicTest/MagicFacadeTests.cs
new file mode 100644
index 0000000..b9b35ef
--- /dev/null
+++ b/WhiteMagicTest/MagicFacadeTests.cs
@@ -0,0 +1,44 @@
+using System.Linq;
+using WhiteMagic;
+using WhiteMagic.Memory;
+using WhiteMagic.Native;
+using WhiteMagic.Thread;
+using Xunit;
+
+namespace WhiteMagicTest;
+
+///
+/// Tests for the convenience accessors exposed directly on .
+///
+public sealed class MagicFacadeTests
+{
+ [Fact]
+ public void QueryRegion_returns_region_containing_image_base()
+ {
+ using var magic = Magic.OpenInProcess();
+ MemoryRegion region = magic.QueryRegion(magic.Memory.ImageBase);
+ Assert.True(region.Contains(magic.Memory.ImageBase));
+ }
+
+ [Fact]
+ public void Regions_enumerates_region_containing_image_base()
+ {
+ using var magic = Magic.OpenInProcess();
+
+ bool found = magic.Regions.Any(r => r.Contains(magic.Memory.ImageBase));
+
+ Assert.True(found);
+ }
+
+ [Fact]
+ public void Threads_factory_enumerates_current_thread()
+ {
+ using var magic = Magic.OpenInProcess();
+ ThreadFactory factory = magic.Threads;
+
+ int currentOsId = (int)NativeMethods.GetCurrentThreadId();
+ bool found = factory.Enumerate().Any(t => t.Id == currentOsId);
+
+ Assert.True(found);
+ }
+}
diff --git a/WhiteMagicTest/Process/ApplicationFinderTests.cs b/WhiteMagicTest/Process/ApplicationFinderTests.cs
new file mode 100644
index 0000000..23f9880
--- /dev/null
+++ b/WhiteMagicTest/Process/ApplicationFinderTests.cs
@@ -0,0 +1,105 @@
+using System.Diagnostics;
+using System.Linq;
+using WhiteMagic;
+using WhiteMagic.Native;
+using WhiteMagic.ProcessDiscovery;
+using Xunit;
+
+namespace WhiteMagicTest.ProcessDiscovery;
+
+///
+/// Tests for process discovery via and the matching
+/// overloads.
+///
+public sealed class ApplicationFinderTests
+{
+ [Fact]
+ public void Enumerate_finds_current_process_by_name()
+ {
+ string currentName = Process.GetCurrentProcess().ProcessName;
+
+ Process[] found = ApplicationFinder.Enumerate(currentName).ToArray();
+
+ Assert.True(found.Length >= 1);
+ Assert.Contains(found, p => p.Id == Process.GetCurrentProcess().Id);
+ }
+
+ [Fact]
+ public void Open_by_name_returns_current_process_when_unique()
+ {
+ string currentName = Process.GetCurrentProcess().ProcessName;
+
+ using Process process = ApplicationFinder.OpenProcess(currentName);
+
+ Assert.Equal(Process.GetCurrentProcess().Id, process.Id);
+ }
+
+ [Fact]
+ public void Open_throws_when_name_is_ambiguous()
+ {
+ // Look for a multi-instance system process; skip if the environment is not typical.
+ Process[] candidates = Process.GetProcessesByName("svchost");
+ if (candidates.Length <= 1)
+ {
+ return;
+ }
+
+ InvalidOperationException ex = Assert.Throws(
+ () => ApplicationFinder.OpenProcess("svchost"));
+
+ Assert.Contains("svchost", ex.Message);
+ Assert.Contains("ambiguous", ex.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void Open_throws_when_no_process_matches()
+ {
+ InvalidOperationException ex = Assert.Throws(
+ () => ApplicationFinder.OpenProcess("probably-not-loaded-xyz.exe"));
+
+ Assert.Contains("No process", ex.Message);
+ }
+
+ [Fact]
+ public void OpenByWindowHandle_returns_owning_process()
+ {
+ IntPtr handle = Process.GetCurrentProcess().MainWindowHandle;
+ if (handle == IntPtr.Zero)
+ {
+ return;
+ }
+
+ using Process process = ApplicationFinder.OpenByWindowHandle(handle);
+ Assert.Equal(Process.GetCurrentProcess().Id, process.Id);
+ }
+
+ [Fact]
+ public void OpenByWindowHandle_throws_for_zero_handle()
+ {
+ Assert.Throws("handle", () => ApplicationFinder.OpenByWindowHandle(IntPtr.Zero));
+ }
+
+ [Fact]
+ public void Magic_Open_by_name_attaches_to_current_process()
+ {
+ string currentName = Process.GetCurrentProcess().ProcessName;
+
+ using var magic = Magic.Open(currentName);
+
+ Assert.Equal(Process.GetCurrentProcess().Id, magic.Memory.ProcessId);
+ }
+
+ [Fact]
+ public void Magic_OpenByWindowHandle_attaches_to_owning_process()
+ {
+ IntPtr handle = Process.GetCurrentProcess().MainWindowHandle;
+ if (handle == IntPtr.Zero)
+ {
+ return;
+ }
+
+ using var magic = Magic.OpenByWindowHandle(handle);
+
+ Assert.Equal(Process.GetCurrentProcess().Id, magic.Memory.ProcessId);
+ }
+}
diff --git a/docs/memory-library-comparison.md b/docs/memory-library-comparison.md
index ff5c748..3e06d4b 100644
--- a/docs/memory-library-comparison.md
+++ b/docs/memory-library-comparison.md
@@ -102,5 +102,5 @@ The design held, but building it surfaced corrections worth recording (each is d
- **`RemoteModule`/`RemoteFunction` follow PE export forwarders** — `kernel32!HeapAlloc` → `NTDLL.RtlAllocateHeap` and similar resolve into the real target module; ordinal and API-set forwarders throw `NotSupportedException` rather than returning a wrong address (task 7.2).
- **Detour prologue safety is tiered** — the default `StubAssembler` length-decoder covers only the common x86/x64 prologue shapes and refuses any opcode outside that set (zero dependency); the optional `IcedAssembler.GetPrologueLength` decodes arbitrary prologues and is plugged in via `DetourManager.PrologueLengthResolver` when full validation is wanted (tasks 4.6, 8.3).
- **Iced has no text parser** — the design assumed arbitrary text assembly could be delegated to Iced, but Iced ships only a *fluent* code assembler and a decoder. `IcedAssembler.Assemble` bridges Intel-syntax text onto the fluent API by reflection (registers, immediates, labels; memory operands unsupported), rather than depending on a parser that does not exist (task 8.2).
-- **Injection bitness corrections** — the thread-hijack injector enforces matching host/target bitness, so the 32-bit path always runs from a 32-bit caller and uses native `GetThreadContext`/`SetThreadContext`; the WOW64 context APIs (for 64-bit callers inspecting WOW64 targets) never apply here and were removed. `ExternalReader` validates `QueryInformation`/`QueryLimitedInformation` access and surfaces `IsWow64Process` failures instead of silently assuming host bitness.
+- **Thread-control, memory-region, and process-discovery gaps are closed** — this change adds `MemoryBase.QueryRegion`/`EnumerateRegions`/`ChangeProtection`, `RemoteThread`/`ThreadFactory`/`FrozenThread`, and `ApplicationFinder` with `Magic.Open` overloads. WhiteMagic now covers the public surfaces of all four reference libraries.
- **Bounds and protection hardening** — `AllocatedMemory` range-checks typed IO against region size; `Patch` mirrors the detour's `VirtualProtectEx` dance; `MainThreadPump` guards the completion race on an already-completed `TaskCompletionSource`.
diff --git a/openspec/changes/add-thread-region-finder/.openspec.yaml b/openspec/changes/add-thread-region-finder/.openspec.yaml
new file mode 100644
index 0000000..7250f8f
--- /dev/null
+++ b/openspec/changes/add-thread-region-finder/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-07-22
diff --git a/openspec/changes/add-thread-region-finder/design.md b/openspec/changes/add-thread-region-finder/design.md
new file mode 100644
index 0000000..d0ae63c
--- /dev/null
+++ b/openspec/changes/add-thread-region-finder/design.md
@@ -0,0 +1,86 @@
+## Context
+
+`whitemagic-foundation` shipped the core (dual `MemoryBase`, execution tiers, hooking, injection, discovery, high-level surface). The post-implementation review confirmed parity with GreyMagic and current BlackMagic but flagged three MemorySharp capabilities still absent: public thread control, memory-region query, and process discovery. This change closes those gaps. It is additive; nothing in `whitemagic-foundation` is reworked.
+
+The consuming use case is unchanged — an external automation host over a legacy x86 desktop app — so every surface here must work **out-of-process** over a `SafeMemoryHandle`, and honor target bitness where the OS structures differ (thread `CONTEXT`).
+
+## Goals / Non-Goals
+
+**Goals:**
+- Public thread surface: enumerate the target's threads, suspend/resume, read/write `CONTEXT`, read a thread's TEB, and a **scoped freeze** (`IDisposable`) that suspends a thread set and resumes on dispose even if the body throws.
+- Memory-region surface: query the region containing an address, enumerate all mapped regions, and a **scoped protection change** (`IDisposable`) that restores original protection on dispose.
+- Process discovery: attach a target by process name, window title, or window handle; enumerate candidate processes.
+- Reuse existing `NativeMethods` (`OpenThread`, `Suspend`/`ResumeThread`, `Get`/`SetThreadContext`, `VirtualProtectEx`, `SafeMemoryHandle`) rather than duplicating them.
+- Test-first for pure logic (region-contains math, freeze/dispose ordering, name/handle matching) with live-process integration tests gated on an available target (self-process).
+
+**Non-Goals:**
+- Managed-loader / in-process pump reachability (separate follow-up).
+- Thread creation — `RemoteThreadExecutor` already owns `CreateRemoteThread`; `RemoteThread` here wraps *existing* target threads.
+- Writing to arbitrary regions found by enumeration beyond what `MemoryBase` read/write already offers.
+- Kernel-level or hidden-thread discovery — toolhelp/`NtQueryInformationThread` visibility is sufficient for the automation use case.
+
+## Decisions
+
+### D1: `RemoteThread` wraps an existing target thread over a `SafeMemoryHandle`
+
+`RemoteThread` opens a thread by TID via `OpenThread(THREAD_ALL_ACCESS...)` into a `SafeMemoryHandle` and exposes `Suspend()`/`Resume()`, `GetContext()`/`SetContext()` (Wow64 variant selected by target bitness, mirroring `DllInjector`'s hijack path), `GetTeb()` (via `NtQueryInformationThread`/`ThreadBasicInformation` → `ManagedTeb`), and `Id`. `Suspend`/`Resume` return the prior suspend count so nested suspends are observable.
+
+**Why**: Mirrors the proven bitness handling already in `DllInjector`; keeps thread handles inside a `SafeMemoryHandle` for deterministic cleanup like every other native handle in the library.
+
+**Alternatives**: expose raw `System.Diagnostics.ProcessThread` — rejected: no suspend/resume/context and no soft handle ownership.
+
+### D2: `ThreadFactory` enumerates via toolhelp snapshot
+
+`ThreadFactory.Enumerate()` walks `CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD)` + `Thread32First`/`Thread32Next`, filtering by owning PID, yielding `RemoteThread`. `MainThread` returns the thread with the earliest creation time (via `GetThreadTimes`), matching MemorySharp's definition. `GetThreadById(id)` opens directly.
+
+**Why**: toolhelp is the documented, x86/x64-uniform thread walk and needs no undocumented structures.
+
+**Alternatives**: `NtQuerySystemInformation(SystemProcessInformation)` — rejected: larger undocumented surface for no gain here.
+
+### D3: Scoped freeze is the default ergonomic
+
+`ThreadFactory.Freeze(predicate = all-but-caller?)` suspends the selected threads and returns a `FrozenThread : IDisposable` whose `Dispose()` resumes exactly the threads it suspended, in reverse order. Individual `RemoteThread.Suspend/Resume` remain available for manual control.
+
+**Why**: The dominant use ("freeze the target while I read/write a consistent snapshot") is a scope. An `IDisposable` makes leak-on-exception impossible: `using (factory.Freeze()) { ...edit... }`.
+
+**Trade-off**: Freezing the target's own threads while calling *into* the target (pump/remote-thread) can deadlock. Documented: freeze is for passive read/write snapshots, not while executing target code.
+
+### D4: `MemoryRegion` is an immutable `VirtualQueryEx` snapshot; enumeration is lazy
+
+`MemoryRegion` holds `BaseAddress`, `Size`, `Protection`, `State`, `Type`, `AllocationBase`, `AllocationProtect`, and `Contains(address)`. `MemoryBase.QueryRegion(address)` returns the single region containing an address; `MemoryBase.EnumerateRegions()` yields regions from address 0 upward by repeatedly calling `VirtualQueryEx(base + size)` until it fails (end of address space). Enumeration is `IEnumerable` (lazy) so a caller can stop early.
+
+**Why**: `VirtualQueryEx` already returns contiguous non-overlapping regions; walking `base+size` is the canonical enumeration. Lazy avoids materializing the whole address space.
+
+### D5: Protection change is a scoped, auto-restoring helper
+
+`MemoryBase.ChangeProtection(address, size, newProtect)` calls `VirtualProtectEx`, captures the old protection, and returns a `ProtectionScope : IDisposable` that restores it on dispose. This is the same protect/restore pattern already inlined in `Detour.Apply`; extracting it lets callers guard their own writes: `using (mem.ChangeProtection(a, n, ExecuteReadWrite)) { mem.WriteBytes(a, patch); }`.
+
+**Why**: Removes a foot-gun (leaving a page writable) and de-duplicates the pattern. `Detour`/`Patch` may later adopt it, but that refactor is out of scope here.
+
+### D6: Process discovery via `System.Diagnostics.Process` + Win32 window queries
+
+`ApplicationFinder` wraps `Process.GetProcessesByName`, a `GetWindow`/`EnumWindows` + `GetWindowThreadProcessId` path for window-title/handle attach, and exposes them as `Magic.Open(string processName)`, `Magic.OpenByWindowTitle(string)`, `Magic.OpenByWindowHandle(IntPtr)` overloads plus `ApplicationFinder.Enumerate()`. Ambiguous matches (multiple processes) throw with the candidate list rather than guessing.
+
+**Why**: Managed `Process` covers name/PID; the existing `WindowFactory`/`RemoteWindow` P/Invoke already resolves windows, so window→PID reuses it. Throwing on ambiguity avoids attaching to the wrong instance.
+
+## Risks / Trade-offs
+
+- **Freeze-while-executing deadlock** → Documented non-use; `Freeze` default predicate can exclude the caller's own thread, but cross-process it cannot exclude the *target's* pump thread — caller must not freeze while the pump runs. (D3)
+- **Suspend count skew** → `Suspend`/`Resume` return prior counts; `FrozenThread` tracks exactly what it suspended and resumes only those, so external suspends are not clobbered. (D3)
+- **`VirtualQueryEx` over a 64-bit address space is large** → enumeration is lazy and `IEnumerable`; callers filtering by `State == Commit` or a range stop early. (D4)
+- **Ambiguous process match** → throw with candidates, never auto-pick. (D6)
+- **Bitness of thread `CONTEXT`** → reuse the exact Wow64/native selection already validated in `DllInjector`. (D1)
+
+## Migration Plan
+
+Additive; nothing to migrate. Suggested slices:
+1. **Memory-region** — `MemoryRegion`, `QueryRegion`, `EnumerateRegions`, `ChangeProtection`/`ProtectionScope`. Smallest, unlocks safe writes immediately.
+2. **Thread-control** — `RemoteThread`, `ThreadFactory`, `FrozenThread`.
+3. **Process discovery** — `ApplicationFinder`, `Magic.Open*` overloads.
+
+**Rollback**: remove the new files and the additive `Magic` members; no existing type is modified.
+
+## Open Questions
+
+- **`Freeze` default predicate**: all target threads, or all-but-main? Leaning all-but-none (freeze everything the caller selects; no implicit exclusion cross-process). To confirm during slice 2.
+- **TEB read for a thread**: `NtQueryInformationThread(ThreadBasicInformation)` (undocumented-ish but stable) vs. deriving from `GetThreadContext`. Leaning the former to match `ManagedTeb`'s existing shape.
diff --git a/openspec/changes/add-thread-region-finder/proposal.md b/openspec/changes/add-thread-region-finder/proposal.md
new file mode 100644
index 0000000..4f3d897
--- /dev/null
+++ b/openspec/changes/add-thread-region-finder/proposal.md
@@ -0,0 +1,34 @@
+## Why
+
+The `whitemagic-foundation` review found WhiteMagic is a superset of GreyMagic and current BlackMagic, but **not yet of MemorySharp**. Three genuinely useful capabilities MemorySharp (and, for threads, current BlackMagic's `SThread`) shipped are missing from WhiteMagic:
+
+1. **Thread control** — WhiteMagic calls `SuspendThread`/`ResumeThread` only *internally* inside `DllInjector` thread-hijack. There is no public surface to enumerate a target's threads, suspend/resume them, or **freeze** them for the duration of an edit. Freezing threads is table-stakes for memory editing/trainers (MemorySharp: `ThreadFactory`/`RemoteThread`/`FrozenThread`; BlackMagic: `SThread`).
+2. **Memory-region query** — WhiteMagic changes page protection inline inside `Detour` but exposes no `VirtualQueryEx` region walk, no query-region-at-address, and no reusable scoped protection helper (MemorySharp: `RemoteRegion`/`MemoryProtection`). Callers cannot inspect what is mapped, its protection, or safely flip protection around a write.
+3. **Process discovery** — no way to open a target by name/window/title; the caller must obtain a PID out of band (MemorySharp: `ApplicationFinder`).
+
+These are all **additive, low-risk** surfaces that sit on the existing `MemoryBase`/`SafeMemoryHandle` and native P/Invoke layer. None requires the deferred managed-loader work.
+
+## What Changes
+
+- **Thread control** (new capability `thread-control`): `RemoteThread` (open by id, suspend/resume, get/set context, get TEB, join), `ThreadFactory` (enumerate the target's threads, get main thread, get-by-id), and `FrozenThread`/`Freeze()` returning an `IDisposable` scope that suspends a set of threads and resumes them on dispose.
+- **Memory-region query** (new capability `memory-region`): `MemoryRegion` (a queried `VirtualQueryEx` result — base, size, protection, state, type), region enumeration across the target's address space, query-region-containing-an-address, and a `ChangeProtection(...)` helper returning an `IDisposable` scope that restores the original protection on dispose.
+- **Process discovery** (added to existing capability `high-level-api`): an `ApplicationFinder`/`Magic.Open` overloads to attach by process name, window title, or window handle, plus enumeration of candidate processes.
+
+No behavior of existing WhiteMagic types changes; these are new types plus additive `Magic` facade members and new native imports.
+
+## Capabilities
+
+### New Capabilities
+- `thread-control`: Enumerate, suspend/resume, freeze (scoped), and read/write the context of a target process's threads.
+- `memory-region`: Query and enumerate mapped memory regions (`VirtualQueryEx`) and change page protection through a scoped, auto-restoring helper.
+
+### Modified Capabilities
+- `high-level-api`: Adds process discovery — attach a target by name/window/handle and enumerate candidates.
+
+## Impact
+
+- **New code**: `WhiteMagic/Thread/RemoteThread.cs`, `ThreadFactory.cs`, `FrozenThread.cs`; `WhiteMagic/Memory/MemoryRegion.cs`, `MemoryRegionEnumerator` (or methods on `MemoryBase`), `ProtectionScope`; `WhiteMagic/Process/ApplicationFinder.cs`; additive `Magic` facade members.
+- **New native imports**: `Thread32First`/`Thread32Next` + `CreateToolhelp32Snapshot` (or `NtQueryInformationProcess` thread walk), `VirtualQueryEx`, `MEMORY_BASIC_INFORMATION`. `OpenThread`/`Suspend`/`Resume`/`Get`/`SetThreadContext` already exist in `NativeMethods`.
+- **No dependency change**: pure P/Invoke over the existing core. No FASM, no Iced, no managed loader.
+- **No changes** to BlackMagic/MemorySharp/GreyMagic or their tests.
+- **Platform**: unchanged — bitness-agnostic (x86 + x64); thread context read honors the target's bitness like the existing hijack path.
diff --git a/openspec/changes/add-thread-region-finder/specs/high-level-api/spec.md b/openspec/changes/add-thread-region-finder/specs/high-level-api/spec.md
new file mode 100644
index 0000000..42ee586
--- /dev/null
+++ b/openspec/changes/add-thread-region-finder/specs/high-level-api/spec.md
@@ -0,0 +1,25 @@
+## ADDED Requirements
+
+### Requirement: Process discovery
+
+WhiteMagic SHALL attach to a target process discovered by process name, window title, or window handle, and SHALL enumerate candidate processes. An ambiguous match MUST fail deterministically rather than attaching to an arbitrary candidate.
+
+#### Scenario: open by process name
+- **WHEN** a target is opened by a unique process name
+- **THEN** it MUST attach to that process
+
+#### Scenario: open by window title
+- **WHEN** a target is opened by a window title
+- **THEN** it MUST attach to the process owning the window with that title
+
+#### Scenario: open by window handle
+- **WHEN** a target is opened by a window handle
+- **THEN** it MUST attach to the process that owns that window
+
+#### Scenario: ambiguous match is rejected
+- **WHEN** more than one process matches the given name or title
+- **THEN** the open MUST fail and surface the set of candidate processes rather than picking one
+
+#### Scenario: enumerate candidates
+- **WHEN** candidate processes are enumerated
+- **THEN** the result MUST list the processes eligible to be opened
diff --git a/openspec/changes/add-thread-region-finder/specs/memory-region/spec.md b/openspec/changes/add-thread-region-finder/specs/memory-region/spec.md
new file mode 100644
index 0000000..89e8e47
--- /dev/null
+++ b/openspec/changes/add-thread-region-finder/specs/memory-region/spec.md
@@ -0,0 +1,41 @@
+## ADDED Requirements
+
+### Requirement: Query the region containing an address
+
+WhiteMagic SHALL return the mapped memory region that contains a given address, including its base, size, protection, state, and type.
+
+#### Scenario: query a committed address
+- **WHEN** the region containing a known committed address is queried
+- **THEN** it MUST return a region whose base and size bracket that address and whose protection reflects the page's actual protection
+
+#### Scenario: region membership test
+- **WHEN** a region is asked whether it contains an address
+- **THEN** it MUST return true only for addresses within `[base, base + size)`
+
+### Requirement: Enumerate mapped regions
+
+WhiteMagic SHALL enumerate the mapped memory regions of the target from the lowest address upward, lazily.
+
+#### Scenario: enumeration walks the address space
+- **WHEN** the target's regions are enumerated
+- **THEN** the sequence MUST yield contiguous, non-overlapping regions ascending by base address until the end of the queryable address space
+
+#### Scenario: early stop
+- **WHEN** a caller stops consuming the enumeration after the first match
+- **THEN** enumeration MUST NOT query the entire address space
+
+### Requirement: Scoped protection change
+
+WhiteMagic SHALL change the protection of a region and restore the original protection when the returned scope is disposed.
+
+#### Scenario: protection is applied within the scope
+- **WHEN** a protection-change scope is created for a region with a new protection
+- **THEN** the region's protection MUST be the requested value for the duration of the scope
+
+#### Scenario: protection is restored on dispose
+- **WHEN** the protection-change scope is disposed
+- **THEN** the region's protection MUST be restored to the value it had before the scope was created
+
+#### Scenario: restore on exception
+- **WHEN** the guarded body throws before the scope is disposed
+- **THEN** the original protection MUST still be restored as the scope unwinds
diff --git a/openspec/changes/add-thread-region-finder/specs/thread-control/spec.md b/openspec/changes/add-thread-region-finder/specs/thread-control/spec.md
new file mode 100644
index 0000000..741d6df
--- /dev/null
+++ b/openspec/changes/add-thread-region-finder/specs/thread-control/spec.md
@@ -0,0 +1,57 @@
+## ADDED Requirements
+
+### Requirement: Enumerate target threads
+
+WhiteMagic SHALL enumerate the threads belonging to the target process and expose each as a controllable thread handle.
+
+#### Scenario: enumerate returns the target's threads
+- **WHEN** the threads of an open target are enumerated
+- **THEN** the result MUST contain a handle for each thread owned by the target process and none owned by other processes
+
+#### Scenario: resolve the main thread
+- **WHEN** the main thread is requested
+- **THEN** it MUST return the earliest-created thread of the target process
+
+#### Scenario: get a thread by id
+- **WHEN** a thread is requested by its thread id
+- **THEN** it MUST return a handle bound to that thread, or fail deterministically if the id is not a thread of the target
+
+### Requirement: Suspend and resume a thread
+
+WhiteMagic SHALL suspend and resume an individual target thread and report the prior suspend count.
+
+#### Scenario: suspend increments the suspend count
+- **WHEN** a running thread is suspended
+- **THEN** the thread MUST stop executing and the returned prior suspend count MUST reflect its state before the call
+
+#### Scenario: resume restores execution
+- **WHEN** a previously suspended thread is resumed to a zero suspend count
+- **THEN** the thread MUST resume executing
+
+### Requirement: Read and write thread context
+
+WhiteMagic SHALL read and write a target thread's register context, selecting the context layout that matches the target's bitness.
+
+#### Scenario: round-trip a register value
+- **WHEN** a thread's context is read, a register is modified, and the context is written back
+- **THEN** a subsequent read MUST reflect the modified register value
+
+#### Scenario: bitness-correct context
+- **WHEN** the target is a 32-bit (WOW64) process
+- **THEN** the WOW64 context layout MUST be used, and for a 64-bit target the native layout MUST be used
+
+### Requirement: Scoped thread freeze
+
+WhiteMagic SHALL provide a scoped freeze that suspends a selected set of target threads and resumes exactly those threads when the scope is disposed, including when the guarded body throws.
+
+#### Scenario: freeze suspends selected threads
+- **WHEN** a freeze scope is created over a set of threads
+- **THEN** each of those threads MUST be suspended for the duration of the scope
+
+#### Scenario: dispose resumes only the frozen threads
+- **WHEN** the freeze scope is disposed
+- **THEN** exactly the threads it suspended MUST be resumed, and threads suspended by other callers MUST be left unchanged
+
+#### Scenario: exception in the body still resumes
+- **WHEN** the guarded body throws before the scope is disposed
+- **THEN** the frozen threads MUST still be resumed as the scope unwinds
diff --git a/openspec/changes/add-thread-region-finder/tasks.md b/openspec/changes/add-thread-region-finder/tasks.md
new file mode 100644
index 0000000..2a9fc68
--- /dev/null
+++ b/openspec/changes/add-thread-region-finder/tasks.md
@@ -0,0 +1,39 @@
+## 1. Memory-region query (spec: memory-region)
+
+- [x] 1.1 Add `VirtualQueryEx` `LibraryImport` and `MEMORY_BASIC_INFORMATION` to `Native/` (32/64-bit-correct layout)
+- [x] 1.2 Add tests for `MemoryRegion.Contains` (in-range true, boundary `[base, base+size)`, out-of-range false)
+- [x] 1.3 Implement `WhiteMagic/Memory/MemoryRegion.cs` (immutable: BaseAddress, Size, Protection, State, Type, AllocationBase, AllocationProtect, Contains) to pass 1.2
+- [x] 1.4 Add tests for `MemoryBase.QueryRegion(address)` against a known committed address in the current process
+- [x] 1.5 Implement `QueryRegion` to pass 1.4
+- [x] 1.6 Add tests for `EnumerateRegions()`: ascending non-overlapping bases, lazy (early stop does not walk whole space — assert via a bounded take)
+- [x] 1.7 Implement lazy `EnumerateRegions()` (walk `base+size` until `VirtualQueryEx` fails) to pass 1.6
+- [x] 1.8 Add tests for `ChangeProtection`/`ProtectionScope`: protection applied in scope, restored on dispose, restored on exception
+- [x] 1.9 Implement `MemoryBase.ChangeProtection` returning `ProtectionScope : IDisposable` to pass 1.8
+
+## 2. Thread control (spec: thread-control)
+
+- [x] 2.1 Add `CreateToolhelp32Snapshot`/`Thread32First`/`Thread32Next` + `THREADENTRY32`, and `GetThreadTimes`, to `Native/` (reuse existing `OpenThread`/`Suspend`/`Resume`/`Get`/`SetThreadContext`)
+- [x] 2.2 Add tests for `RemoteThread`: open by id, `Suspend` returns prior count and stops the thread, `Resume` restarts it (self-process worker thread)
+- [x] 2.3 Implement `WhiteMagic/Thread/RemoteThread.cs` (OpenThread → `SafeMemoryHandle`, Suspend/Resume, Id) to pass 2.2
+- [x] 2.4 Add tests for `GetContext`/`SetContext` round-trip on a suspended self-thread; assert WOW64 vs native selection by target bitness
+- [x] 2.5 Implement context read/write reusing `DllInjector`'s bitness selection to pass 2.4
+- [x] 2.6 Add tests + implement `RemoteThread.GetTeb()` (via `NtQueryInformationThread`/`ThreadBasicInformation` → `ManagedTeb`)
+- [x] 2.7 Add tests for `ThreadFactory`: `Enumerate()` returns only target threads, `MainThread` = earliest-created, `GetThreadById`
+- [x] 2.8 Implement `WhiteMagic/Thread/ThreadFactory.cs` (toolhelp walk filtered by PID; `GetThreadTimes` for main) to pass 2.7
+- [x] 2.9 Add tests for `FrozenThread`/`Freeze()`: suspends selected set, dispose resumes exactly those, body-throws still resumes, external suspends untouched
+- [x] 2.10 Implement `WhiteMagic/Thread/FrozenThread.cs` + `ThreadFactory.Freeze(...)` (reverse-order resume on dispose) to pass 2.9
+
+## 3. Process discovery (spec: high-level-api)
+
+- [x] 3.1 Add tests for `ApplicationFinder.Enumerate()` and open-by-name against the current process
+- [x] 3.2 Implement `WhiteMagic/Process/ApplicationFinder.cs` (`Process.GetProcessesByName`; window-title/handle via existing `WindowFactory` + `GetWindowThreadProcessId`)
+- [x] 3.3 Add tests for ambiguous-match rejection (multiple candidates → throws with candidate list) and open-by-window-handle
+- [x] 3.4 Add `Magic.Open(string processName)`, `Magic.OpenByWindowTitle(string)`, `Magic.OpenByWindowHandle(IntPtr)` overloads delegating to `ApplicationFinder`; add tests
+- [x] 3.5 Wire new surface into the `Magic` facade (expose `Threads` factory and `Regions`/`QueryRegion` accessors) and document freeze-while-executing deadlock caveat in XML docs
+
+## 4. Verification
+
+- [x] 4.1 Run full test suite: `dotnet test WhiteMagicTest/WhiteMagicTest.csproj` — all pass
+- [x] 4.2 Run full build (`dotnet build WhiteMagic.slnx`) — zero errors, zero new warnings in `WhiteMagic`
+- [x] 4.3 Update `docs/memory-library-comparison.md` — mark thread-control, memory-region, and process-discovery gaps closed; note WhiteMagic is now a superset of MemorySharp's public surface (or list any remaining minor helpers deliberately skipped)
+- [x] 4.4 `openspec validate add-thread-region-finder --strict` passes