Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JIT support for control-flow guard on x64 and arm64 #63763

Merged
merged 31 commits into from
Feb 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7075393
Implement control-flow guard support for x64 and arm64
jakobbotsch Jan 20, 2022
dfa28e1
Pipe through the flag to NativeAOT compiler
MichalStrehovsky Jan 19, 2022
e07386f
Represent the helper as an indirection
MichalStrehovsky Jan 19, 2022
fb293b9
Add some verbose output for moved late args
jakobbotsch Jan 20, 2022
4665ea0
Add GT_FIELD_LIST to assert
jakobbotsch Jan 20, 2022
b19e47d
Fix placement moving logic for GT_FIELD_LIST
jakobbotsch Jan 20, 2022
0a221c5
Add more general invariance check
jakobbotsch Jan 20, 2022
6f68c08
Fix release build
jakobbotsch Jan 20, 2022
acf223b
Fix IsInvariant check
jakobbotsch Jan 20, 2022
ef465fb
Add a dTreeRange helper
jakobbotsch Jan 20, 2022
a7a6415
Skip local for call target
jakobbotsch Jan 20, 2022
17e22f0
Add missing parentheses
jakobbotsch Jan 20, 2022
21e5858
Rename IsInvariant -> IsInvariantInRange
jakobbotsch Jan 20, 2022
8baddf0
Another build fix
jakobbotsch Jan 20, 2022
092364e
Fix register for ARM64 dispatcher
jakobbotsch Jan 21, 2022
ce46dcf
Add section on CFG to the clr-abi docs
jakobbotsch Jan 21, 2022
451aaa6
Fix arm64 validator trashed registers and update lowering comment
jakobbotsch Jan 21, 2022
6df00a1
Add line to disasm when CFG is enabled
jakobbotsch Jan 21, 2022
69d3b5f
Add a workaround for reporting dead GC pointer regs too early when th…
jakobbotsch Feb 1, 2022
ca5c444
Merge branch 'main' of github.com:dotnet/runtime into support-cfg
jakobbotsch Feb 1, 2022
6659597
Fix some comments
jakobbotsch Feb 1, 2022
866bcce
Revert "Add a workaround for reporting dead GC pointer regs too early…
jakobbotsch Feb 2, 2022
bd1a7ec
Move all GT_PUTARG_* nodes behind validator to fix GC reporting
jakobbotsch Feb 2, 2022
f9ad18c
Remove unused variable
jakobbotsch Feb 2, 2022
1f42b99
Add a CFG + GC stress job
jakobbotsch Feb 4, 2022
2eeecde
Clarify expected use of CFG
jakobbotsch Feb 8, 2022
f25ba52
Assert we do not lower dispatcher as CFG call
jakobbotsch Feb 8, 2022
47409ba
Update jit-ee interface GUID
jakobbotsch Feb 8, 2022
13233bf
Remove note about preallocating arg for dispatcher
jakobbotsch Feb 8, 2022
447bca8
Remove another TODO
jakobbotsch Feb 8, 2022
95defd6
Fix an assert
jakobbotsch Feb 8, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/design/coreclr/botr/clr-abi.md
Original file line number Diff line number Diff line change
Expand Up @@ -752,3 +752,43 @@ The return value is handled as follows:
4. All other cases require the use of a return buffer, through which the value is returned.

In addition, there is a guarantee that if a return buffer is used a value is stored there only upon ordinary exit from the method. The buffer is not allowed to be used for temporary storage within the method and its contents will be unaltered if an exception occurs while executing the method.

# Control Flow Guard (CFG) support on Windows

Control Flow Guard (CFG) is a security mitigation available in Windows.
When CFG is enabled, the operating system maintains data structures that can be used to verify whether an address is to be considered a valid indirect call target.
This mechanism is exposed through two different helper functions, each with different characteristics.

The first mechanism is a validator that takes the target address as an argument and fails fast if the address is not an expected indirect call target; otherwise, it does nothing and returns.
The second mechanism is a dispatcher that takes the target address in a non-standard register; on successful validation of the address, it jumps directly to the target function.
Windows makes the dispatcher available only on ARM64 and x64, while the validator is available on all platforms.
However, the JIT supports CFG only on ARM64 and x64, with CFG by default being disabled for these platforms.
The expected use of the CFG feature is for NativeAOT scenarios that are running in constrained environments where CFG is required.

