Skip to content

Commit 3494d72

Browse files
Add a test for control flow guard + small JIT fix (#65122)
Adds a test that compiles with CFG enabled and then re-runs itself to crash in various ways. Also fixes a minor RyuJIT issue discovered in the test.
1 parent 49a4893 commit 3494d72

File tree

3 files changed

+212
-1
lines changed

3 files changed

+212
-1
lines changed

src/coreclr/jit/forwardsub.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -654,8 +654,9 @@ bool Compiler::fgForwardSubStatement(Statement* stmt)
654654
// Not doing so can lead to regressions...
655655
//
656656
// Hold off on doing this for call args for now (per issue #51569).
657+
// Hold off on OBJ(GT_LCL_ADDR).
657658
//
658-
if (fwdSubNode->OperIs(GT_OBJ) && !fsv.IsCallArg())
659+
if (fwdSubNode->OperIs(GT_OBJ) && !fsv.IsCallArg() && fwdSubNode->gtGetOp1()->OperIs(GT_ADDR))
659660
{
660661
const bool destroyNodes = false;
661662
GenTree* const optTree = fgMorphTryFoldObjAsLclVar(fwdSubNode->AsObj(), destroyNodes);
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
8+
using System.Text;
9+
10+
// This test is testing control flow guard.
11+
//
12+
// Since the only observable behavior of control flow guard is that it terminates the process,
13+
// to test various scenarios the test re-runs itself. The main executable finishes with
14+
// exit code 100 on success. The subprocesses are all expected to be killed by control
15+
// flow guard and exit with exit code C0000409.
16+
//
17+
// The "corrupted" indirect call target is located in a piece of memory we VirtualAlloc'd.
18+
// By default VirtualAlloc'd RWX memory is considered valid call target.
19+
// The s_armed static variable controls whether we ask the OS to consider the VirtualAlloc'd
20+
// memory an invalid call target.
21+
unsafe class ControlFlowGuardTests
22+
{
23+
static Func<int>[] s_scenarios =
24+
{
25+
TestFunctionPointer.Run,
26+
TestDelegate.Run,
27+
TestCorruptingVTable.Run,
28+
};
29+
30+
static bool s_armed;
31+
32+
static int Main(string[] args)
33+
{
34+
// Are we running the control program?
35+
if (args.Length == 0)
36+
{
37+
// Dry run - execute all scenarios while s_armed is false.
38+
//
39+
// The replaced call target will not be considered invalid by CFG and none of this
40+
// should crash. This is a safeguard to make sure the only reason why the subordinate
41+
// programs could exit with a FailFast is CFG, and not some other bug in the test logic.
42+
Console.WriteLine("*** Dry run ***");
43+
foreach (Func<int> scenario in s_scenarios)
44+
scenario();
45+
46+
// Now launch subordinate processes and check they FailFast
47+
for (int i = 0; i < s_scenarios.Length; i++)
48+
{
49+
Console.WriteLine($"*** Scenario {i} ***");
50+
Process p = Process.Start(new ProcessStartInfo(Environment.ProcessPath, i.ToString()));
51+
p.WaitForExit();
52+
if ((p.ExitCode != -1073740791) && (p.ExitCode != 57005))
53+
{
54+
Console.WriteLine($"FAIL: Scenario exited with exit code {p.ExitCode}");
55+
return 1;
56+
}
57+
else
58+
{
59+
Console.WriteLine($"Crashed as expected.");
60+
}
61+
}
62+
63+
return 100;
64+
}
65+
66+
[DllImport("kernel32", ExactSpelling = true)]
67+
static extern uint GetErrorMode();
68+
69+
[DllImport("kernel32", ExactSpelling = true)]
70+
static extern uint SetErrorMode(uint uMode);
71+
72+
// Don't pop the WER dialog box that blocks the process until someone clicks Close.
73+
SetErrorMode(GetErrorMode() | 0x0002 /* NOGPFAULTERRORBOX */);
74+
75+
// VirtualAlloc should specify TARGETS_INVALID
76+
s_armed = true;
77+
78+
// Run specified subordinate program
79+
if (int.TryParse(args[0], out int index) && ((uint)index) < (uint)s_scenarios.Length)
80+
return s_scenarios[index]();
81+
82+
// Subordinate program unknown
83+
return 10;
84+
}
85+
86+
class TestFunctionPointer
87+
{
88+
public static int Run()
89+
{
90+
var target = (delegate*<void>)CreateNewMethod();
91+
target();
92+
Console.WriteLine("Was able to call the pointer");
93+
return 1;
94+
}
95+
}
96+
97+
class TestDelegate
98+
{
99+
class RawData
100+
{
101+
public IntPtr FirstField;
102+
}
103+
104+
public static int Run()
105+
{
106+
Func<int> del = Run;
107+
108+
// Replace the delegate destination
109+
Span<IntPtr> delegateMemory = MemoryMarshal.CreateSpan(ref Unsafe.As<RawData>(del).FirstField, 4);
110+
int slotIndex = delegateMemory.IndexOf((IntPtr)(delegate*<int>)&Run);
111+
if (slotIndex < 0)
112+
{
113+
Console.WriteLine("Target not found in the delegate?");
114+
return 1;
115+
}
116+
delegateMemory[slotIndex] = CreateNewMethod();
117+
118+
del();
119+
120+
Console.WriteLine("Was able to call the modified delegate");
121+
return 2;
122+
}
123+
}
124+
125+
class TestCorruptingVTable
126+
{
127+
class Test<T>
128+
{
129+
public override string ToString() => "TotallyUniqueString";
130+
}
131+
132+
public static int Run()
133+
{
134+
// Obscure `typeof(string)` so that dataflow analysis can't see it and the MakeGenericType
135+
// call produces a freshly allocated vtable (not a vtable in the readonly data segment of
136+
// the executable that we wouldn't be able to overwrite).
137+
Type stringType = Type.GetType(new StringBuilder("System.").Append("String").ToString());
138+
Type testOfString = typeof(Test<>).MakeGenericType(stringType);
139+
140+
// Patch the MethodTable of Test<string>: find the vtable slot with the ToString method
141+
// and replace it with a new value that is not in the control flow guard bitmask.
142+
IntPtr toStringMethod = testOfString.GetMethod("ToString").MethodHandle.GetFunctionPointer();
143+
var methodTableMemory = new Span<IntPtr>((void*)testOfString.TypeHandle.Value, 64);
144+
int slotIndex = methodTableMemory.IndexOf(toStringMethod);
145+
if (slotIndex < 0)
146+
{
147+
Console.WriteLine("ToString method not found in the MethodTable?");
148+
return 1;
149+
}
150+
methodTableMemory[slotIndex] = CreateNewMethod();
151+
152+
// Allocate the type and call the corrupted virtual slot
153+
object o = Activator.CreateInstance(testOfString);
154+
o.ToString();
155+
156+
// CFG should have stopped the party
157+
Console.WriteLine("Was able to call the modified slot");
158+
return 2;
159+
}
160+
}
161+
162+
static IntPtr CreateNewMethod()
163+
{
164+
[DllImport("kernel32", ExactSpelling = true, SetLastError = true)]
165+
static extern IntPtr VirtualAlloc(IntPtr lpAddress, nuint dwSize, int flAllocationType, int flProtect);
166+
167+
int flProtect = 0x40 /* EXEC_READWRITE */;
168+
169+
if (s_armed)
170+
flProtect |= 0x40000000 /* TARGETS_INVALID */;
171+
172+
IntPtr address = VirtualAlloc(
173+
lpAddress: IntPtr.Zero,
174+
dwSize: 4096,
175+
flAllocationType: 0x00001000 | 0x00002000 /* COMMIT+RESERVE*/,
176+
flProtect: flProtect);
177+
178+
switch (RuntimeInformation.ProcessArchitecture)
179+
{
180+
case Architecture.X64:
181+
case Architecture.X86:
182+
*((byte*)address) = 0xC3; // ret
183+
break;
184+
case Architecture.Arm64:
185+
*((uint*)address) = 0xD65F03C0; // ret
186+
break;
187+
case Architecture.Arm:
188+
*((ushort*)address) = 0x46F7; // mov pc, lr
189+
break;
190+
default:
191+
throw new NotSupportedException();
192+
}
193+
194+
return address;
195+
}
196+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<CLRTestKind>BuildAndRun</CLRTestKind>
5+
<CLRTestPriority>0</CLRTestPriority>
6+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
7+
<ControlFlowGuard>Guard</ControlFlowGuard>
8+
<Optimize>true</Optimize>
9+
<CLRTestTargetUnsupported Condition="'$(TargetsWindows)' != 'true'">true</CLRTestTargetUnsupported>
10+
</PropertyGroup>
11+
<ItemGroup>
12+
<Compile Include="ControlFlowGuard.cs" />
13+
</ItemGroup>
14+
</Project>

0 commit comments

Comments
 (0)