`Iot.Device.Common.ValueArray<T>` is not safe and contains GC holes
See original GitHub issueDescribe the bug I want to preface this by stating that I’m not a consumer of these APIs, I just happened to come across this while perusing source.dot.net.
The ValueArray<T>
type introduced in https://github.com/dotnet/iot/pull/1701 stores a sequence of 8 elements, and allows consumers to retrieve a Span<T>
wrapping them. However, the method for which it does this is not safe and causes the GC to lose track of the references, meaning that the object they belong to may be eligible for garbage collection while the Span<T>
is in use.
For reference, this is the current implementation of ValueArray<T>.AsSpan
:
public unsafe Span<T> AsSpan()
{
return new Span<T>(Unsafe.AsPointer(ref _e0), _count);
}
And this would be an implementation that retains GC tracking (compatible with .NET Core/Standard 2.1 and newer):
public Span<T> AsSpan()
{
return MemoryMarshal.CreateSpan(ref _e0, _count);
}
Unfortunately, MemoryMarshal.CreateSpan
is not available on .NET Standard 2.0. A close equivalent to this API used to exist that would have made it possible to create Span<T>
instances for ValueArray<T>
s stored within classes safely (see https://github.com/dotnet/runtime/issues/27690 and https://github.com/dotnet/runtime/issues/24562).
As the ValueArray<T>
type is stored within class
es, changing it to a ref struct
is also not an option, meaning that there is no safe way to implement AsSpan
on ValueArray<T>
under .NET Standard 2.0.
Issue Analytics
- State:
- Created a year ago
- Reactions:5
- Comments:17 (9 by maintainers)
Top GitHub Comments
An
unmanaged
type is merely astruct
that doesn’t contain any GC pointers. I assume you meant that it does not allow managed types? This is also not true though, as while it disallows using aclass
directly forT
, you can still use astruct
that contains aclass
as one of its fields.So, as a result,
ValueArray<T>
can indirectly contain a GC pointer ifT
does, because it is constrained tostruct
and notunmanaged
. Either way though, the GC issues still apply.To be clear here: there are two separate issues at play.
On .NET Framework, Span uses a “slower” implementation that needs to keep track of an
object
to keep the object live. This is not the case on modern runtimes where the GC allows it to track aref
directly. This is why creating the span like this can cause the object to be prematurely GC’d as the above code shows, and also why there’s no safe way to do this in NS2.0.The usage of
Unsafe.AsPointer
however is still a problem on modern runtimes. If the GC runs the split moment after the call toAsPointer
but before the span is constructed, the GC could move the backing object without updating the unmanaged pointer (depending on how aggressive the JIT is with tracking locals this might also result in “object freed prematurely” though).Also if you use this value array in a local, you can return the span up the stack and the language won’t tell you, allowing you to access garbage memory.