Skip to content

Commit 81a6f78

Browse files
authored
Fix AccessViolationException when scanning Unity 6 modules with protected memory pages (#267)
* Fix AccessViolation scanning Unity 6 modules with protected pages FindSignatureInModule did a raw linear byte walk over the entire loaded module. On Unity 6000.x the GameAssembly image maps regions as PAGE_NOACCESS / guard pages, so dereferencing them throws a process-fatal AccessViolationException before the caller's signature-exhaustion fallback can run (observed registering injected types on Priconne, Unity 6000.0.58f2). Temporarily flip every committed region of the module to PAGE_EXECUTE_READWRITE for the duration of the scan via VirtualQuery/VirtualProtect, then restore the original protections in a finally block. Non-committed (MEM_FREE/MEM_RESERVE) regions are skipped. Mirrors the approach used by downstream forks that already support Unity 6. * Guard the page-protection scan workaround to Windows only Per @ds5678's review on #267: VirtualProtect/VirtualQuery are kernel32 (Windows-only) and PAGE_NOACCESS is a Windows page state. Run the make-readable / restore dance only when OperatingSystem.IsWindows(); on other platforms fall back to the direct FindSignatureInBlock scan (its prior behaviour), so the kernel32 P/Invokes are never called off-Windows. Also satisfies the CA1416 platform-compatibility analyzer. * Scan only readable regions instead of changing page protections Per @ds5678's review on #267 (relaying Discord feedback): modifying page protections (VirtualProtect to PAGE_EXECUTE_READWRITE) is dangerous and unnecessary. VirtualQuery already enumerates the module's regions, so skip the non-readable / guard / uncommitted ones during the signature scan rather than forcing them readable. Removes VirtualProtect and the RWX flip entirely; keeps the Windows-only guard (VirtualQuery is kernel32) with the plain whole-module scan as the off-Windows fallback. * Bound the signature scan to each region + apply review refinements The per-region scan could read up to mask.Length-1 bytes past a readable region into the adjacent guard / PAGE_NOACCESS page -> a fatal AccessViolationException. Cap FindSignatureInBlock's loop at blockSize - mask.Length so every read stays inside the region. Per @ds5678's review: GetModuleRegions now returns the list instead of an out parameter, and its comment is XML documentation. * Use TerraFX.Interop.Windows for VirtualQuery instead of a manual P/Invoke Replace the hand-written kernel32 VirtualQuery DllImport and MEMORY_BASIC_INFORMATION struct with the TerraFX.Interop.Windows bindings, pinned to 10.0.22621.2 (the last release targeting .NET 6). The readable-region scan is unchanged. * Annotate the region scan as Windows-only for CA1416 The TerraFX MEMORY_BASIC_INFORMATION fields are annotated for Windows 6.1+. Mark GetModuleRegions with the matching SupportedOSPlatform attribute and guard the call with OperatingSystem.IsWindowsVersionAtLeast(6, 1) so the platform-compatibility analyzer is satisfied. * Use TerraFX MEM/PAGE constants instead of hand-rolled hex Drop the manual MEM_COMMIT/PAGE_GUARD/PAGE_READABLE consts and the `using static Windows` in favour of the constants TerraFX already exposes (MEM.*, PAGE.*), qualify VirtualQuery, and default-init the MEMORY_BASIC_INFORMATION. The readable mask moves inside the windows6.1 guard so the const fold stays on a supported-platform path.
1 parent 455c42a commit 81a6f78

2 files changed

Lines changed: 66 additions & 10 deletions

File tree

Il2CppInterop.Runtime/Il2CppInterop.Runtime.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
<ItemGroup>
3535
<PackageReference Include="Iced" Version="1.17.0" />
36+
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.22621.2" />
3637
<PackageReference Include="PolySharp" Version="1.14.1">
3738
<PrivateAssets>all</PrivateAssets>
3839
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

Il2CppInterop.Runtime/MemoryUtils.cs

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,47 @@
1-
using System.Diagnostics;
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
24
using System.Linq;
5+
using System.Runtime.Versioning;
36
using Il2CppInterop.Common.XrefScans;
7+
using TerraFX.Interop.Windows;
48

59
namespace Il2CppInterop.Runtime;
610

711
internal class MemoryUtils
812
{
9-
public static nint FindSignatureInModule(ProcessModule module, SignatureDefinition sigDef)
13+
public static unsafe nint FindSignatureInModule(ProcessModule module, SignatureDefinition sigDef)
1014
{
11-
var ptr = FindSignatureInBlock(
12-
module.BaseAddress,
13-
module.ModuleMemorySize,
14-
sigDef.pattern,
15-
sigDef.mask,
16-
sigDef.offset
17-
);
15+
// On newer Unity (6000.x) the loaded GameAssembly maps some pages PAGE_NOACCESS / guard pages; the raw
16+
// linear byte walk in FindSignatureInBlock dereferences them and throws a fatal AccessViolationException.
17+
// Use VirtualQuery to enumerate the module's regions and scan only the readable committed ones, skipping
18+
// the rest -- without ever modifying page protections. VirtualQuery and the TerraFX MEMORY_BASIC_INFORMATION
19+
// fields are Windows-only (the struct is annotated for Windows 6.1+); elsewhere (where this guard-page issue
20+
// does not arise) fall back to the plain whole-module scan.
21+
nint ptr = 0;
22+
if (OperatingSystem.IsWindowsVersionAtLeast(6, 1))
23+
{
24+
// Protections that permit reading; TerraFX defines no single composite of these.
25+
const uint pageReadable = PAGE.PAGE_READONLY | PAGE.PAGE_READWRITE | PAGE.PAGE_WRITECOPY |
26+
PAGE.PAGE_EXECUTE_READ | PAGE.PAGE_EXECUTE_READWRITE | PAGE.PAGE_EXECUTE_WRITECOPY;
27+
var regions = GetModuleRegions(module);
28+
foreach (var region in regions)
29+
{
30+
if (region.State != MEM.MEM_COMMIT || (region.Protect & PAGE.PAGE_GUARD) != 0 ||
31+
(region.Protect & pageReadable) == 0)
32+
continue;
33+
ptr = FindSignatureInBlock((nint)region.BaseAddress, (long)region.RegionSize,
34+
sigDef.pattern, sigDef.mask, sigDef.offset);
35+
if (ptr != 0)
36+
break;
37+
}
38+
}
39+
else
40+
{
41+
ptr = FindSignatureInBlock(module.BaseAddress, module.ModuleMemorySize,
42+
sigDef.pattern, sigDef.mask, sigDef.offset);
43+
}
44+
1845
if (ptr != 0 && sigDef.xref)
1946
ptr = XrefScannerLowLevel.JumpTargets(ptr).FirstOrDefault();
2047
return ptr;
@@ -28,7 +55,11 @@ public static nint FindSignatureInBlock(nint block, long blockSize, string patte
2855
public static unsafe nint FindSignatureInBlock(nint block, long blockSize, char[] pattern, char[] mask,
2956
long sigOffset = 0)
3057
{
31-
for (long address = 0; address < blockSize; address++)
58+
// Stop at blockSize - mask.Length so the inner read (block + address + mask.Length - 1) never passes the
59+
// end of this block. When the caller scans per-region (Unity 6 readable regions interleaved with guard /
60+
// PAGE_NOACCESS pages), an overread off the tail would fault the adjacent page -> a fatal
61+
// AccessViolationException that aborts the chainloader. If the block is smaller than the mask, scan nothing.
62+
for (long address = 0; address <= blockSize - mask.Length; address++)
3263
{
3364
var found = true;
3465
for (uint offset = 0; offset < mask.Length; offset++)
@@ -45,6 +76,30 @@ public static unsafe nint FindSignatureInBlock(nint block, long blockSize, char[
4576
return 0;
4677
}
4778

79+
/// <summary>
80+
/// Walks the module's address space via <c>VirtualQuery</c>, collecting each memory region so the scan can pick
81+
/// the readable committed ones. Stops at the first <c>VirtualQuery</c> failure or once the module end is reached.
82+
/// </summary>
83+
[SupportedOSPlatform("windows6.1")]
84+
internal static unsafe List<MEMORY_BASIC_INFORMATION> GetModuleRegions(ProcessModule module)
85+
{
86+
var regions = new List<MEMORY_BASIC_INFORMATION>();
87+
var moduleEndAddress = (long)module.BaseAddress + module.ModuleMemorySize;
88+
var currentAddress = (long)module.BaseAddress;
89+
while (currentAddress < moduleEndAddress)
90+
{
91+
MEMORY_BASIC_INFORMATION memoryInfo = default;
92+
var result = Windows.VirtualQuery((void*)currentAddress, &memoryInfo, (nuint)sizeof(MEMORY_BASIC_INFORMATION));
93+
if (result == 0)
94+
break; // error, or reached the end of the module's mapped memory
95+
96+
regions.Add(memoryInfo);
97+
currentAddress = (long)memoryInfo.BaseAddress + (long)memoryInfo.RegionSize;
98+
}
99+
100+
return regions;
101+
}
102+
48103
public struct SignatureDefinition
49104
{
50105
public string pattern;

0 commit comments

Comments
 (0)