Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(unsafe) Object Header Verification may read to finalizer queue acting on ValueType or interface #39554

Closed
juliusfriedman opened this issue Jul 17, 2020 · 10 comments
Labels
question Answer questions and provide assistance, not an issue with source code or documentation.

Comments

@juliusfriedman
Copy link
Contributor

juliusfriedman commented Jul 17, 2020

using System;
public class C {
    
    public interface Test{}
    public abstract class WhoCares : Test{}
    public struct Important : Test{}
    
    public unsafe static void M<T>(T t) where T :struct, Test
    {
        //ref T x = ref System.Runtime.CompilerServices.Unsafe.AsRef(t);
        ref T y = ref t;
        ref byte b = ref System.Runtime.CompilerServices.Unsafe.As<T, byte>(ref y);
        
        IntPtr ptr = (IntPtr)System.Runtime.CompilerServices.Unsafe.AsPointer<byte>(ref b);
            
        IntPtr mt = IntPtr.Subtract(ptr, 1);
        
        //Should just be the interface bit
        IntPtr fakeHeader = (IntPtr)((1 == ((uint)1) << 4) ? uint.MaxValue : ulong.MaxValue);
        
        System.Runtime.InteropServices.Marshal.WriteIntPtr((IntPtr)mt, fakeHeader);
    }
    
    public static void Main()
    {
        M<Important>(default);
    }
}

If I have to go to the typeof(T) it's not much different...

Now this object gets into the finalizer queue!

Is this intended behavior and shouldn't there be checks for these kinds of hacks?

cc @jkotas @danmosemsft @GrabYourPitchforks

@Dotnet-GitSync-Bot
Copy link
Collaborator

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added the untriaged New issue has not been triaged by the area owner label Jul 17, 2020
@juliusfriedman juliusfriedman changed the title (unsafe) Object Header Verification may read to finalizer queue acting on ValueType (unsafe) Object Header Verification may read to finalizer queue acting on ValueType or interface Jul 17, 2020
@jkotas
Copy link
Member

jkotas commented Jul 17, 2020

If you are hacking on runtime internals like this using unsafe code, you are in unsupported territory. We do not make any guarantees about the behavior in this case.

Your program can hang, crash, silently corrupt data, format your hard drive, ... . All these behaviors would be by design.

@jkotas jkotas closed this as completed Jul 17, 2020
@juliusfriedman
Copy link
Contributor Author

Thank you for your time!

@jkotas jkotas added the question Answer questions and provide assistance, not an issue with source code or documentation. label Jul 17, 2020
@juliusfriedman
Copy link
Contributor Author

juliusfriedman commented Jul 17, 2020

using System;
public class C {
    
    public interface Test{}
    public abstract class WhoCares : Test{}
    public struct Important : Test{}
    
    public static void M<T>(T t) where T :struct, Test
    {
        //ref T x = ref System.Runtime.CompilerServices.Unsafe.AsRef(t);
        ref T y = ref t;
        ref byte b = ref System.Runtime.CompilerServices.Unsafe.As<T, byte>(ref y);
        
        ref T ptr = ref System.Runtime.CompilerServices.Unsafe.As<byte, T>(ref b);
            
        ref byte mt2 = ref System.Runtime.CompilerServices.Unsafe.AddByteOffset(ref b, new IntPtr(-1));
        
        ref T mt = ref System.Runtime.CompilerServices.Unsafe.AddByteOffset<T>(ref ptr, new IntPtr(-1));
        
        //Should just be the interface bit
        IntPtr fakeHeader = (IntPtr)((1 == ((uint)1) << 4) ? uint.MaxValue : ulong.MaxValue);
        
        //System.Runtime.InteropServices.Marshal.WriteIntPtr((IntPtr)mt, fakeHeader);
    }
    
    public static void Main()
    {
        M<Important>(default);
    }
}

Consider also

ref byte b = ref System.Runtime.CompilerServices.Unsafe.AddByteOffset(ref System.Runtime.CompilerServices.Unsafe.As<T, byte>(ref y), new IntPtr((-1));

@danmoseley
Copy link
Member

@juliusfriedman are you asking a question?

@juliusfriedman
Copy link
Contributor Author

juliusfriedman commented Jul 17, 2020

No I am just saying that:

using System;
public class C {
    
    public interface Test{}
    public abstract class WhoCares : Test{}
    public struct Important : Test{}
    
    public static void M<T>(T t) where T :struct, Test
    {
        //ref T x = ref System.Runtime.CompilerServices.Unsafe.AsRef(t);
        ref T y = ref t;
        ref byte b = ref System.Runtime.CompilerServices.Unsafe.AddByteOffset(ref System.Runtime.CompilerServices.Unsafe.As<T, byte>(ref y), new IntPtr((-1)));
        
        b = byte.MaxValue;
		for(int i = 1; i < IntPtr.Size; ++i)
		{
			b = ref System.Runtime.CompilerServices.Unsafe.AddByteOffset(ref System.Runtime.CompilerServices.Unsafe.As<T, byte>(ref y), new IntPtr((-1)));
			b = byte.MaxValue;
		}
    }
    
    public static void Main()
    {
        M<Important>(default(Important));
    }
}

compiles and results in:

Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at C.M[T](T t)
Command terminated by signal 6

And this is not unsafe code and I don't have a class in use.

I could take that step further but I will stop there as the issue was closed but I just wanted to assert I can do it without unsafe either thanks to Unsafe. (really ref)

@jkotas
Copy link
Member

jkotas commented Jul 17, 2020

Right, unsafe C# keyword does not prevent you from using unsafe library functions. #31354 is about that.

@juliusfriedman
Copy link
Contributor Author

juliusfriedman commented Jul 17, 2020

Consider also:

using System;
public class C {
    
    public interface Test{}
    public abstract class WhoCares : Test{}
    public struct Important : Test{}
        
    public static void M<T>(T t) where T :struct, Test
    {        
        ref T y = ref t;
        ref byte b = ref System.Runtime.CompilerServices.Unsafe.AddByteOffset(ref System.Runtime.CompilerServices.Unsafe.As<T, byte>(ref y), new IntPtr((-2)));   
        b = 0;
		for(int i = 1; i < IntPtr.Size; ++i)
		{
			b = ref System.Runtime.CompilerServices.Unsafe.AddByteOffset(ref System.Runtime.CompilerServices.Unsafe.As<T, byte>(ref y), new IntPtr((1)));
			b = byte.MaxValue;
		}
    }
    
    public static void Main()
    {
        M<Important>(default(Important));
    }
}

Runs fine indicating there is only memory checking on partial components of the object header... Since the HashCode is known I think we can do better at the expense of some additional memory but not much considering we already track the refs.

@john-h-k
Copy link
Contributor

There isn't "memory checking" anywhere causing your error. You just wrecked the header to the point where a component tried to dereference what it expected to be a valid pointer wasn't so it AV'd. The object header is pretty well packed with data, i can't remember the file the flags for it are in but I've looked at it before a couple of times

@sylveon
Copy link
Contributor

sylveon commented Jul 18, 2020

Access violations are always interpreted as NRE because in safe/UB-free code accessing a null pointer is the only way to trigger an AV.

@GrabYourPitchforks GrabYourPitchforks removed the untriaged New issue has not been triaged by the area owner label Jul 18, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
question Answer questions and provide assistance, not an issue with source code or documentation.
Projects
None yet
Development

No branches or pull requests

7 participants