The helpers are exposed to the JIT as standard JIT helpers `CORINFO_HELP_VALIDATE_INDIRECT_CALL` and `CORINFO_HELP_DISPATCH_INDIRECT_CALL`.

To use the validator the JIT expands indirect calls into a call to the validator followed by a call to the validated address.
For the dispatcher the JIT will transform calls to pass the target along but otherwise set up the call as normal.

Note that "indirect call" here refers to any call that is not to an immediate (in the instruction stream) address.
For example, even direct calls may emit indirect call instructions in JIT codegen due to e.g. tiering or if they have not been compiled yet; these are expanded with the CFG mechanism as well.

The next sections describe the calling convention that the JIT expects from these helpers.

## CFG details for ARM64

On ARM64, `CORINFO_HELP_VALIDATE_INDIRECT_CALL` takes the call address in `x15`.
In addition to the usual registers it preserves all float registers, `x0`-`x8` and `x15`.

`CORINFO_HELP_DISPATCH_INDIRECT_CALL` takes the call address in `x9`.
The JIT does not use the dispatch helper by default due to worse branch predictor performance.
Therefore it will expand all indirect calls via the validation helper and a manual call.

## CFG details for x64

On x64, `CORINFO_HELP_VALIDATE_INDIRECT_CALL` takes the call address in `rcx`.
In addition to the usual registers it also preserves all float registers and `rcx` and `r10`; furthermore, shadow stack space is not required to be allocated.

`CORINFO_HELP_DISPATCH_INDIRECT_CALL` takes the call address in `rax` and it reserves the right to use and trash `r10` and `r11`.
The JIT uses the dispatch helper on x64 whenever possible as it is expected that the code size benefits outweighs the less accurate branch prediction.
However, note that the use of `r11` in the dispatcher makes it incompatible with VSD calls where the JIT must fall back to the validator and a manual call.
11 changes: 10 additions & 1 deletion eng/pipelines/common/templates/runtimes/run-test-job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ jobs:
- ${{ if in(parameters.testGroup, 'pgo') }}:
- name: timeoutPerTestCollectionInMinutes
value: 120
- ${{ if in(parameters.testGroup, 'jit-cfg') }}:
- name: timeoutPerTestCollectionInMinutes
value: 120

- ${{ if eq(parameters.compositeBuildMode, true) }}:
- name: crossgenArg
Expand All @@ -210,7 +213,7 @@ jobs:
# TODO: update these numbers as they were determined long ago
${{ if eq(parameters.testGroup, 'innerloop') }}:
timeoutInMinutes: 200
${{ if in(parameters.testGroup, 'outerloop', 'jit-experimental', 'pgo') }}:
${{ if in(parameters.testGroup, 'outerloop', 'jit-experimental', 'pgo', 'jit-cfg') }}:
timeoutInMinutes: 270
${{ if in(parameters.testGroup, 'gc-longrunning', 'gc-simulator') }}:
timeoutInMinutes: 480
Expand Down Expand Up @@ -551,6 +554,12 @@ jobs:
- jitpartialcompilation_osr
- jitpartialcompilation_osr_pgo
- jitobjectstackallocation
${{ if in(parameters.testGroup, 'jit-cfg') }}:
scenarios:
- jitcfg
- jitcfg_dispatcher_always
- jitcfg_dispatcher_never
- jitcfg_gcstress0xc
${{ if in(parameters.testGroup, 'ilasm') }}:
scenarios:
- ilasmroundtrip
Expand Down
52 changes: 52 additions & 0 deletions eng/pipelines/coreclr/jit-cfg.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
trigger: none
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this pipeline is great! Especially with GCStress. Thanks for doing this.


schedules:
- cron: "0 22 * * 0,6"
displayName: Sun at 2:00 PM (UTC-8:00)
branches:
include:
- main
always: true

jobs:

