CVE-2023-28638: Improper Restriction of Operations within the Bounds of a Memory Buffer (CWE-119)
value: Snappier is a high performance C# implementation of the Snappy compression algorithm. This is a buffer overrun vulnerability that can affect any user of Snappier 1.1.0. In this release, much of the code was rewritten to use byte references rather than pointers to pinned buffers. This change generally improves performance and reduces workload on the garbage collector. However, when the garbage collector performs compaction and rearranges memory, it must update any byte references on the stack to refer to the updated location. The .NET garbage collector can only update these byte references if they still point within the buffer or to a point one byte past the end of the buffer. If they point outside this area, the buffer itself may be moved while the byte reference stays the same. There are several places in 1.1.0 where byte references very briefly point outside the valid areas of buffers. These are at locations in the code being used for buffer range checks. While the invalid references are never dereferenced directly, if a GC compaction were to occur during the brief window when they are on the stack then it could invalidate the buffer range check and allow other operations to overrun the buffer.  This should be very difficult for an attacker to trigger intentionally. It would require a repetitive bulk attack with the hope that a GC compaction would occur at precisely the right moment during one of the requests. However, one of the range checks with this problem is a check based on input data in the decompression buffer, meaning malformed input data could be used to increase the chance of success. Note that any resulting buffer overrun is likely to cause access to protected memory, which will then cause an exception and the process to be terminated. Therefore, the most likely result of an attack is a denial of service. This issue has been patched in release 1.1.1. Users are advised to upgrade. Users unable to upgrade may pin buffers to a fixed location before using them for compression or decompression to mitigate some, but not all, of these cases. At least one temporary decompression buffer is internal to the library and never pinned.

CWE description: The product performs operations on a memory buffer, but it reads from or writes to a memory location outside the buffer's intended boundary. This may result in read or write operations on unexpected memory locations that could be linked to other variables, data structures, or internal program data.
CVSS v3 base score: 5.9
Published: 2023-03-27T21:15Z
Repository: https://github.com/brantburnett/Snappier
Language: C#
Files changed: Snappier.Benchmarks/FindMatchLength.cs, Snappier.Tests/Internal/SnappyCompressorTests.cs, Snappier/Internal/SnappyCompressor.cs, Snappier/Internal/SnappyDecompressor.cs
Commit date: 2023-03-26 18:20:20 -0400
Commit message: Allow ref byte to point just past the end of spans (#73)

Motivation
----------
I was under the impression that `ref byte` should never point past the
end of a `Span<byte>` or `ReadOnlySpan<byte>` because then GC couldn't
recognize the pointer and adjust it during GC moves. However, this there
is a specific exception that allows `ref byte` to point precisely one
byte past the end and still be recognized to allow pointer arithmetic
scenarios like the ones in this algorithm.

Modifications
-------------
Where there is more complex logic (compared to the reference C++
implementation) to allow buffer end pointers to point to the last byte
in the buffer simplify this logic and point one byte past the end of the
buffer.

Also ensure that comparison operations that ensure a certain length
don't involve an intermediate pointer that moves off the beginning of
the buffer.

Results
-------
We're now back to closer to the reference C++ implementation but still
have memory safety.
Diff: diff --git a/Snappier.Benchmarks/FindMatchLength.cs b/Snappier.Benchmarks/FindMatchLength.cs
index 0d7a11c..b392bab 100644
--- a/Snappier.Benchmarks/FindMatchLength.cs
+++ b/Snappier.Benchmarks/FindMatchLength.cs
@@ -62,11 +62,11 @@ namespace Snappier.Benchmarks
         {
             ulong data = 0;
 
             ref byte s1 = ref _array[0];
             ref byte s2 = ref Unsafe.Add(ref s1, 12);
-            ref byte s2Limit = ref Unsafe.Add(ref s1, _array.Length - 1);
+            ref byte s2Limit = ref Unsafe.Add(ref s1, _array.Length);
 
             return SnappyCompressor.FindMatchLength(ref s1, ref s2, ref s2Limit, ref data);
         }
     }
 }
diff --git a/Snappier.Tests/Internal/SnappyCompressorTests.cs b/Snappier.Tests/Internal/SnappyCompressorTests.cs
index 01a7d4a..6bfc289 100644
--- a/Snappier.Tests/Internal/SnappyCompressorTests.cs
+++ b/Snappier.Tests/Internal/SnappyCompressorTests.cs
@@ -90,11 +90,11 @@ namespace Snappier.Tests.Internal
             ulong data = 0;
             ref byte s1 = ref array[0];
             ref byte s2 = ref Unsafe.Add(ref s1, s1String.Length);
 
             var result =
-                SnappyCompressor.FindMatchLength(ref s1, ref s2, ref Unsafe.Add(ref s2, length - 1), ref data);
+                SnappyCompressor.FindMatchLength(ref s1, ref s2, ref Unsafe.Add(ref s2, length), ref data);
 
             Assert.Equal(result.matchLength < 8, result.matchLengthLessThan8);
             Assert.Equal(expectedResult, result.matchLength);
         }
 
diff --git a/Snappier/Internal/SnappyCompressor.cs b/Snappier/Internal/SnappyCompressor.cs
index b8f8303..5bccad8 100644
--- a/Snappier/Internal/SnappyCompressor.cs
+++ b/Snappier/Internal/SnappyCompressor.cs
@@ -123,20 +123,19 @@ namespace Snappier.Internal
                 Debug.Assert((tableSpan.Length & (tableSpan.Length - 1)) == 0); // table must be power of two
 
                 uint mask = (uint)(2 * (tableSpan.Length - 1));
 
                 ref byte inputStart = ref Unsafe.AsRef(in input[0]);
-                // Last byte of the input, not one byte past the end, to avoid issues on GC moves
-                ref byte inputEnd = ref Unsafe.Add(ref inputStart, input.Length - 1);
+                ref byte inputEnd = ref Unsafe.Add(ref inputStart, input.Length);
                 ref byte ip = ref inputStart;
 
                 ref byte op = ref output[0];
                 ref ushort table = ref tableSpan[0];
 
                 if (input.Length >= Constants.InputMarginBytes)
                 {
-                    ref byte ipLimit = ref Unsafe.Subtract(ref inputEnd, Constants.InputMarginBytes - 1);
+                    ref byte ipLimit = ref Unsafe.Subtract(ref inputEnd, Constants.InputMarginBytes);
 
                     for (uint preload = Helpers.UnsafeReadUInt32(ref Unsafe.Add(ref ip, 1));;)
                     {
                         // Bytes in [nextEmit, ip) will be emitted as literal bytes.  Or
                         // [nextEmit, ipEnd) after the main loop.
@@ -286,11 +285,11 @@ namespace Snappier.Internal
                         }
 
                         // Step 2: A 4-byte match has been found.  We'll later see if more
                         // than 4 bytes match.  But, prior to the match, input
                         // bytes [next_emit, ip) are unmatched.  Emit them as "literal bytes."
-                        Debug.Assert(!Unsafe.IsAddressGreaterThan(ref Unsafe.Add(ref nextEmit, 16), ref Unsafe.Add(ref inputEnd, 1)));
+                        Debug.Assert(!Unsafe.IsAddressGreaterThan(ref Unsafe.Add(ref nextEmit, 16), ref inputEnd));
                         op = ref EmitLiteralFast(ref op, ref nextEmit, (uint) Unsafe.ByteOffset(ref nextEmit, ref ip));
 
                         // Step 3: Call EmitCopy, and then see if another EmitCopy could
                         // be our next move.  Repeat until we find no match for the
                         // input immediately after what was consumed by the last EmitCopy call.
@@ -348,13 +347,13 @@ namespace Snappier.Internal
                     }
                 }
 
                 emit_remainder:
                 // Emit the remaining bytes as a literal
-                if (!Unsafe.IsAddressGreaterThan(ref ip, ref inputEnd))
+                if (Unsafe.IsAddressLessThan(ref ip, ref inputEnd))
                 {
-                    op = ref EmitLiteralSlow(ref op, ref ip, (uint) Unsafe.ByteOffset(ref ip, ref inputEnd) + 1);
+                    op = ref EmitLiteralSlow(ref op, ref ip, (uint) Unsafe.ByteOffset(ref ip, ref inputEnd));
                 }
 
                 return (int) Unsafe.ByteOffset(ref output[0], ref op);
             }
         }
@@ -488,65 +487,60 @@ namespace Snappier.Internal
 
         /// <summary>
         /// Find the largest n such that
         ///
         ///   s1[0,n-1] == s2[0,n-1]
-        ///   and n &lt;= (s2_limit - s2 + 1).
+        ///   and n &lt;= (s2_limit - s2).
         ///
         /// Return (n, n &lt; 8).
         /// Reads up to and including *s2_limit but not beyond.
-        /// Does not read *(s1 + (s2_limit - s2 + 1)) or beyond.
-        /// Requires that s2_limit+1 &gt;= s2.
+        /// Does not read *(s1 + (s2_limit - s2)) or beyond.
+        /// Requires that s2_limit &gt;= s2.
         ///
         /// In addition populate *data with the next 5 bytes from the end of the match.
         /// This is only done if 8 bytes are available (s2_limit - s2 &gt;= 8). The point is
         /// that on some arch's this can be done faster in this routine than subsequent
         /// loading from s2 + n.
         /// </summary>
