diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Add.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Add.cs
index f2ef214cf..19f46c06f 100644
--- a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Add.cs
+++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Add.cs
@@ -16,6 +16,11 @@ internal static partial class TensorPrimitives_
/// The first addends.
/// The second addends.
/// The destination for the sums.
+ /// and do not have the same length.
+ /// is shorter than the input spans.
+ ///
+ /// An input and overlap without beginning at the same memory location.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add(ReadOnlySpan x, ReadOnlySpan y, Span destination)
where T : IAdditionOperators, IAdditiveIdentity
@@ -28,6 +33,10 @@ internal static partial class TensorPrimitives_
/// The first addends.
/// The scalar second addend.
/// The destination for the sums.
+ /// is shorter than .
+ ///
+ /// and overlap without beginning at the same memory location.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Add(ReadOnlySpan x, T y, Span destination)
where T : IAdditionOperators, IAdditiveIdentity
diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Clamp.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Clamp.cs
index e2cc9097e..b974163dc 100644
--- a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Clamp.cs
+++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Clamp.cs
@@ -18,6 +18,10 @@ internal static partial class TensorPrimitives_
/// The inclusive lower bound.
/// The inclusive upper bound.
/// The destination for the clamped values.
+ /// is shorter than .
+ ///
+ /// and overlap without beginning at the same memory location.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Clamp(ReadOnlySpan x, T min, T max, Span destination)
where T : INumber
diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Divide.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Divide.cs
index e40a26cea..f4a5bd01a 100644
--- a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Divide.cs
+++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Divide.cs
@@ -16,6 +16,10 @@ internal static partial class TensorPrimitives_
/// The dividend values.
/// The divisor.
/// The destination for the quotient values.
+ /// is shorter than .
+ ///
+ /// and overlap without beginning at the same memory location.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Divide(ReadOnlySpan x, T y, Span destination)
where T : IDivisionOperators
diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Helpers.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Helpers.cs
index 3ba1b61af..e80ca5411 100644
--- a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Helpers.cs
+++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Helpers.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -109,6 +110,47 @@ internal static partial class TensorPrimitives_
public static abstract Vector512 Invoke(Vector512 x, Vector512 y, Vector512 z);
}
+ ///
+ /// Validates that an input and destination are either disjoint or begin at the same memory location.
+ ///
+ /// The element type.
+ /// The input values.
+ /// The destination values.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void ValidateInputOutputSpanNonOverlapping(ReadOnlySpan input, Span destination)
+ {
+ // Runtime TensorPrimitives permits exact same-start overlap for in-place operation. A shifted overlap is
+ // rejected because forward SIMD stores could overwrite input elements before a later load consumes them.
+ if (!Unsafe.AreSame(ref MemoryMarshal.GetReference(input), ref MemoryMarshal.GetReference(destination))
+ && input.Overlaps(destination))
+ {
+ ThrowInputAndDestinationSpanMustNotOverlap();
+ }
+ }
+
+ ///
+ /// Throws when input spans do not have the same length.
+ ///
+ [DoesNotReturn]
+ private static void ThrowSpansMustHaveSameLength()
+ => throw new ArgumentException("Input span arguments must all have the same length.");
+
+ ///
+ /// Throws when the destination cannot hold every result.
+ ///
+ [DoesNotReturn]
+ private static void ThrowDestinationTooShort()
+ => throw new ArgumentException("Destination is too short.", "destination");
+
+ ///
+ /// Throws when an input and destination overlap without beginning at the same memory location.
+ ///
+ [DoesNotReturn]
+ private static void ThrowInputAndDestinationSpanMustNotOverlap()
+ => throw new ArgumentException(
+ "The destination span may only overlap with an input span if the two spans start at the same memory location.",
+ "destination");
+
///
/// Performs an element-wise binary operation between two spans.
///
@@ -124,6 +166,19 @@ internal static partial class TensorPrimitives_
Span destination)
where TOperator : struct, IBinaryOperator
{
+ if (x.Length != y.Length)
+ {
+ ThrowSpansMustHaveSameLength();
+ }
+
+ if (x.Length > destination.Length)
+ {
+ ThrowDestinationTooShort();
+ }
+
+ ValidateInputOutputSpanNonOverlapping(x, destination);
+ ValidateInputOutputSpanNonOverlapping(y, destination);
+
ref T xRef = ref MemoryMarshal.GetReference(x);
ref T yRef = ref MemoryMarshal.GetReference(y);
ref T destinationRef = ref MemoryMarshal.GetReference(destination);
@@ -173,6 +228,13 @@ internal static partial class TensorPrimitives_
Span destination)
where TOperator : struct, IBinaryOperator
{
+ if (x.Length > destination.Length)
+ {
+ ThrowDestinationTooShort();
+ }
+
+ ValidateInputOutputSpanNonOverlapping(x, destination);
+
ref T xRef = ref MemoryMarshal.GetReference(x);
ref T destinationRef = ref MemoryMarshal.GetReference(destination);
nuint length = (uint)x.Length;
@@ -220,6 +282,13 @@ internal static partial class TensorPrimitives_
Span destination)
where TOperator : struct, IBinaryOperator
{
+ if (x.Length > destination.Length)
+ {
+ ThrowDestinationTooShort();
+ }
+
+ ValidateInputOutputSpanNonOverlapping(x, destination);
+
ref T xRef = ref MemoryMarshal.GetReference(x);
ref T destinationRef = ref MemoryMarshal.GetReference(destination);
nuint length = (uint)x.Length;
@@ -282,6 +351,13 @@ internal static partial class TensorPrimitives_
Span destination)
where TOperator : struct, ITernaryOperator
{
+ if (x.Length > destination.Length)
+ {
+ ThrowDestinationTooShort();
+ }
+
+ ValidateInputOutputSpanNonOverlapping(x, destination);
+
ref T xRef = ref MemoryMarshal.GetReference(x);
ref T destinationRef = ref MemoryMarshal.GetReference(destination);
nuint length = (uint)x.Length;
diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Max.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Max.cs
index 31312dddf..fc621a122 100644
--- a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Max.cs
+++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Max.cs
@@ -16,6 +16,10 @@ internal static partial class TensorPrimitives_
/// The values to compare.
/// The value to compare with each element.
/// The destination for the maximum values.
+ /// is shorter than .
+ ///
+ /// and overlap without beginning at the same memory location.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Max(ReadOnlySpan x, T y, Span destination)
where T : INumber
diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Multiply.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Multiply.cs
index deb1e922e..367a7ab0b 100644
--- a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Multiply.cs
+++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Multiply.cs
@@ -16,6 +16,10 @@ internal static partial class TensorPrimitives_
/// The multiplicands.
/// The multiplier.
/// The destination for the products.
+ /// is shorter than .
+ ///
+ /// and overlap without beginning at the same memory location.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Multiply(ReadOnlySpan x, T y, Span destination)
where T : IMultiplyOperators, IMultiplicativeIdentity
diff --git a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs
index 99dc80184..05f93ac66 100644
--- a/src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs
+++ b/src/ImageSharp/Common/Helpers/TensorPrimitives_.Negate.cs
@@ -56,6 +56,10 @@ internal static partial class TensorPrimitives_
/// The element type.
/// The values to negate.
/// The destination for the negated values.
+ /// is shorter than .
+ ///
+ /// and overlap without beginning at the same memory location.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Negate(ReadOnlySpan x, Span destination)
where T : IUnaryNegationOperators
@@ -72,6 +76,13 @@ internal static partial class TensorPrimitives_
private static void InvokeSpanIntoSpan(ReadOnlySpan x, Span destination)
where TOperator : struct, IUnaryOperator
{
+ if (x.Length > destination.Length)
+ {
+ ThrowDestinationTooShort();
+ }
+
+ ValidateInputOutputSpanNonOverlapping(x, destination);
+
ref T xRef = ref MemoryMarshal.GetReference(x);
ref T destinationRef = ref MemoryMarshal.GetReference(destination);
nuint length = (uint)x.Length;
diff --git a/tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs b/tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs
index 5ce5401f8..b2f4dfe76 100644
--- a/tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs
+++ b/tests/ImageSharp.Tests/Common/TensorPrimitivesTests.cs
@@ -51,6 +51,88 @@ public class TensorPrimitivesTests
| HwIntrinsics.DisableArm64Sve
| HwIntrinsics.DisableHWIntrinsic);
+ ///
+ /// Verifies that span-to-span operations require equal input lengths before accessing either input.
+ ///
+ [Fact]
+ public void AddSpanSpanRejectsMismatchedInputLengths()
+ {
+ ArgumentException exception = Assert.Throws(
+ () => TensorPrimitives_.Add(new int[4], new int[3], new int[4]));
+
+ Assert.Null(exception.ParamName);
+ }
+
+ ///
+ /// Verifies that every operation rejects a destination that cannot hold all input elements.
+ ///
+ [Fact]
+ public void OperationsRejectShortDestinations()
+ {
+ int[] integers = new int[4];
+ float[] singles = new float[4];
+
+ Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Add(integers, integers, new int[3])).ParamName);
+ Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Add(integers, 1, new int[3])).ParamName);
+ Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Multiply(singles, 2F, new float[3])).ParamName);
+ Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Divide(singles, 2F, new float[3])).ParamName);
+ Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Max(singles, 2F, new float[3])).ParamName);
+ Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Clamp(singles, 0F, 1F, new float[3])).ParamName);
+ Assert.Equal("destination", Assert.Throws(() => TensorPrimitives_.Negate(singles, new float[3])).ParamName);
+ }
+
+ ///
+ /// Verifies that every operation rejects shifted input and destination overlap.
+ ///
+ [Fact]
+ public void OperationsRejectShiftedOverlap()
+ {
+ int[] integers = new int[5];
+ int[] separateIntegers = new int[4];
+ float[] singles = new float[5];
+
+ // Both inputs need independent validation because either one may alias a shifted destination.
+ Assert.Equal(
+ "destination",
+ Assert.Throws(
+ () => TensorPrimitives_.Add(integers.AsSpan(0, 4), separateIntegers, integers.AsSpan(1, 4))).ParamName);
+
+ Assert.Equal(
+ "destination",
+ Assert.Throws(
+ () => TensorPrimitives_.Add(separateIntegers, integers.AsSpan(0, 4), integers.AsSpan(1, 4))).ParamName);
+
+ Assert.Equal(
+ "destination",
+ Assert.Throws(
+ () => TensorPrimitives_.Add(integers.AsSpan(0, 4), 1, integers.AsSpan(1, 4))).ParamName);
+
+ Assert.Equal(
+ "destination",
+ Assert.Throws(
+ () => TensorPrimitives_.Multiply(singles.AsSpan(0, 4), 2F, singles.AsSpan(1, 4))).ParamName);
+
+ Assert.Equal(
+ "destination",
+ Assert.Throws(
+ () => TensorPrimitives_.Divide(singles.AsSpan(0, 4), 2F, singles.AsSpan(1, 4))).ParamName);
+
+ Assert.Equal(
+ "destination",
+ Assert.Throws(
+ () => TensorPrimitives_.Max(singles.AsSpan(0, 4), 2F, singles.AsSpan(1, 4))).ParamName);
+
+ Assert.Equal(
+ "destination",
+ Assert.Throws(
+ () => TensorPrimitives_.Clamp(singles.AsSpan(0, 4), 0F, 1F, singles.AsSpan(1, 4))).ParamName);
+
+ Assert.Equal(
+ "destination",
+ Assert.Throws(
+ () => TensorPrimitives_.Negate(singles.AsSpan(0, 4), singles.AsSpan(1, 4))).ParamName);
+ }
+
///
/// Runs the TensorPrimitives compatibility assertions inside a process configured for one hardware-intrinsic tier.
///