- template: /eng/pipelines/common/platform-matrix.yml
parameters:
jobTemplate: /eng/pipelines/common/build-coreclr-and-libraries-job.yml
buildConfig: checked
platforms:
- OSX_arm64
- OSX_x64
- Linux_arm64
- Linux_x64
- windows_arm64
- windows_x64
- CoreClrTestBuildHost # Either OSX_x64 or Linux_x64
jobParameters:
testGroup: jit-cfg

- template: /eng/pipelines/common/platform-matrix.yml
parameters:
jobTemplate: /eng/pipelines/common/templates/runtimes/build-test-job.yml
buildConfig: checked
platforms:
- CoreClrTestBuildHost # Either OSX_x64 or Linux_x64
jobParameters:
testGroup: jit-cfg

- template: /eng/pipelines/common/platform-matrix.yml
parameters:
jobTemplate: /eng/pipelines/common/templates/runtimes/run-test-job.yml
buildConfig: checked
platforms:
- OSX_arm64
- OSX_x64
- Linux_arm64
- Linux_x64
- windows_arm64
- windows_x64
helixQueueGroup: ci
helixQueuesTemplate: /eng/pipelines/coreclr/templates/helix-queues-setup.yml
jobParameters:
testGroup: jit-cfg
liveLibrariesBuildConfig: Release
3 changes: 3 additions & 0 deletions src/coreclr/inc/corinfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,9 @@ enum CorInfoHelpFunc
CORINFO_HELP_CLASSPROFILE64, // Update 64-bit class profile for a call site
CORINFO_HELP_PARTIAL_COMPILATION_PATCHPOINT, // Notify runtime that code has reached a part of the method that wasn't originally jitted.

CORINFO_HELP_VALIDATE_INDIRECT_CALL, // CFG: Validate function pointer
jakobbotsch marked this conversation as resolved.
Show resolved Hide resolved
CORINFO_HELP_DISPATCH_INDIRECT_CALL, // CFG: Validate and dispatch to pointer

CORINFO_HELP_COUNT,
};

Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/inc/corjitflags.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class CORJIT_FLAGS
CORJIT_FLAG_DEBUG_EnC = 3, // We are in Edit-n-Continue mode
CORJIT_FLAG_DEBUG_INFO = 4, // generate line and local-var info
CORJIT_FLAG_MIN_OPT = 5, // disable all jit optimizations (not necesarily debuggable code)
CORJIT_FLAG_UNUSED1 = 6,
CORJIT_FLAG_ENABLE_CFG = 6, // generate control-flow guard checks
CORJIT_FLAG_MCJIT_BACKGROUND = 7, // Calling from multicore JIT background thread, do not call JitComplete

#if defined(TARGET_X86)
Expand Down
11 changes: 6 additions & 5 deletions src/coreclr/inc/jiteeversionguid.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,14 @@ typedef const GUID *LPCGUID;
#define GUID_DEFINED
#endif // !GUID_DEFINED

