Skip to content

JIT: Copies and address lifetime extension can misclassify native local homes as byrefs #135427

Open

Description

@tannergooding

Description

Follow-up to dotnet/runtime#135374 and dotnet/runtime#135381. The descriptor-based unspill fix addresses the reported reload sequence, but two other paths can leave a native-int local's continuing register home incorrectly marked as a byref:

  • Permanent scalar GT_COPY moves use the individual use-node's GC type when producing the new home.
  • Address lifetime-extension helpers temporarily mark the operand's register using its node type, then cannot undo that mark if the register remains a live local home.

These are source-analysis findings, not newly observed runtime crashes.

Reproduction Steps

No executable reproducer has been constructed or run for these additional paths.

The relevant source-level state is a still-live register-candidate local with descriptor type TYP_I_IMPL and an inlined use node typed TYP_BYREF. This mismatch is explicitly permitted by the local-type invariant and constructed by inliner argument substitution.

The copy and loop paths below can positively mark the continuing home as TYP_BYREF. A subsequent native-typed use in the same basic block does not repair that GC classification.

Expected behavior

A continuing local register home retains the local descriptor's GC classification. Genuine temporary GC roots remain reported until their final machine-code use, without changing the classification of a still-live native local home.

Actual behavior

The continuing home can retain an incorrect byref bit. GCInfo::gcMarkRegSetNpt intentionally does not clear registers in GetMaskVars(), so the ordinary cleanup does not undo the positive mark.

LinearScan::spillGCRefs checks the interval type and its most recent use-node type. After a subsequent native-typed use, those checks can miss the stale byref classification retained in codegen's GC masks. If such a register is callee-saved and remains reported at an unmanaged call, this recreates the incorrect GC-reporting condition described in #135374.

Regression?

Pre-existing relative to #135381; these paths are unchanged by that PR. The introducing change has not been identified.

Known Workarounds

None verified.

Configuration

CoreCLR JIT, fixed-register code generation. The scalar copy path is shared; the affected address lifetime-extension sites span xarch, ARM/ARM64, LoongArch64, and RISC-V. No released-version or OS-specific reproduction has been established.

Source references below are pinned to the reviewed #135381 head, 7e5263c8d9e0a5e89846dfd4891fa53e2ccf537a. The relevant paths were also checked against #135415 head, 313f777aa09607a97fe43e4a2cc17f10696569f2, where they remain unchanged.

Other information

Permanent scalar copies: LSRA's fixed-register-conflict path can select moveReg for a local. insertCopyOrReload creates the copy using tree->TypeGet(); only temporary copies get the copy's last-use flag. genRegCopy installs the permanent destination as the live local's new home, then genProduceReg marks it using the copy-node type. Variable-count memory shifts provide a relevant fixed-register conflict through BuildShiftRotate's RCX requirement.

Address lifetime extension: These sites pair gcMarkRegPtrVal(addressReg, operand->TypeGet()) with gcMarkRegSetNpt. Their address need not be a dying temporary: BuildUse uses the original local interval for register-candidate local operands.

Target Affected sites
xarch Returned-value Interlocked.And/Or retry loop; GC-struct zeroing loop
ARM64 Non-LSE exchange/add and compare-exchange retry loops
ARM/ARM64 GC-struct zeroing loop
LoongArch64 GC-struct zeroing loop
RISC-V Compare-exchange retry loop; GC-struct zeroing loop

dotnet/runtime#135415 does not address these sites. Its changes fix emitter GC-state updates for register swaps and memory-result overwrites, plus inactive InlinedCallFrame handling during suspension. The issue here is the continuing local-home classification in codegen's GC masks, not those emitter update omissions.

A fix must preserve genuine GC locals and temporary roots; globally clearing live local homes in gcMarkRegSetNpt would violate its existing contract. Coverage should distinguish permanent home moves from temporary copies and address lifetime extension from last-use cleanup.

Note

This issue was drafted by GitHub Copilot from source analysis.

Activity

  1. added
    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
    on Oct 8, 2026
  2. dotnet-policy-service commented on Oct 8, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
    See info in area-owners.md if you want to be subscribed.

  3. removed
    untriagedNew issue has not been triaged by the area owner
    on Oct 9, 2026
  4. added this to the 12.0.0 milestone on Oct 9, 2026
  5. modified the milestones: 12.0.0, Future on Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions