Skip to content

Commit

Permalink
Add JIT support for control-flow guard on x64 and arm64 (#63763)
Browse files Browse the repository at this point in the history
Add support for generating control-flow guard checks. The support is enabled by a JIT flag or by setting COMPlus_JitForceControlFlowGuard=1.

Co-authored-by: Michal Strehovský <[email protected]>
  • Loading branch information
jakobbotsch and MichalStrehovsky authored Feb 8, 2022
1 parent b305a17 commit 3fc8b09
Show file tree
Hide file tree
Showing 38 changed files with 747 additions and 110 deletions.
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

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
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 @@ -8848,6 +8848,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

0 comments on commit 3fc8b09

Please sign in to comment.