Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should this unsafe code work also in .NET Core 3?

Tags:

c#

stackalloc

I'm refactoring my libraries to use Span<T> for avoiding heap allocations if possible but as I target also older frameworks I'm implementing some general fallback solutions as well. But now I found a weird issue and I'm not quite sure whether I found a bug in .NET Core 3 or am I doing something illegal.

The issue:

// This returns 1 as expected but cannot be used in older frameworks: private static uint ReinterpretNew() {     Span<byte> bytes = stackalloc byte[4];     bytes[0] = 1; // FillBytes(bytes);      // returning bytes as uint:     return Unsafe.As<byte, uint>(ref bytes.GetPinnableReference()); }  // This returns garbage in .NET Core 3.0 with release build: private static unsafe uint ReinterpretOld() {     byte* bytes = stackalloc byte[4];     bytes[0] = 1; // FillBytes(bytes);      // returning bytes as uint:     return *(uint*)bytes; } 

Interestingly enough, ReinterpretOld works well in .NET Framework and in .NET Core 2.0 (so I could be happy with it after all), still, it bothers me a bit.

Btw. ReinterpretOld can be fixed also in .NET Core 3.0 by a small modification:

//return *(uint*)bytes; uint* asUint = (uint*)bytes; return *asUint; 

My Question:

Is this a bug or does ReinterpretOld work in older frameworks only by accident and should I apply the fix also for them?

Remarks:

  • The debug build works also in .NET Core 3.0
  • I tried to apply [MethodImpl(MethodImplOptions.NoInlining)] to ReinterpretOld but it had no effect.
like image 976
György Kőszeg Avatar asked Nov 26 '19 13:11

György Kőszeg


People also ask

How do I allow unsafe code in Visual Studio?

To set the unsafe option in Visual Studio 2012, click on Project in the main menu, select the Build pane, and check the box that says "allow unsafe code."

Why do we use unsafe in C#?

Unsafe code in C# isn't necessarily dangerous; it's just code whose safety cannot be verified. Unsafe code has the following properties: Methods, types, and code blocks can be defined as unsafe. In some cases, unsafe code may increase an application's performance by removing array bounds checks.

What is the difference between unsafe code and unmanaged code?

This is responsible for things like memory management and garbage collection. So unmanaged simply runs outside of the context of the CLR. unsafe is kind of "in between" managed and unmanaged. unsafe still runs under the CLR, but it will let you access memory directly through pointers.

What does it mean for code to be unsafe?

What Does Unsafe Mean? Unsafe is a C programming language (C#) keyword used to denote a section of code that is not managed by the Common Language Runtime (CLR) of the . NET Framework, or unmanaged code.


1 Answers

Ooh, this is a fun find; what is happening here is that your local is getting optimized away - there are no locals remaining, which means that there is no .locals init, which means that stackalloc behaves differently, and does not wipe the space;

private static unsafe uint Reinterpret1() {     byte* bytes = stackalloc byte[4];     bytes[0] = 1;      return *(uint*)bytes; }  private static unsafe uint Reinterpret2() {     byte* bytes = stackalloc byte[4];     bytes[0] = 1;      uint* asUint = (uint*)bytes;     return *asUint; } 

becomes:

.method private hidebysig static uint32 Reinterpret1() cil managed {     .maxstack 8     L_0000: ldc.i4.4      L_0001: conv.u      L_0002: localloc      L_0004: dup      L_0005: ldc.i4.1      L_0006: stind.i1      L_0007: ldind.u4      L_0008: ret  }  .method private hidebysig static uint32 Reinterpret2() cil managed {     .maxstack 3     .locals init (         [0] uint32* numPtr)     L_0000: ldc.i4.4      L_0001: conv.u      L_0002: localloc      L_0004: dup      L_0005: ldc.i4.1      L_0006: stind.i1      L_0007: stloc.0      L_0008: ldloc.0      L_0009: ldind.u4      L_000a: ret  } 

I think I'd be happy to say that this is a compiler bug, or at least: an undesirable side-effect and behavior given that previous decisions have been put in place to say "emit the .locals init", specifically to try and keep stackalloc sane - but whether the compiler folks agree is up to them.

The workaround is: treat the stackalloc space as undefined (which, to be fair, is what you're meant to do); if you expect it to be zeros: manually zero it.

like image 102
Marc Gravell Avatar answered Sep 20 '22 17:09

Marc Gravell