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/6] 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/6] 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/6] 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/6] 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 // +------------------------+-----------------+----------------------+------------------+----------------------------+--------------+ From 8a30c17e36c2a2abc1afdcb23ca3ba20949dc804 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Wed, 8 Apr 2026 06:02:59 +0100 Subject: [PATCH 5/6] Handle integer overflow when processing large-dimensioned null vectors --- .../src/Microsoft/Data/SqlTypes/SqlVector.cs | 29 +++++++++---------- .../Microsoft/Data/SqlTypes/SqlVectorTest.cs | 3 +- 2 files changed, 16 insertions(+), 16 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 fd475e33e7..3558cb21bc 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs @@ -39,21 +39,16 @@ namespace Microsoft.Data.SqlTypes; private SqlVector(int length) { - if (length < 0) + (_elementType, _elementSize, int maxElements) = GetTypeFieldsOrThrow(); + if (length < 0 || length > maxElements) { throw ADP.InvalidArraySize(nameof(length)); } - (_elementType, _elementSize) = GetTypeFieldsOrThrow(); - IsNull = true; Length = length; _size = TdsEnums.VECTOR_HEADER_SIZE + (_elementSize * Length); - if (_size > TdsEnums.MAXSIZE) - { - throw ADP.InvalidArraySize(nameof(length)); - } _tdsBytes = Array.Empty(); Memory = new(); @@ -65,16 +60,16 @@ private SqlVector(int length) /// public SqlVector(ReadOnlyMemory memory) { - (_elementType, _elementSize) = GetTypeFieldsOrThrow(); + (_elementType, _elementSize, int maxElements) = GetTypeFieldsOrThrow(); + if (memory.Length > maxElements) + { + throw ADP.InvalidArraySize(nameof(memory)); + } IsNull = false; Length = memory.Length; _size = TdsEnums.VECTOR_HEADER_SIZE + (_elementSize * Length); - if (_size > TdsEnums.MAXSIZE) - { - throw ADP.InvalidArraySize(nameof(memory)); - } _tdsBytes = MakeTdsBytes(memory); Memory = memory; @@ -82,7 +77,7 @@ public SqlVector(ReadOnlyMemory memory) internal SqlVector(byte[] tdsBytes) { - (_elementType, _elementSize) = GetTypeFieldsOrThrow(); + (_elementType, _elementSize, _) = GetTypeFieldsOrThrow(); (Length, _size) = GetCountsOrThrow(tdsBytes); @@ -133,10 +128,11 @@ internal string GetString() #region Helpers - private (byte, byte) GetTypeFieldsOrThrow() + private static (byte, byte, int) GetTypeFieldsOrThrow() { byte elementType; byte elementSize; + int maxSize; if (typeof(T) == typeof(float)) { @@ -147,8 +143,11 @@ internal string GetString() { throw SQL.VectorTypeNotSupported(typeof(T).FullName); } + // The size of a vector (including its header) must not exceed the maximum size of a TDS packet. + // Calculate the maximum number of elements to simplify the validation of input sizes in constructors. + maxSize = (TdsEnums.MAXSIZE - TdsEnums.VECTOR_HEADER_SIZE) / elementSize; - return (elementType, elementSize); + return (elementType, elementSize, maxSize); } private byte[] MakeTdsBytes(ReadOnlyMemory values) 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 ed48ad8e45..545f8af083 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 @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -32,6 +32,7 @@ public void Construct_Length_Negative() public void Construct_Length_Exceeds_8000() { Assert.Throws(() => SqlVector.CreateNull(1999)); + Assert.Throws(() => SqlVector.CreateNull(int.MaxValue / 2)); } [Fact] From 3a33d2142cb6731a1f8661d51116f5ff0a61e859 Mon Sep 17 00:00:00 2001 From: Edward Neal <55035479+edwardneal@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:52:47 +0100 Subject: [PATCH 6/6] Correct endregion whitespace --- .../src/Microsoft/Data/SqlTypes/SqlVector.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 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 3558cb21bc..d87a539c37 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlTypes/SqlVector.cs @@ -273,6 +273,6 @@ private T[] MakeArray() return result; } - -#endregion + + #endregion }