-        /// <remarks>
-        /// The reference implementation has s2Limit as one byte past the end of the input,
-        /// but this implementation has it at the end of the input. This ensures that it always
-        /// points within the array in case GC moves the array.
-        /// </remarks>
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         internal static (int matchLength, bool matchLengthLessThan8) FindMatchLength(
             ref byte s1, ref byte s2, ref byte s2Limit, ref ulong data)
         {
-            Debug.Assert(!Unsafe.IsAddressLessThan(ref Unsafe.Add(ref s2Limit, 1), ref s2));
+            Debug.Assert(!Unsafe.IsAddressLessThan(ref s2Limit, ref s2));
 
             if (BitConverter.IsLittleEndian && IntPtr.Size == 8)
             {
                 // Special implementation for 64-bit little endian processors (i.e. Intel/AMD x64)
                 return FindMatchLengthX64(ref s1, ref s2, ref s2Limit, ref data);
             }
 
             int matched = 0;
 
-            while (!Unsafe.IsAddressGreaterThan(ref s2, ref Unsafe.Subtract(ref s2Limit, 3))
+            while (Unsafe.ByteOffset(ref s2, ref s2Limit) >= (nint)4
                    && Helpers.UnsafeReadUInt32(ref s2) == Helpers.UnsafeReadUInt32(ref Unsafe.Add(ref s1, matched)))
             {
                 s2 = ref Unsafe.Add(ref s2, 4);
                 matched += 4;
             }
 
-            if (BitConverter.IsLittleEndian && !Unsafe.IsAddressGreaterThan(ref s2, ref Unsafe.Subtract(ref s2Limit, 3)))
+            if (BitConverter.IsLittleEndian && Unsafe.ByteOffset(ref s2, ref s2Limit) >= (nint)4)
             {
                 uint x = Helpers.UnsafeReadUInt32(ref s2) ^ Helpers.UnsafeReadUInt32(ref Unsafe.Add(ref s1, matched));
                 int matchingBits = Helpers.FindLsbSetNonZero(x);
                 matched += matchingBits >> 3;
                 s2 = ref Unsafe.Add(ref s2, matchingBits >> 3);
             }
             else
             {
-                while (!Unsafe.IsAddressGreaterThan(ref s2, ref s2Limit) && Unsafe.Add(ref s1, matched) == s2)
+                while (Unsafe.IsAddressLessThan(ref s2, ref s2Limit) && Unsafe.Add(ref s1, matched) == s2)
                 {
                     s2 = ref Unsafe.Add(ref s2, 1);
                     ++matched;
                 }
             }
 
-            if (!Unsafe.IsAddressGreaterThan(ref s2, ref Unsafe.Subtract(ref s2Limit, 7)))
+            if (Unsafe.ByteOffset(ref s2, ref s2Limit) >= (nint)8)
             {
                 data = Helpers.UnsafeReadUInt64(ref s2);
             }
 
             return (matched, matched < 8);
@@ -560,11 +554,11 @@ namespace Snappier.Internal
 
             // This block isn't necessary for correctness; we could just start looping
             // immediately.  As an optimization though, it is useful.  It creates some not
             // uncommon code paths that determine, without extra effort, whether the match
             // length is less than 8.
-            if (!Unsafe.IsAddressGreaterThan(ref s2, ref Unsafe.Subtract(ref s2Limit, 15)))
+            if (Unsafe.ByteOffset(ref s2, ref s2Limit) >= (nint)16)
             {
                 ulong a1 = Helpers.UnsafeReadUInt64(ref s1);
                 ulong a2 = Helpers.UnsafeReadUInt64(ref s2);
 
                 if (a1 != a2)
@@ -588,11 +582,11 @@ namespace Snappier.Internal
 
             // Find out how long the match is. We loop over the data 64 bits at a
             // time until we find a 64-bit block that doesn't match; then we find
             // the first non-matching bit and use that to calculate the total
             // length of the match.
-            while (!Unsafe.IsAddressGreaterThan(ref s2, ref Unsafe.Subtract(ref s2Limit, 15)))
+            while (Unsafe.ByteOffset(ref s2, ref s2Limit) >= (nint)16)
             {
                 ulong a1 = Helpers.UnsafeReadUInt64(ref Unsafe.Add(ref s1, matched));
                 ulong a2 = Helpers.UnsafeReadUInt64(ref s2);
                 if (a1 == a2)
                 {
@@ -613,20 +607,20 @@ namespace Snappier.Internal
                     Debug.Assert(matched >= 8);
                     return ((int)matched, false);
                 }
             }
 
-            while (!Unsafe.IsAddressGreaterThan(ref s2, ref s2Limit))
+            while (Unsafe.IsAddressLessThan(ref s2, ref s2Limit))
             {
                 if (Unsafe.Add(ref s1, matched) == s2)
                 {
                     s2 = ref Unsafe.Add(ref s2, 1);
                     matched++;
                 }
                 else
                 {
-                    if (!Unsafe.IsAddressGreaterThan(ref s2, ref Unsafe.Subtract(ref s2Limit, 7)))
+                    if (Unsafe.ByteOffset(ref s2, ref s2Limit) >= (nint)8)
                     {
                         data = Helpers.UnsafeReadUInt64(ref s2);
                     }
 
                     return ((int)matched, matched < 8);
diff --git a/Snappier/Internal/SnappyDecompressor.cs b/Snappier/Internal/SnappyDecompressor.cs
index c0c5817..eade2e4 100644
--- a/Snappier/Internal/SnappyDecompressor.cs
+++ b/Snappier/Internal/SnappyDecompressor.cs
@@ -184,21 +184,15 @@ namespace Snappier.Internal
             ReadOnlySpan<ushort> charTable = Constants.CharTable.AsSpan(0, 256);
 
             unchecked
             {
                 ref byte input = ref Unsafe.AsRef(in inputSpan[0]);
-
-                // The reference Snappy implementation uses inputEnd as a pointer one byte past the end of the buffer.
-                // However, this is not safe when using ref locals. The ref must point to somewhere within the array
-                // so that GC can adjust the ref if the memory is moved.
-                ref byte inputEnd = ref Unsafe.Add(ref input, inputSpan.Length - 1);
+                ref byte inputEnd = ref Unsafe.Add(ref input, inputSpan.Length);
 
                 // Track the point in the input before which input is guaranteed to have at least Constants.MaxTagLength bytes left
-                ref byte inputLimitMinMaxTagLength = ref Unsafe.Subtract(ref inputEnd, Math.Min(inputSpan.Length, Constants.MaximumTagLength - 1) - 1);
+                ref byte inputLimitMinMaxTagLength = ref Unsafe.Subtract(ref inputEnd, Math.Min(inputSpan.Length, Constants.MaximumTagLength - 1));
 
-                // We always allocate buffer with at least one extra byte on the end, so bufferEnd doesn't have the same
-                // restrictions as inputEnd.
                 ref byte buffer = ref _lookbackBuffer.Span[0];
                 ref byte bufferEnd = ref Unsafe.Add(ref buffer, _lookbackBuffer.Length);
                 ref byte op = ref Unsafe.Add(ref buffer, _lookbackPosition);
 
                 // Get a reference to the first byte in the scratch buffer, we'll reuse this so that we don't repeat range checks every time
@@ -237,13 +231,13 @@ namespace Snappier.Internal
 
                     if (newScratchLength > 0)
                     {
                         // Data has been moved to the scratch buffer
                         input = ref scratch;
-                        inputEnd = ref Unsafe.Add(ref input, newScratchLength - 1);
+                        inputEnd = ref Unsafe.Add(ref input, newScratchLength);
                         inputLimitMinMaxTagLength = ref Unsafe.Subtract(ref inputEnd,
-                            Math.Min(newScratchLength, Constants.MaximumTagLength - 1) - 1);
+                            Math.Min(newScratchLength, Constants.MaximumTagLength - 1));
                     }
                 }
 
                 uint preload = Helpers.UnsafeReadUInt32(ref input);
 
@@ -254,11 +248,11 @@ namespace Snappier.Internal
 
                     if ((c & 0x03) == Constants.Literal)
                     {
                         nint literalLength = unchecked((c >> 2) + 1);
 
-                        if (TryFastAppend(ref op, ref bufferEnd, in input, Unsafe.ByteOffset(ref input, ref inputEnd) + 1, literalLength))
+                        if (TryFastAppend(ref op, ref bufferEnd, in input, Unsafe.ByteOffset(ref input, ref inputEnd), literalLength))
                         {
                             Debug.Assert(literalLength < 61);
                             op = ref Unsafe.Add(ref op, literalLength);
                             input = ref Unsafe.Add(ref input, literalLength);
                             // NOTE: There is no RefillTag here, as TryFastAppend()
@@ -278,11 +272,11 @@ namespace Snappier.Internal
                                 (int) literalLengthLength) + 1;
 
                             input = ref Unsafe.Add(ref input, literalLengthLength);
                         }
 
-                        nint inputRemaining = Unsafe.ByteOffset(ref input, ref inputEnd) + 1;
+                        nint inputRemaining = Unsafe.ByteOffset(ref input, ref inputEnd);
                         if (inputRemaining < literalLength)
                         {
                             Append(ref op, ref bufferEnd, in input, inputRemaining);
                             op = ref Unsafe.Add(ref op, inputRemaining);
                             _remainingLiteral = (int) (literalLength - inputRemaining);
@@ -304,13 +298,13 @@ namespace Snappier.Internal
 
                                 if (newScratchLength > 0)
                                 {
                                     // Data has been moved to the scratch buffer
                                     input = ref scratch;
-                                    inputEnd = ref Unsafe.Add(ref input, newScratchLength - 1);
+                                    inputEnd = ref Unsafe.Add(ref input, newScratchLength);
                                     inputLimitMinMaxTagLength = ref Unsafe.Subtract(ref inputEnd,
-                                        Math.Min(newScratchLength, Constants.MaximumTagLength - 1) - 1);
+                                        Math.Min(newScratchLength, Constants.MaximumTagLength - 1));
 
                                 }
                             }
 
                             preload = Helpers.UnsafeReadUInt32(ref input);
@@ -365,13 +359,13 @@ namespace Snappier.Internal
 
                             if (newScratchLength > 0)
                             {
                                 // Data has been moved to the scratch buffer
                                 input = ref scratch;
-                                inputEnd = ref Unsafe.Add(ref input, newScratchLength - 1);
+                                inputEnd = ref Unsafe.Add(ref input, newScratchLength);
                                 inputLimitMinMaxTagLength = ref Unsafe.Subtract(ref inputEnd,
-                                    Math.Min(newScratchLength, Constants.MaximumTagLength - 1) - 1);
+                                    Math.Min(newScratchLength, Constants.MaximumTagLength - 1));
                             }
                         }
 
                         preload = Helpers.UnsafeReadUInt32(ref input);
                     }
@@ -413,11 +407,11 @@ namespace Snappier.Internal
 
                     literalLength = Helpers.ExtractLowBytes(literalLengthTemp,
                         (int) literalLengthLength) + 1;
                 }
 
-                nint inputRemaining = Unsafe.ByteOffset(ref input, ref inputEnd) + 1;
+                nint inputRemaining = Unsafe.ByteOffset(ref input, ref inputEnd);
                 if (inputRemaining < literalLength)
                 {
                     Append(ref op, ref bufferEnd, in input, inputRemaining);
                     _remainingLiteral = (int) (literalLength - inputRemaining);
                     _lookbackPosition += (int)Unsafe.ByteOffset(ref buffer, ref op);
@@ -466,20 +460,20 @@ namespace Snappier.Internal
         // DecompressAllTags will short circuit.
         private uint RefillTagFromScratch(ref byte input, ref byte inputEnd, ref byte scratch)
         {
             Debug.Assert(_scratchLength > 0);
 
-            if (Unsafe.IsAddressGreaterThan(ref input, ref inputEnd))
+            if (!Unsafe.IsAddressLessThan(ref input, ref inputEnd))
             {
                 return 0;
             }
 
             // Read the tag character
             uint entry = Constants.CharTable[scratch];
             uint needed = (entry >> 11) + 1; // +1 byte for 'c'
 
-            uint toCopy = Math.Min((uint)Unsafe.ByteOffset(ref input, ref inputEnd) + 1, needed - _scratchLength);
+            uint toCopy = Math.Min((uint)Unsafe.ByteOffset(ref input, ref inputEnd), needed - _scratchLength);
             Unsafe.CopyBlockUnaligned(ref Unsafe.Add(ref scratch, _scratchLength), ref input, toCopy);
 
             _scratchLength += toCopy;
 
             if (_scratchLength < needed)
@@ -500,20 +494,20 @@ namespace Snappier.Internal
         // Returns a small number if we have enough data for this tag but not enough to safely load preload without a buffer
         // overrun. In this case, further reads should be from scratch with a length up to the returned number. Scratch will
         // always have some extra bytes on the end so we don't risk buffer overruns.
         private uint RefillTag(ref byte input, ref byte inputEnd, ref byte scratch)
         {
-            if (Unsafe.IsAddressGreaterThan(ref input, ref inputEnd))
+            if (!Unsafe.IsAddressLessThan(ref input, ref inputEnd))
             {
                 return uint.MaxValue;
             }
 
             // Read the tag character
             uint entry = Constants.CharTable[input];
             uint needed = (entry >> 11) + 1; // +1 byte for 'c'
 
-            uint inputLength = (uint)Unsafe.ByteOffset(ref input, ref inputEnd) + 1;
+            uint inputLength = (uint)Unsafe.ByteOffset(ref input, ref inputEnd);
             if (inputLength < needed)
             {
                 // Data is insufficient, copy to scratch
                 Unsafe.CopyBlockUnaligned(ref scratch, ref input, inputLength);
 
@@ -553,15 +547,12 @@ namespace Snappier.Internal
                     if (_lookbackBufferArray is not null)
                     {
                         ArrayPool<byte>.Shared.Return(_lookbackBufferArray);
                     }
 
-                    // Always pad the lookback buffer with an extra byte that we don't use. This allows a "ref byte" reference past
-                    // the end of the perceived buffer that still points within the array. This is a requirement so that GC can recognize
-                    // the "ref byte" points within the array and adjust it if the array is moved.
-                    _lookbackBufferArray = ArrayPool<byte>.Shared.Rent(value.GetValueOrDefault() + 1);
-                    _lookbackBuffer = _lookbackBufferArray.AsMemory(0, _lookbackBufferArray.Length - 1);
+                    _lookbackBufferArray = ArrayPool<byte>.Shared.Rent(value.GetValueOrDefault());
+                    _lookbackBuffer = _lookbackBufferArray.AsMemory(0, _lookbackBufferArray.Length);
                 }
             }
         }
 
         public int UnreadBytes
@@ -593,22 +584,22 @@ namespace Snappier.Internal
             Append(ref op, ref Unsafe.Add(ref lookbackSpan[0], lookbackSpan.Length), in inputPtr, input.Length);
             _lookbackPosition += input.Length;
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private void Append(ref byte op, ref byte bufferEnd, in byte input, nint length)
+        private static void Append(ref byte op, ref byte bufferEnd, in byte input, nint length)
         {
             if (length > Unsafe.ByteOffset(ref op, ref bufferEnd))
             {
                 ThrowHelper.ThrowInvalidDataException("Data too long");
             }
 
             Unsafe.CopyBlockUnaligned(ref op, ref Unsafe.AsRef(in input), (uint) length);
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private bool TryFastAppend(ref byte op, ref byte bufferEnd, in byte input, nint available, nint length)
+        private static bool TryFastAppend(ref byte op, ref byte bufferEnd, in byte input, nint available, nint length)
         {
             if (length <= 16 && available >= 16 + Constants.MaximumTagLength &&
                 Unsafe.ByteOffset(ref op, ref bufferEnd) >= (nint) 16)
             {
                 CopyHelpers.UnalignedCopy128(in input, ref op);
@@ -617,23 +608,27 @@ namespace Snappier.Internal
 
             return false;
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private void AppendFromSelf(ref byte op, ref byte buffer, ref byte bufferEnd, uint copyOffset, nint length)
+        private static void AppendFromSelf(ref byte op, ref byte buffer, ref byte bufferEnd, uint copyOffset, nint length)
         {
-            ref byte source = ref Unsafe.Subtract(ref op, copyOffset);
-            if (!Unsafe.IsAddressLessThan(ref source, ref op) || Unsafe.IsAddressLessThan(ref source, ref buffer))
+            // ToInt64() ensures that this logic works correctly on x86 (with a slight perf hit on x86, though). This is because
+            // nint is only 32-bit on x86, so casting uint copyOffset to an nint for the comparison can result in a negative number with some
+            // forms of illegal data. This would then bypass the exception and cause unsafe memory access. Performing the comparison
+            // as a long ensures we have enough bits to not lose data. On 64-bit platforms this is effectively a no-op.
+            if (copyOffset == 0 || Unsafe.ByteOffset(ref buffer, ref op).ToInt64() < copyOffset)
             {
                 ThrowHelper.ThrowInvalidDataException("Invalid copy offset");
             }
 
             if (length > Unsafe.ByteOffset(ref op, ref bufferEnd))
             {
                 ThrowHelper.ThrowInvalidDataException("Data too long");
             }
 
+            ref byte source = ref Unsafe.Subtract(ref op, copyOffset);
             CopyHelpers.IncrementalCopy(ref source, ref op,
                 ref Unsafe.Add(ref op, length), ref bufferEnd);
         }
 
         public int Read(Span<byte> destination)

Vulnerable code:             ref byte s2Limit = ref Unsafe.Add(ref s1, _array.Length - 1);
                SnappyCompressor.FindMatchLength(ref s1, ref s2, ref Unsafe.Add(ref s2, length - 1), ref data);
                // Last byte of the input, not one byte past the end, to avoid issues on GC moves
                ref byte inputEnd = ref Unsafe.Add(ref inputStart, input.Length - 1);
                    ref byte ipLimit = ref Unsafe.Subtract(ref inputEnd, Constants.InputMarginBytes - 1);
                        Debug.Assert(!Unsafe.IsAddressGreaterThan(ref Unsafe.Add(ref nextEmit, 16), ref Unsafe.Add(ref inputEnd, 1)));
                if (!Unsafe.IsAddressGreaterThan(ref ip, ref inputEnd))
                    op = ref EmitLiteralSlow(ref op, ref ip, (uint) Unsafe.ByteOffset(ref ip, ref inputEnd) + 1);
        ///   and n &lt;= (s2_limit - s2 + 1).
        /// Does not read *(s1 + (s2_limit - s2 + 1)) or beyond.
        /// Requires that s2_limit+1 &gt;= s2.
        /// <remarks>
        /// The reference implementation has s2Limit as one byte past the end of the input,
        /// but this implementation has it at the end of the input. This ensures that it always
        /// points within the array in case GC moves the array.
        /// </remarks>
            Debug.Assert(!Unsafe.IsAddressLessThan(ref Unsafe.Add(ref s2Limit, 1), ref s2));
            while (!Unsafe.IsAddressGreaterThan(ref s2, ref Unsafe.Subtract(ref s2Limit, 3))
            if (BitConverter.IsLittleEndian && !Unsafe.IsAddressGreaterThan(ref s2, ref Unsafe.Subtract(ref s2Limit, 3)))
                while (!Unsafe.IsAddressGreaterThan(ref s2, ref s2Limit) && Unsafe.Add(ref s1, matched) == s2)
            if (!Unsafe.IsAddressGreaterThan(ref s2, ref Unsafe.Subtract(ref s2Limit, 7)))
            if (!Unsafe.IsAddressGreaterThan(ref s2, ref Unsafe.Subtract(ref s2Limit, 15)))
            while (!Unsafe.IsAddressGreaterThan(ref s2, ref Unsafe.Subtract(ref s2Limit, 15)))
            while (!Unsafe.IsAddressGreaterThan(ref s2, ref s2Limit))
                    if (!Unsafe.IsAddressGreaterThan(ref s2, ref Unsafe.Subtract(ref s2Limit, 7)))

                // The reference Snappy implementation uses inputEnd as a pointer one byte past the end of the buffer.
                // However, this is not safe when using ref locals. The ref must point to somewhere within the array
                // so that GC can adjust the ref if the memory is moved.
                ref byte inputEnd = ref Unsafe.Add(ref input, inputSpan.Length - 1);
                ref byte inputLimitMinMaxTagLength = ref Unsafe.Subtract(ref inputEnd, Math.Min(inputSpan.Length, Constants.MaximumTagLength - 1) - 1);
                // We always allocate buffer with at least one extra byte on the end, so bufferEnd doesn't have the same
                // restrictions as inputEnd.
                        inputEnd = ref Unsafe.Add(ref input, newScratchLength - 1);
                            Math.Min(newScratchLength, Constants.MaximumTagLength - 1) - 1);
                        if (TryFastAppend(ref op, ref bufferEnd, in input, Unsafe.ByteOffset(ref input, ref inputEnd) + 1, literalLength))
                        nint inputRemaining = Unsafe.ByteOffset(ref input, ref inputEnd) + 1;
                                    inputEnd = ref Unsafe.Add(ref input, newScratchLength - 1);
                                        Math.Min(newScratchLength, Constants.MaximumTagLength - 1) - 1);
                                inputEnd = ref Unsafe.Add(ref input, newScratchLength - 1);
                                    Math.Min(newScratchLength, Constants.MaximumTagLength - 1) - 1);
                nint inputRemaining = Unsafe.ByteOffset(ref input, ref inputEnd) + 1;
            if (Unsafe.IsAddressGreaterThan(ref input, ref inputEnd))
            uint toCopy = Math.Min((uint)Unsafe.ByteOffset(ref input, ref inputEnd) + 1, needed - _scratchLength);
            if (Unsafe.IsAddressGreaterThan(ref input, ref inputEnd))
            uint inputLength = (uint)Unsafe.ByteOffset(ref input, ref inputEnd) + 1;
                    // Always pad the lookback buffer with an extra byte that we don't use. This allows a "ref byte" reference past
                    // the end of the perceived buffer that still points within the array. This is a requirement so that GC can recognize
                    // the "ref byte" points within the array and adjust it if the array is moved.
                    _lookbackBufferArray = ArrayPool<byte>.Shared.Rent(value.GetValueOrDefault() + 1);
                    _lookbackBuffer = _lookbackBufferArray.AsMemory(0, _lookbackBufferArray.Length - 1);
        private void Append(ref byte op, ref byte bufferEnd, in byte input, nint length)
        private bool TryFastAppend(ref byte op, ref byte bufferEnd, in byte input, nint available, nint length)
        private void AppendFromSelf(ref byte op, ref byte buffer, ref byte bufferEnd, uint copyOffset, nint length)
            ref byte source = ref Unsafe.Subtract(ref op, copyOffset);
            if (!Unsafe.IsAddressLessThan(ref source, ref op) || Unsafe.IsAddressLessThan(ref source, ref buffer))
Fixed code:             ref byte s2Limit = ref Unsafe.Add(ref s1, _array.Length);
                SnappyCompressor.FindMatchLength(ref s1, ref s2, ref Unsafe.Add(ref s2, length), ref data);
                ref byte inputEnd = ref Unsafe.Add(ref inputStart, input.Length);
                    ref byte ipLimit = ref Unsafe.Subtract(ref inputEnd, Constants.InputMarginBytes);
                        Debug.Assert(!Unsafe.IsAddressGreaterThan(ref Unsafe.Add(ref nextEmit, 16), ref inputEnd));
                if (Unsafe.IsAddressLessThan(ref ip, ref inputEnd))
                    op = ref EmitLiteralSlow(ref op, ref ip, (uint) Unsafe.ByteOffset(ref ip, ref inputEnd));
        ///   and n &lt;= (s2_limit - s2).
        /// Does not read *(s1 + (s2_limit - s2)) or beyond.
        /// Requires that s2_limit &gt;= s2.
            Debug.Assert(!Unsafe.IsAddressLessThan(ref s2Limit, ref s2));
            while (Unsafe.ByteOffset(ref s2, ref s2Limit) >= (nint)4
            if (BitConverter.IsLittleEndian && Unsafe.ByteOffset(ref s2, ref s2Limit) >= (nint)4)
                while (Unsafe.IsAddressLessThan(ref s2, ref s2Limit) && Unsafe.Add(ref s1, matched) == s2)
            if (Unsafe.ByteOffset(ref s2, ref s2Limit) >= (nint)8)
            if (Unsafe.ByteOffset(ref s2, ref s2Limit) >= (nint)16)
            while (Unsafe.ByteOffset(ref s2, ref s2Limit) >= (nint)16)
            while (Unsafe.IsAddressLessThan(ref s2, ref s2Limit))
                    if (Unsafe.ByteOffset(ref s2, ref s2Limit) >= (nint)8)
                ref byte inputEnd = ref Unsafe.Add(ref input, inputSpan.Length);
                ref byte inputLimitMinMaxTagLength = ref Unsafe.Subtract(ref inputEnd, Math.Min(inputSpan.Length, Constants.MaximumTagLength - 1));
                        inputEnd = ref Unsafe.Add(ref input, newScratchLength);
                            Math.Min(newScratchLength, Constants.MaximumTagLength - 1));
                        if (TryFastAppend(ref op, ref bufferEnd, in input, Unsafe.ByteOffset(ref input, ref inputEnd), literalLength))
                        nint inputRemaining = Unsafe.ByteOffset(ref input, ref inputEnd);
                                    inputEnd = ref Unsafe.Add(ref input, newScratchLength);
                                        Math.Min(newScratchLength, Constants.MaximumTagLength - 1));
                                inputEnd = ref Unsafe.Add(ref input, newScratchLength);
                                    Math.Min(newScratchLength, Constants.MaximumTagLength - 1));
                nint inputRemaining = Unsafe.ByteOffset(ref input, ref inputEnd);
            if (!Unsafe.IsAddressLessThan(ref input, ref inputEnd))
            uint toCopy = Math.Min((uint)Unsafe.ByteOffset(ref input, ref inputEnd), needed - _scratchLength);
            if (!Unsafe.IsAddressLessThan(ref input, ref inputEnd))
            uint inputLength = (uint)Unsafe.ByteOffset(ref input, ref inputEnd);
                    _lookbackBufferArray = ArrayPool<byte>.Shared.Rent(value.GetValueOrDefault());
                    _lookbackBuffer = _lookbackBufferArray.AsMemory(0, _lookbackBufferArray.Length);
        private static void Append(ref byte op, ref byte bufferEnd, in byte input, nint length)
        private static bool TryFastAppend(ref byte op, ref byte bufferEnd, in byte input, nint available, nint length)
        private static void AppendFromSelf(ref byte op, ref byte buffer, ref byte bufferEnd, uint copyOffset, nint length)
            // ToInt64() ensures that this logic works correctly on x86 (with a slight perf hit on x86, though). This is because
            // nint is only 32-bit on x86, so casting uint copyOffset to an nint for the comparison can result in a negative number with some
            // forms of illegal data. This would then bypass the exception and cause unsafe memory access. Performing the comparison
            // as a long ensures we have enough bits to not lose data. On 64-bit platforms this is effectively a no-op.
            if (copyOffset == 0 || Unsafe.ByteOffset(ref buffer, ref op).ToInt64() < copyOffset)
            ref byte source = ref Unsafe.Subtract(ref op, copyOffset);