From d2dd1b038d2391cc05fe5ef525d03fffb96025ac Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Wed, 24 Dec 2025 00:09:55 +0000 Subject: [PATCH 1/4] Explicitly perform little-endian multibyte writes --- .../src/Microsoft/Data/SqlTypes/SqlVector.cs | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs index cf04ff8636..f08d3f9f0f 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs @@ -158,32 +158,29 @@ private byte[] MakeTdsBytes(ReadOnlyMemory values) // +------------------------+-----------------+----------------------+--------------------------------------------------------------+ byte[] result = new byte[_size]; + ReadOnlySpan valueSpan = values.Span; // Header Bytes result[0] = VecHeaderMagicNo; result[1] = VecVersionNo; - result[2] = (byte)(Length & 0xFF); - result[3] = (byte)((Length >> 8) & 0xFF); + BinaryPrimitives.WriteUInt16LittleEndian(result.AsSpan(2), (ushort)Length); result[4] = _elementType; result[5] = 0x00; result[6] = 0x00; result[7] = 0x00; -#if NETFRAMEWORK - // Copy data via marshaling. - if (MemoryMarshal.TryGetArray(values, out ArraySegment segment)) - { - Buffer.BlockCopy(segment.Array, segment.Offset * _elementSize, result, TdsEnums.VECTOR_HEADER_SIZE, segment.Count * _elementSize); - } - else + if (typeof(T) == typeof(float)) { - Buffer.BlockCopy(values.ToArray(), 0, result, TdsEnums.VECTOR_HEADER_SIZE, values.Length * _elementSize); - } + for (int i = 0, currPosition = TdsEnums.VECTOR_HEADER_SIZE; i < values.Length; i++, currPosition += _elementSize) + { +#if NET + BinaryPrimitives.WriteSingleLittleEndian(result.AsSpan(currPosition), (float)(object)valueSpan[i]); #else - // Fast span-based copy. - var byteSpan = MemoryMarshal.AsBytes(values.Span); - byteSpan.CopyTo(result.AsSpan(TdsEnums.VECTOR_HEADER_SIZE)); + BinaryPrimitives.WriteInt32LittleEndian(result.AsSpan(currPosition), BitConverterCompatible.SingleToInt32Bits((float)(object)valueSpan[i])); #endif + } + } + return result; } @@ -227,16 +224,22 @@ private T[] MakeArray() return Array.Empty(); } -#if NETFRAMEWORK // Allocate array and copy bytes into it T[] result = new T[Length]; - Buffer.BlockCopy(_tdsBytes, 8, result, 0, _elementSize * Length); - return result; + + if (typeof(T) == typeof(float)) + { + for (int i = 0, currPosition = TdsEnums.VECTOR_HEADER_SIZE; i < Length; i++, currPosition += _elementSize) + { +#if NET + result[i] = (T)(object)BinaryPrimitives.ReadSingleLittleEndian(_tdsBytes.AsSpan(currPosition)); #else - ReadOnlySpan dataSpan = _tdsBytes.AsSpan(8, _elementSize * Length); - return MemoryMarshal.Cast(dataSpan).ToArray(); + result[i] = (T)(object)BitConverterCompatible.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(_tdsBytes.AsSpan(currPosition))); #endif + } + } + return result; } - #endregion +#endregion } From c66a1e5b48c203902ed5d27468eddf107466effd Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Mon, 5 Jan 2026 14:37:56 +0000 Subject: [PATCH 2/4] Tighten validation of array lengths Vectors must always be less than 8000 bytes. This also means that Length will always be <= ushort.MaxValue. Add assertion to highlight this. --- .../src/Microsoft/Data/SqlTypes/SqlVector.cs | 10 ++++++++++ .../UnitTests/Microsoft/Data/SqlTypes/SqlVectorTest.cs | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs index f08d3f9f0f..dd99fa4135 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs @@ -50,6 +50,10 @@ private SqlVector(int length) Length = length; _size = TdsEnums.VECTOR_HEADER_SIZE + (_elementSize * Length); + if (_size > TdsEnums.MAXSIZE) + { + throw ADP.ArgumentOutOfRange(nameof(length), SQLResource.InvalidArraySizeMessage); + } _tdsBytes = Array.Empty(); Memory = new(); @@ -67,6 +71,10 @@ public SqlVector(ReadOnlyMemory memory) Length = memory.Length; _size = TdsEnums.VECTOR_HEADER_SIZE + (_elementSize * Length); + if (_size > TdsEnums.MAXSIZE) + { + throw ADP.ArgumentOutOfRange(nameof(memory), SQLResource.InvalidArraySizeMessage); + } _tdsBytes = MakeTdsBytes(memory); Memory = memory; @@ -145,6 +153,8 @@ internal string GetString() private byte[] MakeTdsBytes(ReadOnlyMemory values) { + Debug.Assert(Length <= TdsEnums.MAXSIZE); + //Refer to TDS section 2.2.5.5.7 for vector header format // +------------------------+-----------------+----------------------+------------------+----------------------------+--------------+ // | Field | Size (bytes) | Example Value | Description | diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlVectorTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlVectorTest.cs index adca0e2b99..c39fe686e3 100644 --- a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlVectorTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlTypes/SqlVectorTest.cs @@ -28,6 +28,12 @@ public void Construct_Length_Negative() Assert.Throws(() => SqlVector.CreateNull(-1)); } + [Fact] + public void Construct_Length_Exceeds_8000() + { + Assert.Throws(() => SqlVector.CreateNull(1999)); + } + [Fact] public void Construct_Length() { From d9763736958a38f53ae08021fa4dd98985208b8d Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:35:25 +0000 Subject: [PATCH 3/4] Partially revert changes to avoid performance regression --- .../src/Microsoft/Data/SqlTypes/SqlVector.cs | 52 ++++++++++++++----- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs index dd99fa4135..6ca822cd42 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs @@ -179,15 +179,28 @@ private byte[] MakeTdsBytes(ReadOnlyMemory values) result[6] = 0x00; result[7] = 0x00; - if (typeof(T) == typeof(float)) + // If .NET is running on a little-endian architecture, cast directly to a byte array and proceed. + // This optimisation relies upon the base type of the vector transporting values in a format and + // endianness which is identical to the client. This is true for all little-endian clients reading + // float32-based vectors. + if (BitConverter.IsLittleEndian) + { + ReadOnlySpan valuesAsBytes = MemoryMarshal.AsBytes(valueSpan); + + valuesAsBytes.CopyTo(result.AsSpan(TdsEnums.VECTOR_HEADER_SIZE)); + } + else { - for (int i = 0, currPosition = TdsEnums.VECTOR_HEADER_SIZE; i < values.Length; i++, currPosition += _elementSize) + if (typeof(T) == typeof(float)) { -#if NET - BinaryPrimitives.WriteSingleLittleEndian(result.AsSpan(currPosition), (float)(object)valueSpan[i]); -#else - BinaryPrimitives.WriteInt32LittleEndian(result.AsSpan(currPosition), BitConverterCompatible.SingleToInt32Bits((float)(object)valueSpan[i])); -#endif + for (int i = 0, currPosition = TdsEnums.VECTOR_HEADER_SIZE; i < values.Length; i++, currPosition += _elementSize) + { + #if NET + BinaryPrimitives.WriteSingleLittleEndian(result.AsSpan(currPosition), (float)(object)valueSpan[i]); + #else + BinaryPrimitives.WriteInt32LittleEndian(result.AsSpan(currPosition), BitConverterCompatible.SingleToInt32Bits((float)(object)valueSpan[i])); + #endif + } } } @@ -237,17 +250,28 @@ private T[] MakeArray() // Allocate array and copy bytes into it T[] result = new T[Length]; - if (typeof(T) == typeof(float)) + // See the comment in MakeTdsBytes for more information on this optimisation. + if (BitConverter.IsLittleEndian) { - for (int i = 0, currPosition = TdsEnums.VECTOR_HEADER_SIZE; i < Length; i++, currPosition += _elementSize) + Span valuesAsBytes = MemoryMarshal.AsBytes(result.AsSpan()); + + _tdsBytes.AsSpan(TdsEnums.VECTOR_HEADER_SIZE).CopyTo(valuesAsBytes); + } + else + { + if (typeof(T) == typeof(float)) { -#if NET - result[i] = (T)(object)BinaryPrimitives.ReadSingleLittleEndian(_tdsBytes.AsSpan(currPosition)); -#else - result[i] = (T)(object)BitConverterCompatible.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(_tdsBytes.AsSpan(currPosition))); -#endif + for (int i = 0, currPosition = TdsEnums.VECTOR_HEADER_SIZE; i < Length; i++, currPosition += _elementSize) + { + #if NET + result[i] = (T)(object)BinaryPrimitives.ReadSingleLittleEndian(_tdsBytes.AsSpan(currPosition)); + #else + result[i] = (T)(object)BitConverterCompatible.Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(_tdsBytes.AsSpan(currPosition))); + #endif + } } } + return result; } From 8a908ed9a25da515a0239efd6e880478a1a9ff83 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Mon, 5 Jan 2026 15:38:51 +0000 Subject: [PATCH 4/4] Correct invalid debug assertion Length is the number of elements. --- .../src/Microsoft/Data/SqlTypes/SqlVector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs index 6ca822cd42..dd2a94c367 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs @@ -153,7 +153,7 @@ internal string GetString() private byte[] MakeTdsBytes(ReadOnlyMemory values) { - Debug.Assert(Length <= TdsEnums.MAXSIZE); + Debug.Assert(Length <= ushort.MaxValue); //Refer to TDS section 2.2.5.5.7 for vector header format // +------------------------+-----------------+----------------------+------------------+----------------------------+--------------+