constexpr GUID JITEEVersionIdentifier = { /* ccb0c159-04b3-47f6-993e-79114c9cbef8 */
0xccb0c159,
0x04b3,
0x47f6,
{0x99, 0x3e, 0x79, 0x11, 0x4c, 0x9c, 0xbe, 0xf8}
constexpr GUID JITEEVersionIdentifier = { /* 63009f0c-662a-485b-bac1-ff67be6c7f9d */
0x63009f0c,
0x662a,
0x485b,
{0xba, 0xc1, 0xff, 0x67, 0xbe, 0x6c, 0x7f, 0x9d}
};


//////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// END JITEEVersionIdentifier
Expand Down
8 changes: 8 additions & 0 deletions src/coreclr/inc/jithelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,14 @@
JITHELPER(CORINFO_HELP_CLASSPROFILE64, JIT_ClassProfile64, CORINFO_HELP_SIG_REG_ONLY)
JITHELPER(CORINFO_HELP_PARTIAL_COMPILATION_PATCHPOINT, JIT_PartialCompilationPatchpoint, CORINFO_HELP_SIG_REG_ONLY)

#if defined(TARGET_AMD64) || defined(TARGET_ARM64)
JITHELPER(CORINFO_HELP_VALIDATE_INDIRECT_CALL, JIT_ValidateIndirectCall, CORINFO_HELP_SIG_REG_ONLY)
JITHELPER(CORINFO_HELP_DISPATCH_INDIRECT_CALL, JIT_DispatchIndirectCall, CORINFO_HELP_SIG_REG_ONLY)
#else
JITHELPER(CORINFO_HELP_VALIDATE_INDIRECT_CALL, NULL, CORINFO_HELP_SIG_REG_ONLY)
JITHELPER(CORINFO_HELP_DISPATCH_INDIRECT_CALL, NULL, CORINFO_HELP_SIG_REG_ONLY)
#endif

#undef JITHELPER
#undef DYNAMICJITHELPER
#undef JITHELPER
Expand Down
31 changes: 17 additions & 14 deletions src/coreclr/jit/codegenarmarch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3134,9 +3134,6 @@ void CodeGen::genCall(GenTreeCall* call)
// into a volatile register that won't be restored by epilog sequence.
if (call->IsFastTailCall())
{
// Don't support fast tail calling JIT helpers
assert(call->gtCallType != CT_HELPER);

GenTree* target = getCallTarget(call, nullptr);

if (target != nullptr)
Expand Down Expand Up @@ -3177,22 +3174,28 @@ void CodeGen::genCall(GenTreeCall* call)

genCallInstruction(call);

// if it was a pinvoke we may have needed to get the address of a label
if (genPendingCallLabel)
// for pinvoke/intrinsic/tailcalls we may have needed to get the address of
// a label. In case it is indirect with CFG enabled make sure we do not get
// the address after the validation but only after the actual call that
// comes after.
if (genPendingCallLabel && !call->IsHelperCall(compiler, CORINFO_HELP_VALIDATE_INDIRECT_CALL))
{
genDefineInlineTempLabel(genPendingCallLabel);
genPendingCallLabel = nullptr;
}

// Update GC info:
// All Callee arg registers are trashed and no longer contain any GC pointers.
// TODO-Bug?: As a matter of fact shouldn't we be killing all of callee trashed regs here?
// For now we will assert that other than arg regs gc ref/byref set doesn't contain any other
// registers from RBM_CALLEE_TRASH
assert((gcInfo.gcRegGCrefSetCur & (RBM_CALLEE_TRASH & ~RBM_ARG_REGS)) == 0);
assert((gcInfo.gcRegByrefSetCur & (RBM_CALLEE_TRASH & ~RBM_ARG_REGS)) == 0);
gcInfo.gcRegGCrefSetCur &= ~RBM_ARG_REGS;
gcInfo.gcRegByrefSetCur &= ~RBM_ARG_REGS;
#ifdef DEBUG
// Killed registers should no longer contain any GC pointers.
regMaskTP killMask = RBM_CALLEE_TRASH;
if (call->IsHelperCall())
{
CorInfoHelpFunc helpFunc = compiler->eeGetHelperNum(call->gtCallMethHnd);
killMask = compiler->compHelperCallKillSet(helpFunc);
}

assert((gcInfo.gcRegGCrefSetCur & killMask) == 0);
assert((gcInfo.gcRegByrefSetCur & killMask) == 0);
#endif

var_types returnType = call->TypeGet();
if (returnType != TYP_VOID)
Expand Down
8 changes: 8 additions & 0 deletions src/coreclr/jit/codegencommon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,9 @@ regMaskTP Compiler::compHelperCallKillSet(CorInfoHelpFunc helper)
case CORINFO_HELP_INIT_PINVOKE_FRAME:
return RBM_INIT_PINVOKE_FRAME_TRASH;

case CORINFO_HELP_VALIDATE_INDIRECT_CALL:
return RBM_VALIDATE_INDIRECT_CALL_TRASH;

default:
return RBM_CALLEE_TRASH;
}
Expand Down Expand Up @@ -2204,6 +2207,11 @@ void CodeGen::genGenerateMachineCode()
compiler->fgPgoInlineePgo, compiler->fgPgoInlineeNoPgoSingleBlock, compiler->fgPgoInlineeNoPgo);
}

if (compiler->opts.IsCFGEnabled())
{
printf("; control-flow guard enabled\n");
}

