Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does the CLR allow mutating boxed immutable value types?

I have a situation where I have a simple, immutable value type:

public struct ImmutableStruct
{
    private readonly string _name;

    public ImmutableStruct( string name )
    {
        _name = name;
    }

    public string Name
    {
        get { return _name; }
    }
}

When I box an instance of this value type, I would normally expect that whatever it is that I boxed would come out the same when I do an unbox. To my big suprise this is not the case. Using Reflection someone may easily modify my box's memory by reinitializing the data contained therein:

class Program
{
    static void Main( string[] args )
    {
        object a = new ImmutableStruct( Guid.NewGuid().ToString() );

        PrintBox( a );
        MutateTheBox( a );
        PrintBox( a );;
    }

    private static void PrintBox( object a )
    {
        Console.WriteLine( String.Format( "Whats in the box: {0} :: {1}", ((ImmutableStruct)a).Name, a.GetType() ) );
    }

    private static void MutateTheBox( object a )
    {
        var ctor = typeof( ImmutableStruct ).GetConstructors().Single();
        ctor.Invoke( a, new object[] { Guid.NewGuid().ToString() } );
    }
}

Sample output:

Whats in the box: 013b50a4-451e-4ae8-b0ba-73bdcb0dd612 :: ConsoleApplication1.ImmutableStruct Whats in the box: 176380e4-d8d8-4b8e-a85e-c29d7f09acd0 :: ConsoleApplication1.ImmutableStruct

(There's actually a small hint in the MSDN that indicates this is the intended behavior)

Why does the CLR allow mutating boxed (immutable) value types in this subtle way? I know that readonly is no guarantee, and I know that using "traditional" reflection a value instance can be easily mutated. This behavior becomes an issue, when the reference to the box is copied around and mutations show up in unexpected places.

One thing I have though about is that this enables using Reflection on value types at all - since the System.Reflection API works with object only. But Reflection breaks apart when using Nullable<> value types (they get boxed to null if they do not have a Value). Whats the story here?

like image 680
Johannes Rudolph Avatar asked Aug 22 '11 16:08

Johannes Rudolph


People also ask

Why are value types immutable?

The most basic types (numbers, strings, and null) are inherently immutable because there is nothing (field/property) to change about them. A 5 is a 5 is a 5. Any operation on the 5 only returns another immutable value.

What is immutable reference type?

Reference type whose object value cannot be modified once created. Immutable type variables can be used with same assumptions as primitive type variables that is, when immutable type variables are passed to method as arguments, the object value of that variable is guaranteed to be preserved.


1 Answers

Boxes aren't immutable as far as the CLR is concerned. Indeed, in C++/CLI I believe there's a way of mutating them directly.

However, in C# the unboxing operation always takes a copy - it's the C# language which prevents you from mutating the box, not the CLR. The IL unbox instruction merely provides a typed pointer into the box. From section 4.32 of partition III of ECMA-335 (the unbox instruction):

The unbox instruction converts obj (of type O), the boxed representation of a value type, to valueTypePtr (a controlled-mutability managed pointer (§1.8.1.2.2), type &), its unboxed form. valuetype is a metadata token (a typeref, typedef or typespec). The type of valuetype contained within obj must be verifier-assignable-to valuetype.

Unlike box, which is required to make a copy of a value type for use in the object, unbox is not required to copy the value type from the object. Typically it simply computes the address of the value type that is already present inside of the boxed object.

The C# compiler always generates IL which results in unbox being followed by a copying operation, or unbox.any which is equivalent to unbox followed by ldobj. The generated IL isn't part of the C# spec of course, but this is (section 4.3 of the C# 4 spec):

An unboxing operation to a non-nullable-value-type consists of first checking that the object instance is a boxed value of the given non-nullable-value-type, and then copying the value out of the instance.

Unboxing to a nullable-type produces the null value of the nullable-type if the source operand is null, or the wrapped result of unboxing the object instance to the underlying type of the nullable-type otherwise.

In this case, you're using reflection and therefore bypassing the protection offered by C#. (It's a particularly odd use of reflection too, I must say... calling a constructor "on" a target instance is very strange - I don't think I've ever seen that before.)

like image 144
Jon Skeet Avatar answered Sep 27 '22 19:09

Jon Skeet