Skip to content

A possible ref safety "hole" around UnscopedRef method invoked on an expression classified as a value or readonly variable of a struct type  #67435

@AlekseyTs

Description

@AlekseyTs

Compile and run the following code:

https://sharplab.io/#v2:EYLgtghglgdgNAFxAJwK4wD4GIaoDZ4TB4CmABACZQDORpAsAFBMuMACATGQMJMDeTMkLJsAjADYyAZTIBZUQAoAlGQC8APjIwSAd2nKyfMgDE1ZURbIBfANythIiWVgI5HBchIAzZzFcAPOGkyAE8lQWEBRgcHNgB2Mn87aOEre2ExSRc5AGZlCKEomIyE2XdPH3llADoAJW9lIIpvCHwEJWSHNOYUoUzfV1kAFnzewwKY+LcPbzlFJTqGpSDtPSkDI1NVcw4c6w6J7on+7NkAVlGHIuKyADcIZESzVf0VTbNRDiHrTpupspmlXmiy8jUSBzG3QcxycbG+smgMEukQmsVE1TEAE4FLI8koITdHBjRNjhsoCX90VicRd8b8hN0jj02HtqAg0ABjVxSfjHPbZYzJCYAbTE1QAIlAIABzGAAezZUA51Gq3DlzQAgjAIHgQtQaNUAKowagcuUABxIFHqXgAunyyBUBmQbcjCqiSo7ZoLDkxukA=

class C
{
    static S M1() => new S() { F = 111 };

    static int M2(ref int x, S y)
    {
        return x;
    }

    static int M3()
    {
        return M2(ref M1().Ref(), default);
    }

    static int M4()
    {
        return M2(ref M1().Ref(), new S() { F = 123 });
    }

    static int M5()
    {
        var x = new S() { F = 124 };
        return M2(ref M1().Ref(), x);
    }
    
    static void Main()
    {
        System.Console.WriteLine(M3());
        System.Console.WriteLine(M4());
        System.Console.WriteLine(M5());
    }
}


public struct S
{
    public int F;

    [System.Diagnostics.CodeAnalysis.UnscopedRef]
    public ref int Ref()
    {
        return ref F;
    }
}

Observed:

0
123
111

Expected either an error for Ref invocations, or the following output:

111
111
111

The problem is the fact, that Ref is invoked on a value (vs. a variable) and its result refers to a memory in a short lived temp synthesized by compiler. The lifetime of that temp is the Ref invocation. Compiler is free to reuse it afterwards. Effectively, we end up with a reference to a memory that is no longer alive. This can be observed in IL for M3:

    .method private hidebysig static 
        int32 M3 () cil managed 
    {
        // Method begins at RVA 0x208c
        // Code size 28 (0x1c)
        .maxstack 2
        .locals init (
            [0] valuetype S
        )

        IL_0000: call valuetype S C::M1()
        IL_0005: stloc.0
        IL_0006: ldloca.s 0
        IL_0008: call instance int32& S::Ref()
        IL_000d: ldloca.s 0
        IL_000f: initobj S
        IL_0015: ldloc.0
        IL_0016: call int32 C::M2(int32&, valuetype S)
        IL_001b: ret
    } // end of method C::M3

Note that local 0 is used with different values in the process of calculating both arguments for M2.

Perhaps compiler should disallow invoking UnscopedRef methods on expressions classified as values of struct types. It should be fine to invoke such methods on expressions classified as variables of struct types.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions