Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit c108c9d

Browse files
committed
Avoid creating illegal byref pointers
Byref pointers need to point within their "host" object -- thus the alternate name "interior pointers". If the JIT creates and reports a pointer as a "byref", but it points outside the host object, and a GC occurs that moves the host object, the byref pointer will not be updated. If a subsequent calculation puts the byref "back" into the host object, it will actually be pointing to garbage, since the host object has moved. This occurred on ARM with array index calculations, in particular because ARM doesn't have a single-instruction "base + scale*index + offset" addressing mode. Thus, we were generating, for the jaggedarr_cs_do test case, `ProcessJagged3DArray()` function: ``` // r0 = array object, r6 = computed index offset. We mark r4 as a byref. add r4, r0, r6 // r4 - 32 is the offset of the object we care about. Then we load the array element. // In this case, the loaded element is a gcref, so r4 becomes a gcref. ldr r4, [r4-32] ``` We get this math because the user code uses `a[i - 10]`, which is essentially `a + (i - 10) * 4 + 8` for element size 4. This is optimized to `a + i * 4 - 32`. In the above code, `r6` is `i * 4`. In this case, after the first instruction, `r4` can point beyond the array. If a GC happens, `r4` isn't updated, and the second instruction loads garbage. There are several fixes: 1. Change array morphing in `fgMorphArrayIndex()` to rearrange the array index IR node creation to only create a byref pointer that is precise; don't create "intermediate" byref pointers that don't represent the actual array element address being computed. The tree matching code that annotates the generated tree with field sequences needs to be updated to match the new form. 2. Change `fgMoveOpsLeft()` to prevent the left-weighted reassociation optimization `[byref]+ (ref, [int]+ (int, int)) => [byref]+ ([byref]+ (ref, int), int)`. This optimization creates "incorrect" byrefs that don't necessarily point within the host object. 3. Add an additional condition to the `Fold "((x+icon1)+icon2) to (x+(icon1+icon2))"` morph optimization to prevent merging of constant TYP_REF nodes, which now were being recognized due to different tree shapes. This was probably always a problem, but the particular tree shape wasn't seen before. These fixes are all-platform. However, to reduce risk at this point, the are enabled for ARM only, under the `FEATURE_PREVENT_BAD_BYREFS` `#ifdef`. Fixes #17517. There are many, many diffs. For ARM32 ngen-based desktop asm diffs, it is a 0.30% improvement across all framework assemblies. A lot of the diffs seem to be because we CSE the entire array address offset expression, not just the index expression.
1 parent 4eb8b37 commit c108c9d

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

src/jit/morph.cpp

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5810,6 +5810,25 @@ void Compiler::fgMoveOpsLeft(GenTree* tree)
58105810
break;
58115811
}
58125812

5813+
#if FEATURE_PREVENT_BAD_BYREFS
5814+
5815+
// Don't split up a byref calculation and create a new byref. E.g.,
5816+
// [byref]+ (ref, [int]+ (int, int)) => [byref]+ ([byref]+ (ref, int), int).
5817+
// Doing this transformation could create a situation where the first
5818+
// addition (that is, [byref]+ (ref, int) ) creates a byref pointer that
5819+
// no longer points within the ref object. If a GC happens, the byref won't
5820+
// get updated. This can happen, for instance, if one of the int components
5821+
// is negative. It also requires the address generation be in a fully-interruptible
5822+
// code region.
5823+
//
5824+
if (varTypeIsGC(op1->TypeGet()) && op2->TypeGet() == TYP_I_IMPL)
5825+
{
5826+
assert(varTypeIsGC(tree->TypeGet()) && (oper == GT_ADD));
5827+
break;
5828+
}
5829+
5830+
#endif // FEATURE_PREVENT_BAD_BYREFS
5831+
58135832
/* Change "(x op (y op z))" to "(x op y) op z" */
58145833
/* ie. "(op1 op (ad1 op ad2))" to "(op1 op ad1) op ad2" */
58155834

@@ -6224,6 +6243,27 @@ GenTree* Compiler::fgMorphArrayIndex(GenTree* tree)
62246243
addr = index;
62256244
}
62266245

6246+
#if FEATURE_PREVENT_BAD_BYREFS
6247+
6248+
// Be careful to only create the byref pointer when the full index expression is added to the array reference.
6249+
// We don't want to create a partial byref address expression that doesn't include the full index offset:
6250+
// a byref must point within the containing object. It is dangerous (especially when optimizations come into
6251+
// play) to create a "partial" byref that doesn't point exactly to the correct object; there is risk that
6252+
// the partial byref will not point within the object, and thus not get updated correctly during a GC.
6253+
// This is mostly a risk in fully-interruptible code regions.
6254+
6255+
/* Add the first element's offset */
6256+
6257+
GenTree* cns = gtNewIconNode(elemOffs, TYP_I_IMPL);
6258+
6259+
addr = gtNewOperNode(GT_ADD, TYP_I_IMPL, addr, cns);
6260+
6261+
/* Add the object ref to the element's offset */
6262+
6263+
addr = gtNewOperNode(GT_ADD, TYP_BYREF, arrRef, addr);
6264+
6265+
#else // !FEATURE_PREVENT_BAD_BYREFS
6266+
62276267
/* Add the object ref to the element's offset */
62286268

62296269
addr = gtNewOperNode(GT_ADD, TYP_BYREF, arrRef, addr);
@@ -6234,6 +6274,8 @@ GenTree* Compiler::fgMorphArrayIndex(GenTree* tree)
62346274

62356275
addr = gtNewOperNode(GT_ADD, TYP_BYREF, addr, cns);
62366276

6277+
#endif // !FEATURE_PREVENT_BAD_BYREFS
6278+
62376279
#if SMALL_TREE_NODES
62386280
assert((tree->gtDebugFlags & GTF_DEBUG_NODE_LARGE) || GenTree::s_gtNodeSizes[GT_IND] == TREE_NODE_SZ_SMALL);
62396281
#endif
@@ -6329,6 +6371,35 @@ GenTree* Compiler::fgMorphArrayIndex(GenTree* tree)
63296371
GenTree* cnsOff = nullptr;
63306372
if (addr->OperGet() == GT_ADD)
63316373
{
6374+
6375+
#if FEATURE_PREVENT_BAD_BYREFS
6376+
6377+
assert(addr->TypeGet() == TYP_BYREF);
6378+
assert(addr->gtOp.gtOp1->TypeGet() == TYP_REF);
6379+
6380+
addr = addr->gtOp.gtOp2;
6381+
6382+
// Look for the constant [#FirstElem] node here, or as the RHS of an ADD.
6383+
6384+
if (addr->gtOper == GT_CNS_INT)
6385+
{
6386+
cnsOff = addr;
6387+
addr = nullptr;
6388+
}
6389+
else
6390+
{
6391+
if ((addr->OperGet() == GT_ADD) && (addr->gtOp.gtOp2->gtOper == GT_CNS_INT))
6392+
{
6393+
cnsOff = addr->gtOp.gtOp2;
6394+
addr = addr->gtOp.gtOp1;
6395+
}
6396+
6397+
// Label any constant array index contributions with #ConstantIndex and any LclVars with GTF_VAR_ARR_INDEX
6398+
addr->LabelIndex(this);
6399+
}
6400+
6401+
#else // !FEATURE_PREVENT_BAD_BYREFS
6402+
63326403
if (addr->gtOp.gtOp2->gtOper == GT_CNS_INT)
63336404
{
63346405
cnsOff = addr->gtOp.gtOp2;
@@ -6346,6 +6417,8 @@ GenTree* Compiler::fgMorphArrayIndex(GenTree* tree)
63466417
addr = addr->gtOp.gtOp1;
63476418
}
63486419
assert(addr->TypeGet() == TYP_REF);
6420+
6421+
#endif // !FEATURE_PREVENT_BAD_BYREFS
63496422
}
63506423
else if (addr->OperGet() == GT_CNS_INT)
63516424
{
@@ -13477,9 +13550,25 @@ GenTree* Compiler::fgMorphSmpOp(GenTree* tree, MorphAddrContext* mac)
1347713550
if (op2->IsCnsIntOrI() && varTypeIsIntegralOrI(typ))
1347813551
{
1347913552
/* Fold "((x+icon1)+icon2) to (x+(icon1+icon2))" */
13553+
CLANG_FORMAT_COMMENT_ANCHOR;
13554+
13555+
#if FEATURE_PREVENT_BAD_BYREFS
13556+
13557+
if (op1->gtOper == GT_ADD && //
13558+
!gtIsActiveCSE_Candidate(op1) && //
13559+
!op1->gtOverflow() && //
13560+
op1->gtOp.gtOp2->IsCnsIntOrI() && //
13561+
(op1->gtOp.gtOp2->OperGet() == op2->OperGet()) && //
13562+
(op1->gtOp.gtOp2->TypeGet() != TYP_REF) && // Don't fold REFs
13563+
(op2->TypeGet() != TYP_REF)) // Don't fold REFs
13564+
13565+
#else // !FEATURE_PREVENT_BAD_BYREFS
1348013566

1348113567
if (op1->gtOper == GT_ADD && !gtIsActiveCSE_Candidate(op1) && op1->gtOp.gtOp2->IsCnsIntOrI() &&
1348213568
!op1->gtOverflow() && op1->gtOp.gtOp2->OperGet() == op2->OperGet())
13569+
13570+
#endif // !FEATURE_PREVENT_BAD_BYREFS
13571+
1348313572
{
1348413573
cns1 = op1->gtOp.gtOp2;
1348513574
op2->gtIntConCommon.SetIconValue(cns1->gtIntConCommon.IconValue() +

src/jit/target.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,21 @@ typedef unsigned short regPairNoSmall; // arm: need 12 bits
317317
// #define PSEUDORANDOM_NOP_INSERTION
318318
// #endif
319319

320+
// TODO-Cleanup: FEATURE_PREVENT_BAD_BYREFS guards code that prevents creating byref pointers to array elements
321+
// that are not "complete". That is, it only allows byref pointers to the exact array element, not to a portion
322+
// of the address expression leading to the full addressing expression. This prevents the possibility of creating
323+
// an illegal byref, which is an expression that points outside of the "host" object. Such bad byrefs won't get
324+
// updated properly during a GC, leaving them to point to garbage. This led to a GC hole on ARM due to ARM's
325+
// limited addressing modes (found in GCStress). The change is applicable and possibly desirable for other platforms,
326+
// but was put under ifdef to avoid introducing potential destabilizing change to those platforms at the end of the
327+
// .NET Core 2.1 ship cycle. More detail here: https://github.com/dotnet/coreclr/pull/17524. Consider making this
328+
// all-platform and removing these #ifdefs.
329+
#if defined(_TARGET_ARM_)
330+
#define FEATURE_PREVENT_BAD_BYREFS 1
331+
#else
332+
#define FEATURE_PREVENT_BAD_BYREFS 0
333+
#endif
334+
320335
/*****************************************************************************/
321336

322337
// clang-format off

0 commit comments

Comments
 (0)