Skip to content

JIT: potential GC hole with collectible code in delegate #105082

Open

Description

@huoyaoyuan

Originally discovered in #104731 (comment)

When a delegate references collectible code (dynamic method or collectible assembly), and being the only thing keeping the code alive, the code may be collected before invoked.

Consider the following method:

        [MethodImpl(MethodImplOptions.NoInlining)]
        static void Test(Action<int> action, int counts)
        {
            int sum = 0;
            for (int i = 0; i < counts; i++)
            {
                sum += i;
            }
            action(sum);
        }

Current codegen on arm64:

; Assembly listing for method CSPlayground.Program:Test(System.Action`1[int],int) (FullOpts)
; Emitting BLENDED_CODE for generic ARM64 - Windows
; FullOpts code
; optimized code
; fp based frame
; fully interruptible
; No PGO data
; invoked as altjit
; Final local variable assignments
;
;  V00 arg0         [V00,T03] (  4,  4   )     ref  ->   x2         class-hnd single-def <System.Action`1[int]>
;  V01 arg1         [V01,T02] (  4,  7   )     int  ->   x1         single-def
;  V02 loc0         [V02,T01] (  4, 10   )     int  ->   x0        
;  V03 loc1         [V03,T00] (  5, 17   )     int  ->   x3        
;# V04 OutArgs      [V04    ] (  1,  1   )  struct ( 0) [sp+0x00]  do-not-enreg[XS] addr-exposed "OutgoingArgSpace"
;
; Lcl frame size = 0

G_M16562_IG01:        ; bbWeight=1, gcrefRegs=0000 {}, byrefRegs=0000 {}, byref, nogc <-- Prolog IG
            stp     fp, lr, [sp, #-0x10]!
            mov     fp, sp
            mov     x2, x0
                             ; gcrRegs +[x2]
						;; size=12 bbWeight=1 PerfScore 2.00
G_M16562_IG02:        ; bbWeight=1, gcrefRegs=0004 {x2}, byrefRegs=0000 {}, byref, isz, align
            mov     w0, wzr
            mov     w3, wzr
            cmp     w1, #0
            ble     G_M16562_IG04
            align   [4 bytes for IG03]
            align   [0 bytes]
            align   [0 bytes]
            align   [0 bytes]
						;; size=20 bbWeight=1 PerfScore 3.00
G_M16562_IG03:        ; bbWeight=4, gcrefRegs=0004 {x2}, byrefRegs=0000 {}, loop=IG03, byref, isz
            add     w0, w0, w3
            add     w3, w3, #1
            cmp     w3, w1
            blt     G_M16562_IG03
						;; size=16 bbWeight=4 PerfScore 10.00
G_M16562_IG04:        ; bbWeight=1, gcrefRegs=0004 {x2}, byrefRegs=0000 {}, byref
            mov     w1, w0
            ldr     x0, [x2, #0x08]
                             ; gcrRegs +[x0]
            ldr     x2, [x2, #0x18]
                             ; gcrRegs -[x2]
            blr     x2
                             ; gcrRegs -[x0]
                             ; gcr arg pop 0
						;; size=16 bbWeight=1 PerfScore 7.50
G_M16562_IG05:        ; bbWeight=1, epilog, nogc, extend
            ldp     fp, lr, [sp], #0x10
            ret     lr
						;; size=8 bbWeight=1 PerfScore 2.00

; Total bytes of code 72, prolog size 12, PerfScore 24.50, instruction count 21, allocated bytes for code 72 (MethodHash=da6bbf4d) for method CSPlayground.Program:Test(System.Action`1[int],int) (FullOpts)

The ldr x2, [x2, #0x18] instruction loads the function pointer from the delegate object. Now no register holds the delegate object, and GC is not aware of the function pointer in x2. If GC happens before the next blr x2 instruction, it may collect the delegate object and associated code.

The loop in front of the invocation makes the method fully interruptible, and thus GC can happen at any instruction except prolog/epilog.

All RISC architectures should be suffered from this issue. In xarch, loading the function pointer can be contained in the call instruction like call [rax+0x18], so GC won't lose the track.

Activity

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

    @dotnet-policy-service
    Contributor

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

  3. self-assigned this
    on Jul 18, 2024
  4. removed
    untriagedNew issue has not been triaged by the area owner
    on Jul 18, 2024
  5. added this to the 9.0.0 milestone on Jul 18, 2024
  6. EgorBo commented on Jul 18, 2024

    @EgorBo
    Member

    Thanks for noticing the issue!

  7. huoyaoyuan commented on Jul 18, 2024

    @huoyaoyuan
    MemberAuthor

    An ideal fix would be making GC info aware of the function pointer in register.

    GC should treat it like some IP that appears in a frame of the stack, to keep the code alive. It's not a brand new concept, just need to properly report it.

  8. added
    Priority:2Work that is important, but not critical for the release
    on Jul 22, 2024
  9. added
    in-prThere is an active PR which will close this issue when it is merged
    on Aug 1, 2024
  10. modified the milestones: 9.0.0, 10.0.0 on Nov 19, 2024
  11. modified the milestones: 10.0.0, 11.0.0 on Sep 29, 2025
  12. added 2 commits that reference this issue on Sep 3, 2026
    7518178
    b2c6ce8
  13. modified the milestones: 11.0.0, 12.0.0 on Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Priority:2Work that is important, but not critical for the releasearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIin-prThere is an active PR which will close this issue when it is merged

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions