Add memory-region query, enumeration, and scoped protection
Implements VirtualQueryEx + MEMORY_BASIC_INFORMATION wrappers, the immutable MemoryRegion record, the ProtectionScope disposable helper, and MemoryBase.QueryRegion/EnumerateRegions/ChangeProtection. Closes section 1 of add-thread-region-finder.
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
using System;
|
||||||
|
using WhiteMagic.Native;
|
||||||
|
|
||||||
|
namespace WhiteMagic.Memory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An immutable snapshot of a memory region as reported by <c>VirtualQueryEx</c>.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct MemoryRegion
|
||||||
|
{
|
||||||
|
/// <summary>The base address of the region of pages.</summary>
|
||||||
|
public IntPtr BaseAddress { get; }
|
||||||
|
|
||||||
|
/// <summary>The size of the region, in bytes.</summary>
|
||||||
|
public nuint Size { get; }
|
||||||
|
|
||||||
|
/// <summary>The access protection of the pages in the region.</summary>
|
||||||
|
public MemoryProtectionType Protection { get; }
|
||||||
|
|
||||||
|
/// <summary>The state of the pages in the region.</summary>
|
||||||
|
public MemoryState State { get; }
|
||||||
|
|
||||||
|
/// <summary>The type of pages in the region.</summary>
|
||||||
|
public MemoryType Type { get; }
|
||||||
|
|
||||||
|
/// <summary>The base address of a range of pages allocated by VirtualAllocEx.</summary>
|
||||||
|
public IntPtr AllocationBase { get; }
|
||||||
|
|
||||||
|
/// <summary>The memory protection option when the region was initially allocated.</summary>
|
||||||
|
public MemoryProtectionType AllocationProtect { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="MemoryRegion"/> from explicit values.
|
||||||
|
/// </summary>
|
||||||
|
public MemoryRegion(
|
||||||
|
IntPtr baseAddress,
|
||||||
|
nuint size,
|
||||||
|
MemoryProtectionType protection,
|
||||||
|
MemoryState state,
|
||||||
|
MemoryType type,
|
||||||
|
IntPtr allocationBase,
|
||||||
|
MemoryProtectionType allocationProtect)
|
||||||
|
{
|
||||||
|
BaseAddress = baseAddress;
|
||||||
|
Size = size;
|
||||||
|
Protection = protection;
|
||||||
|
State = state;
|
||||||
|
Type = type;
|
||||||
|
AllocationBase = allocationBase;
|
||||||
|
AllocationProtect = allocationProtect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="MemoryRegion"/> from a raw <c>MEMORY_BASIC_INFORMATION</c>.
|
||||||
|
/// </summary>
|
||||||
|
internal MemoryRegion(MemoryBasicInformation info)
|
||||||
|
{
|
||||||
|
BaseAddress = info.BaseAddress;
|
||||||
|
Size = info.RegionSize;
|
||||||
|
AllocationBase = info.AllocationBase;
|
||||||
|
AllocationProtect = (MemoryProtectionType)info.AllocationProtect;
|
||||||
|
Protection = (MemoryProtectionType)info.Protect;
|
||||||
|
State = (MemoryState)info.State;
|
||||||
|
Type = (MemoryType)info.Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns <see langword="true"/> if <paramref name="address"/> is inside the region,
|
||||||
|
/// defined as <c>[BaseAddress, BaseAddress + Size)</c>.
|
||||||
|
/// </summary>
|
||||||
|
public bool Contains(IntPtr address)
|
||||||
|
{
|
||||||
|
return (nuint)(address - BaseAddress) < Size;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using WhiteMagic.Native;
|
||||||
|
|
||||||
|
namespace WhiteMagic.Memory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A scope that temporarily changes page protection via <c>VirtualProtectEx</c> and
|
||||||
|
/// restores the original protection when disposed, including when the guarded body throws.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ProtectionScope : IDisposable
|
||||||
|
{
|
||||||
|
private readonly MemoryBase _memory;
|
||||||
|
private readonly IntPtr _address;
|
||||||
|
private readonly nint _size;
|
||||||
|
private readonly MemoryProtectionType _originalProtection;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new protection scope, applying <paramref name="newProtection"/> to the
|
||||||
|
/// specified range immediately.
|
||||||
|
/// </summary>
|
||||||
|
internal ProtectionScope(MemoryBase memory, IntPtr address, nint size, MemoryProtectionType newProtection)
|
||||||
|
{
|
||||||
|
_memory = memory ?? throw new ArgumentNullException(nameof(memory));
|
||||||
|
|
||||||
|
if (address == IntPtr.Zero)
|
||||||
|
throw new ArgumentException("Address cannot be zero.", nameof(address));
|
||||||
|
|
||||||
|
if (size <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(size), "Size must be positive.");
|
||||||
|
|
||||||
|
_address = address;
|
||||||
|
_size = size;
|
||||||
|
|
||||||
|
if (!NativeMethods.VirtualProtectEx(
|
||||||
|
memory.Handle,
|
||||||
|
address,
|
||||||
|
size,
|
||||||
|
newProtection,
|
||||||
|
out _originalProtection))
|
||||||
|
{
|
||||||
|
int error = Marshal.GetLastPInvokeError();
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"VirtualProtectEx failed to change protection: error {error}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Restores the original page protection if it has not already been restored.</summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
_disposed = true;
|
||||||
|
NativeMethods.VirtualProtectEx(_memory.Handle, _address, _size, _originalProtection, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using WhiteMagic.Hooking;
|
using WhiteMagic.Hooking;
|
||||||
|
using WhiteMagic.Memory;
|
||||||
using WhiteMagic.Native;
|
using WhiteMagic.Native;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@@ -268,6 +270,58 @@ public abstract class MemoryBase : IDisposable
|
|||||||
return (IntPtr)((nint)absolute - (nint)ImageBase);
|
return (IntPtr)((nint)absolute - (nint)ImageBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Memory region query ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queries the memory region that contains <paramref name="address"/> in the target
|
||||||
|
/// process using <c>VirtualQueryEx</c>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An immutable snapshot of the region.</returns>
|
||||||
|
/// <exception cref="InvalidOperationException">The query fails.</exception>
|
||||||
|
public MemoryRegion QueryRegion(IntPtr address)
|
||||||
|
{
|
||||||
|
nuint bufferSize = (nuint)Marshal.SizeOf<MemoryBasicInformation>();
|
||||||
|
nuint result = NativeMethods.VirtualQueryEx(Handle, address, out MemoryBasicInformation info, bufferSize);
|
||||||
|
|
||||||
|
if (result == 0)
|
||||||
|
{
|
||||||
|
int error = Marshal.GetLastPInvokeError();
|
||||||
|
throw new InvalidOperationException($"VirtualQueryEx failed for address 0x{address:X}: error {error}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MemoryRegion(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enumerates the memory regions of the target process from the lowest address upward.
|
||||||
|
/// The walk is lazy; callers can stop early without walking the entire address space.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<MemoryRegion> EnumerateRegions()
|
||||||
|
{
|
||||||
|
IntPtr address = IntPtr.Zero;
|
||||||
|
nuint bufferSize = (nuint)Marshal.SizeOf<MemoryBasicInformation>();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
nuint result = NativeMethods.VirtualQueryEx(Handle, address, out MemoryBasicInformation info, bufferSize);
|
||||||
|
if (result == 0)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
yield return new MemoryRegion(info);
|
||||||
|
address = info.BaseAddress + (nint)info.RegionSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Changes the page protection on a region of memory and returns a disposable scope
|
||||||
|
/// that restores the original protection on dispose, including when an exception escapes
|
||||||
|
/// the guarded body.
|
||||||
|
/// </summary>
|
||||||
|
public ProtectionScope ChangeProtection(IntPtr address, nint size, MemoryProtectionType protection)
|
||||||
|
{
|
||||||
|
return new ProtectionScope(this, address, size, protection);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Lifecycle ──────────────────────────────────────────────────────────
|
// ── Lifecycle ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -156,3 +156,61 @@ public static class ContextFlags
|
|||||||
/// <summary>AMD64: control, integer, and segment registers.</summary>
|
/// <summary>AMD64: control, integer, and segment registers.</summary>
|
||||||
public const uint Amd64Full = Amd64Control | Amd64Integer | Amd64Segments;
|
public const uint Amd64Full = Amd64Control | Amd64Integer | Amd64Segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Values that describe the state of memory pages returned by <c>VirtualQueryEx</c>.
|
||||||
|
/// </summary>
|
||||||
|
public enum MemoryState : uint
|
||||||
|
{
|
||||||
|
/// <summary>Indicates committed pages for which physical storage has been allocated.</summary>
|
||||||
|
Commit = 0x1000,
|
||||||
|
|
||||||
|
/// <summary>Indicates reserved pages where a range of the virtual address space is reserved without any physical storage being allocated.</summary>
|
||||||
|
Reserve = 0x2000,
|
||||||
|
|
||||||
|
/// <summary>Indicates free pages not accessible to the calling process and available to be allocated.</summary>
|
||||||
|
Free = 0x10000,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Values that describe the type of memory pages returned by <c>VirtualQueryEx</c>.
|
||||||
|
/// </summary>
|
||||||
|
public enum MemoryType : uint
|
||||||
|
{
|
||||||
|
/// <summary>Indicates that the memory pages within the region are private.</summary>
|
||||||
|
Private = 0x20000,
|
||||||
|
|
||||||
|
/// <summary>Indicates that the memory pages within the region are mapped into the view of a section.</summary>
|
||||||
|
Mapped = 0x40000,
|
||||||
|
|
||||||
|
/// <summary>Indicates that the memory pages within the region are mapped into the view of an image section.</summary>
|
||||||
|
Image = 0x1000000,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flags used by <c>CreateToolhelp32Snapshot</c> to specify the portions of the system to include in the snapshot.
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum SnapshotFlags : uint
|
||||||
|
{
|
||||||
|
/// <summary>Enumerate the heap list.</summary>
|
||||||
|
HeapList = 0x00000001,
|
||||||
|
|
||||||
|
/// <summary>Enumerate the process list.</summary>
|
||||||
|
Process = 0x00000002,
|
||||||
|
|
||||||
|
/// <summary>Enumerate the thread list.</summary>
|
||||||
|
Thread = 0x00000004,
|
||||||
|
|
||||||
|
/// <summary>Enumerate the module list.</summary>
|
||||||
|
Module = 0x00000008,
|
||||||
|
|
||||||
|
/// <summary>Enumerate the 32-bit module list for the specified process.</summary>
|
||||||
|
Module32 = 0x00000010,
|
||||||
|
|
||||||
|
/// <summary>Include all processes and threads in the system.</summary>
|
||||||
|
All = 0x0000001F,
|
||||||
|
|
||||||
|
/// <summary>Indicate that the snapshot handle is to be inheritable.</summary>
|
||||||
|
Inherit = 0x80000000,
|
||||||
|
}
|
||||||
|
|||||||
@@ -173,4 +173,46 @@ internal static partial class NativeMethods
|
|||||||
SafeMemoryHandle handle,
|
SafeMemoryHandle handle,
|
||||||
uint milliseconds);
|
uint milliseconds);
|
||||||
|
|
||||||
|
// ── Memory query ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Retrieves information about a range of pages in the virtual address space of a specified process.</summary>
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
internal static partial nuint VirtualQueryEx(
|
||||||
|
SafeMemoryHandle process,
|
||||||
|
IntPtr address,
|
||||||
|
out MemoryBasicInformation buffer,
|
||||||
|
nuint length);
|
||||||
|
|
||||||
|
// ── Thread enumeration ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Takes a snapshot of the specified processes, as well as the heaps, modules, and threads used by these processes.</summary>
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
internal static partial SafeMemoryHandle CreateToolhelp32Snapshot(
|
||||||
|
SnapshotFlags dwFlags,
|
||||||
|
int th32ProcessID);
|
||||||
|
|
||||||
|
/// <summary>Retrieves information about the first thread of any process encountered in a system snapshot.</summary>
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool Thread32First(
|
||||||
|
SafeMemoryHandle hSnapshot,
|
||||||
|
ref ThreadEntry32 lpte);
|
||||||
|
|
||||||
|
/// <summary>Retrieves information about the next thread of any process encountered in a system snapshot.</summary>
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool Thread32Next(
|
||||||
|
SafeMemoryHandle hSnapshot,
|
||||||
|
ref ThreadEntry32 lpte);
|
||||||
|
|
||||||
|
/// <summary>Retrieves timing information for the specified thread.</summary>
|
||||||
|
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
internal static partial bool GetThreadTimes(
|
||||||
|
SafeMemoryHandle thread,
|
||||||
|
out long creationTime,
|
||||||
|
out long exitTime,
|
||||||
|
out long kernelTime,
|
||||||
|
out long userTime);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,3 +205,61 @@ public unsafe struct Context64
|
|||||||
/// <summary>The source RIP of the last exception.</summary>
|
/// <summary>The source RIP of the last exception.</summary>
|
||||||
public ulong LastExceptionFromRip;
|
public ulong LastExceptionFromRip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Layout matches <c>MEMORY_BASIC_INFORMATION</c>. Uses pointer-sized fields so the
|
||||||
|
/// structure is 28 bytes on x86 and 48 bytes on x64, matching the layout the OS expects
|
||||||
|
/// from a caller of those bitnesses.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct MemoryBasicInformation
|
||||||
|
{
|
||||||
|
/// <summary>A pointer to the base address of the region of pages.</summary>
|
||||||
|
public nint BaseAddress;
|
||||||
|
|
||||||
|
/// <summary>A pointer to the base address of a range of pages allocated by the VirtualAllocEx function.</summary>
|
||||||
|
public nint AllocationBase;
|
||||||
|
|
||||||
|
/// <summary>The memory protection option when the region was initially allocated.</summary>
|
||||||
|
public uint AllocationProtect;
|
||||||
|
|
||||||
|
/// <summary>The size of the region beginning at the base address, in bytes.</summary>
|
||||||
|
public nuint RegionSize;
|
||||||
|
|
||||||
|
/// <summary>The state of the pages in the region.</summary>
|
||||||
|
public uint State;
|
||||||
|
|
||||||
|
/// <summary>The access protection of the pages in the region.</summary>
|
||||||
|
public uint Protect;
|
||||||
|
|
||||||
|
/// <summary>The type of pages in the region.</summary>
|
||||||
|
public uint Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Layout matches <c>THREADENTRY32</c> used by <c>Thread32First</c>/<c>Thread32Next</c>.
|
||||||
|
/// </summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
internal struct ThreadEntry32
|
||||||
|
{
|
||||||
|
/// <summary>The size of the structure, in bytes.</summary>
|
||||||
|
public uint dwSize;
|
||||||
|
|
||||||
|
/// <summary>This member is no longer used and is always zero.</summary>
|
||||||
|
public uint cntUsage;
|
||||||
|
|
||||||
|
/// <summary>The thread identifier.</summary>
|
||||||
|
public uint th32ThreadID;
|
||||||
|
|
||||||
|
/// <summary>The identifier of the process that owns the thread.</summary>
|
||||||
|
public uint th32OwnerProcessID;
|
||||||
|
|
||||||
|
/// <summary>The kernel base priority level assigned to the thread.</summary>
|
||||||
|
public int tpBasePri;
|
||||||
|
|
||||||
|
/// <summary>This member is no longer used.</summary>
|
||||||
|
public int tpDeltaPri;
|
||||||
|
|
||||||
|
/// <summary>This member is reserved.</summary>
|
||||||
|
public uint dwFlags;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using WhiteMagic;
|
||||||
|
using WhiteMagic.Memory;
|
||||||
|
using WhiteMagic.Native;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace WhiteMagicTest.Memory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for memory-region query, enumeration and scoped protection (tasks 1.2, 1.4, 1.6, 1.8).
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MemoryRegionTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Contains_returns_true_for_addresses_inside_half_open_range()
|
||||||
|
{
|
||||||
|
var region = new MemoryRegion(
|
||||||
|
new IntPtr(0x10000),
|
||||||
|
0x1000,
|
||||||
|
MemoryProtectionType.ReadWrite,
|
||||||
|
MemoryState.Commit,
|
||||||
|
MemoryType.Private,
|
||||||
|
new IntPtr(0x10000),
|
||||||
|
MemoryProtectionType.ReadWrite);
|
||||||
|
|
||||||
|
Assert.True(region.Contains(new IntPtr(0x10000)));
|
||||||
|
Assert.True(region.Contains(new IntPtr(0x10FFF)));
|
||||||
|
Assert.False(region.Contains(new IntPtr(0x11000)));
|
||||||
|
Assert.False(region.Contains(new IntPtr(0x0FFF)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void QueryRegion_returns_region_containing_committed_address()
|
||||||
|
{
|
||||||
|
using var reader = new InProcessReader();
|
||||||
|
nint pageSize = Environment.SystemPageSize;
|
||||||
|
|
||||||
|
IntPtr block = NativeMethods.VirtualAllocEx(
|
||||||
|
reader.Handle,
|
||||||
|
IntPtr.Zero,
|
||||||
|
pageSize,
|
||||||
|
MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
|
||||||
|
MemoryProtectionType.ReadWrite);
|
||||||
|
|
||||||
|
Assert.NotEqual(IntPtr.Zero, block);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
MemoryRegion region = reader.QueryRegion(block);
|
||||||
|
|
||||||
|
Assert.Equal(block, region.BaseAddress);
|
||||||
|
Assert.True(region.Contains(block));
|
||||||
|
Assert.True(region.Contains(block + (int)pageSize - 1));
|
||||||
|
Assert.Equal(MemoryState.Commit, region.State);
|
||||||
|
Assert.Equal(MemoryType.Private, region.Type);
|
||||||
|
Assert.Equal(MemoryProtectionType.ReadWrite, region.Protection);
|
||||||
|
Assert.Equal(MemoryProtectionType.ReadWrite, region.AllocationProtect);
|
||||||
|
Assert.Equal(block, region.AllocationBase);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
NativeMethods.VirtualFreeEx(reader.Handle, block, 0, MemoryFreeType.Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EnumerateRegions_yields_ascending_non_overlapping_regions()
|
||||||
|
{
|
||||||
|
using var reader = new InProcessReader();
|
||||||
|
|
||||||
|
MemoryRegion[] regions = reader.EnumerateRegions().Take(5).ToArray();
|
||||||
|
Assert.True(regions.Length > 0);
|
||||||
|
|
||||||
|
for (int i = 1; i < regions.Length; i++)
|
||||||
|
{
|
||||||
|
Assert.True(
|
||||||
|
(nuint)regions[i].BaseAddress >=
|
||||||
|
(nuint)regions[i - 1].BaseAddress + regions[i - 1].Size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EnumerateRegions_is_lazy_and_stops_early()
|
||||||
|
{
|
||||||
|
using var reader = new InProcessReader();
|
||||||
|
|
||||||
|
// Taking a single item must not force a full address-space walk.
|
||||||
|
MemoryRegion first = reader.EnumerateRegions().First();
|
||||||
|
Assert.True(first.Size > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ChangeProtection_applies_new_protection_inside_scope_and_restores_on_dispose()
|
||||||
|
{
|
||||||
|
using var reader = new InProcessReader();
|
||||||
|
nint pageSize = Environment.SystemPageSize;
|
||||||
|
|
||||||
|
IntPtr block = NativeMethods.VirtualAllocEx(
|
||||||
|
reader.Handle,
|
||||||
|
IntPtr.Zero,
|
||||||
|
pageSize,
|
||||||
|
MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
|
||||||
|
MemoryProtectionType.ReadWrite);
|
||||||
|
|
||||||
|
Assert.NotEqual(IntPtr.Zero, block);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.Equal(MemoryProtectionType.ReadWrite, reader.QueryRegion(block).Protection);
|
||||||
|
|
||||||
|
using (reader.ChangeProtection(block, pageSize, MemoryProtectionType.ExecuteReadWrite))
|
||||||
|
{
|
||||||
|
Assert.Equal(MemoryProtectionType.ExecuteReadWrite, reader.QueryRegion(block).Protection);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(MemoryProtectionType.ReadWrite, reader.QueryRegion(block).Protection);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
NativeMethods.VirtualFreeEx(reader.Handle, block, 0, MemoryFreeType.Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ChangeProtection_restores_original_protection_when_body_throws()
|
||||||
|
{
|
||||||
|
using var reader = new InProcessReader();
|
||||||
|
nint pageSize = Environment.SystemPageSize;
|
||||||
|
|
||||||
|
IntPtr block = NativeMethods.VirtualAllocEx(
|
||||||
|
reader.Handle,
|
||||||
|
IntPtr.Zero,
|
||||||
|
pageSize,
|
||||||
|
MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
|
||||||
|
MemoryProtectionType.ReadWrite);
|
||||||
|
|
||||||
|
Assert.NotEqual(IntPtr.Zero, block);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.Throws<InvalidOperationException>(new Action(() =>
|
||||||
|
{
|
||||||
|
using (reader.ChangeProtection(block, pageSize, MemoryProtectionType.ExecuteReadWrite))
|
||||||
|
{
|
||||||
|
Assert.Equal(MemoryProtectionType.ExecuteReadWrite, reader.QueryRegion(block).Protection);
|
||||||
|
throw new InvalidOperationException("Intentional failure inside scope.");
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
Assert.Equal(MemoryProtectionType.ReadWrite, reader.QueryRegion(block).Protection);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
NativeMethods.VirtualFreeEx(reader.Handle, block, 0, MemoryFreeType.Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user