if (compiler->opts.jitFlags->IsSet(JitFlags::JIT_FLAG_ALT_JIT))
{
printf("; invoked as altjit\n");
Expand Down
36 changes: 22 additions & 14 deletions src/coreclr/jit/codegenxarch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5218,9 +5218,6 @@ void CodeGen::genCall(GenTreeCall* call)
// that won't be restored by epilog sequence.
if (call->IsFastTailCall())
{
// Don't support fast tail calling JIT helpers
assert(call->gtCallType != CT_HELPER);

GenTree* target = getCallTarget(call, nullptr);
if (target != nullptr)
{
Expand Down Expand Up @@ -5272,22 +5269,28 @@ void CodeGen::genCall(GenTreeCall* call)

genCallInstruction(call X86_ARG(stackArgBytes));

// if it was a pinvoke or intrinsic we may have needed to get the address of a label
if (genPendingCallLabel)
// for pinvoke/intrinsic/tailcalls we may have needed to get the address of
// a label. In case it is indirect with CFG enabled make sure we do not get
// the address after the validation but only after the actual call that
// comes after.
if (genPendingCallLabel && !call->IsHelperCall(compiler, CORINFO_HELP_VALIDATE_INDIRECT_CALL))
{
genDefineInlineTempLabel(genPendingCallLabel);
genPendingCallLabel = nullptr;
}

// Update GC info:
// All Callee arg registers are trashed and no longer contain any GC pointers.
// TODO-XArch-Bug?: As a matter of fact shouldn't we be killing all of callee trashed regs here?
// For now we will assert that other than arg regs gc ref/byref set doesn't contain any other
// registers from RBM_CALLEE_TRASH.
assert((gcInfo.gcRegGCrefSetCur & (RBM_CALLEE_TRASH & ~RBM_ARG_REGS)) == 0);
assert((gcInfo.gcRegByrefSetCur & (RBM_CALLEE_TRASH & ~RBM_ARG_REGS)) == 0);
gcInfo.gcRegGCrefSetCur &= ~RBM_ARG_REGS;
gcInfo.gcRegByrefSetCur &= ~RBM_ARG_REGS;
#ifdef DEBUG
// Killed registers should no longer contain any GC pointers.
regMaskTP killMask = RBM_CALLEE_TRASH;
if (call->IsHelperCall())
{
CorInfoHelpFunc helpFunc = compiler->eeGetHelperNum(call->gtCallMethHnd);
killMask = compiler->compHelperCallKillSet(helpFunc);
}

assert((gcInfo.gcRegGCrefSetCur & killMask) == 0);
assert((gcInfo.gcRegByrefSetCur & killMask) == 0);
#endif

var_types returnType = call->TypeGet();
if (returnType != TYP_VOID)
Expand Down Expand Up @@ -5563,6 +5566,11 @@ void CodeGen::genCallInstruction(GenTreeCall* call X86_ARG(target_ssize_t stackA
#endif
if (target->isContainedIndir())
{
// When CFG is enabled we should not be emitting any non-register indirect calls.
assert(!compiler->opts.IsCFGEnabled() ||
call->IsHelperCall(compiler, CORINFO_HELP_VALIDATE_INDIRECT_CALL) ||
call->IsHelperCall(compiler, CORINFO_HELP_DISPATCH_INDIRECT_CALL));

if (target->AsIndir()->HasBase() && target->AsIndir()->Base()->isContainedIntOrIImmed())
{
// Note that if gtControlExpr is an indir of an absolute address, we mark it as
Expand Down
14 changes: 14 additions & 0 deletions src/coreclr/jit/compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8845,6 +8845,20 @@ void dTreeLIR(GenTree* tree)
cTreeLIR(JitTls::GetCompiler(), tree);
}

void dTreeRange(GenTree* first, GenTree* last)
{
Compiler* comp = JitTls::GetCompiler();
GenTree* cur = first;
while (true)
{
cTreeLIR(comp, cur);
if (cur == last)
break;

cur = cur->gtNext;
}
}

void dTrees()
{
cTrees(JitTls::GetCompiler());
Expand Down
Loading