Skip to content

Span2D/ReadOnlySpan2D.TryGetSpan() has incorrect implementation for legacy framework #3947

@Ilia-Kosenkov

Description

@Ilia-Kosenkov

Describe the bug

I will use Span2D<T> as an example, everything described is also applicable to ReadOnlySpan2D<T> as the codebase is similar.

Method Span2D<T>.TryGetSpan(out Span<T> span) is used in several places, most notably in Span2D<T>.CopyTo(Span<T>).
When using CopyTo in my project, I encountered an ArgumentOutOfRangeException in the legacy framework (.NET 4.8).
I traced the issue down to TryGetSpan, which consistently fails in .NET 4.8 (when constructed from a 1D array) but not in .NET 5.0+ where there is a runtime support for spans.

TryGetSpan has bool return type and Try in its name, suggesting it should not fail at all but rather return false if the requested operation cannot be performed.

The problem arises here:
https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/d3426fad3d3decc000f42323165e00efe28c12a0/Microsoft.Toolkit.HighPerformance/Memory/Span2D%7BT%7D.cs#L1062-L1068

Here, the instance object is converted (reinterpreted) to the array type T[], and then the AsSpan method is called.
I assume this is the standard MemoryExtensions.AsSpan<T>(T[], int start, int length).
The problem is, this method accepts start and length in array elements, while this.offset, which is passed there, is actually an internal byte offset within the 1D array, see this constructor
https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/d3426fad3d3decc000f42323165e00efe28c12a0/Microsoft.Toolkit.HighPerformance/Memory/Span2D%7BT%7D.cs#L252-L256

This offset is non-zero even when Span2D<T> points to the whole array. If I am not mistaken, the default value should be 8 in x64 process, corresponding to the internal offset of the data pointer within the array instance.
Now check this fragment again

Unsafe.As<T[]>(this.Instance).AsSpan((int)this.Offset, (int)Length)

if Span2D<T> points to the full array, .AsSpan tries to point to a subarray starting at Offset with length Length, which goes beyond the initial array by exactly Offset elements.
The situation should get worse if Span2D<T> is sliced and has non-default Offset value. The error should grow proportionally to the sizeof<T>, because Offset is measured in bytes and .AsSpan() accepts index of the array element. See Span2D<T> getter implementation:
https://github.com/windows-toolkit/WindowsCommunityToolkit/blob/d3426fad3d3decc000f42323165e00efe28c12a0/Microsoft.Toolkit.HighPerformance/Memory/Span2D%7BT%7D.cs#L950

Steps to Reproduce

 var arr = new int[128];
 var span2D = new Span2D<int>(arr, 8, 16);
 span2D.TryGetSpan(out var span);

Expected behavior

A new Span<int> instance is created that points to the whole arr.

Actual behaviour

ArgumentOutOfRangeException gets thrown at TryGetSpan with the following message:
"Specified argument was out of the range of valid values. Parameter name: start"

Possible solutions

1. Use internal constructor of Span<T> that accepts ByReference<T>, IntPtr and int.

Here IntPtr offset is the same thing as in Span2D<T>.

2. Convert offset into element index and use .AsSpan with a proper index and length.

var initialOffset = ObjectMarshal.DangerousGetObjectDataByteOffset(
  arr,
  ref ArrayExtensions.DangerousGetReference(arr)
);
// This gives the index of the starting element within an array for T : unmanaged
// For ref types (T : class) I guess the element size is IntPtr.Size
var elemOffset = (this.Offset.ToInt64() - initialOffset.ToInt64()) / Unsafe.SizeOf<T>();
Unsafe.As<T[]>(this.Instance).AsSpan((int)elemOffset, (int)Length)

Environment

Package Version(s): 7.0.1

.NET 4.8 @ Windows 10
 OS Name:     Windows
 OS Version:  10.0.19042
 OS Platform: Windows
 RID:         win10-x64
 .NET Framework 4.8
Mono @ Ubuntu WSL2
Linux DESKTOP-PC 5.4.72-microsoft-standard-WSL2 #1 SMP Wed Oct 28 23:40:43 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

Mono JIT compiler version 6.12.0.122 (tarball Mon Feb 22 17:29:18 UTC 2021)
Copyright (C) 2002-2014 Novell, Inc, Xamarin Inc and Contributors. www.mono-project.com
        TLS:           __thread
        SIGSEGV:       altstack
        Notifications: epoll
        Architecture:  amd64
        Disabled:      none
        Misc:          softdebug
        Interpreter:   yes
        LLVM:          yes(610)
        Suspend:       hybrid
        GC:            sgen (concurrent by default)

Metadata

Metadata

Assignees

Labels

Completed 🔥bug 🐛An unexpected issue that highlights incorrect behaviorhigh-performance 🚂Issues/PRs for the Microsoft.Toolkit.HighPerformance package

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions