fix: Fixes ObjectPropertyList double return issue (#1969)

- Fixes double return issue with object property list that is causing corruption.
- Adds DEBUG_ARRAYPOOL define constant which will crash on double return or invalid return scenarios.

> [!IMPORTANT]
> **Developer Notes**
> STArrayPool rented arrays **MUST NOT** be returned **ONLY ONCE** otherwise there will be corruption from double-use.
> Use `DEBUG_ARRAYPOOL` to test potential broken STArrayPool use cases.

> [!NOTE]
> **Why can't I enable the debug all the time?**
> Other than the fact that it will crash due to bad code, the actual tracking system is highly detrimental/problematic for performance and memory consumption by creating objects that have a stack trace.
This commit is contained in:
Kamron Batman 2024-10-07 19:21:55 -07:00
parent 4b3b2839c6
commit b9d63e4160
No known key found for this signature in database
GPG key ID: 7D81DF26D9A5D94A
3 changed files with 69 additions and 8 deletions

View file

@ -15,6 +15,16 @@ namespace Server.Buffers;
*/
public class STArrayPool<T> : ArrayPool<T>
{
#if DEBUG_ARRAYPOOL
private class RentReturnStatus
{
public string StackTrace { get; set; }
public bool IsRented { get; set; }
}
private static readonly ConditionalWeakTable<T[], RentReturnStatus> _rentedArrays = new();
#endif
private const int StackArraySize = 32;
private const int BucketCount = 27; // SelectBucketIndex(1024 * 1024 * 1024 + 1)
private static readonly STArrayPool<T> _shared = new();
@ -39,6 +49,12 @@ public class STArrayPool<T> : ArrayPool<T>
if (buffer is not null)
{
cachedBuckets[bucketIndex].Array = null;
#if DEBUG_ARRAYPOOL
_rentedArrays.AddOrUpdate(
buffer,
new RentReturnStatus { IsRented = true }
);
#endif
return buffer;
}
}
@ -52,6 +68,12 @@ public class STArrayPool<T> : ArrayPool<T>
buffer = b.TryPop();
if (buffer is not null)
{
#if DEBUG_ARRAYPOOL
_rentedArrays.AddOrUpdate(
buffer,
new RentReturnStatus { IsRented = true }
);
#endif
return buffer;
}
}
@ -70,8 +92,16 @@ public class STArrayPool<T> : ArrayPool<T>
throw new ArgumentOutOfRangeException(nameof(minimumLength));
}
buffer = GC.AllocateUninitializedArray<T>(minimumLength);
return buffer;
var array = GC.AllocateUninitializedArray<T>(minimumLength);
#if DEBUG_ARRAYPOOL
_rentedArrays.AddOrUpdate(
array,
new RentReturnStatus { IsRented = true, StackTrace = Environment.StackTrace }
);
#endif
return array;
}
public override void Return(T[]? array, bool clearArray = false)
@ -91,10 +121,26 @@ public class STArrayPool<T> : ArrayPool<T>
Array.Clear(array);
}
#if DEBUG_ARRAYPOOL
if (array.Length != GetMaxSizeForBucket(bucketIndex) || !_rentedArrays.TryGetValue(array, out var status))
{
throw new ArgumentException("Buffer is not from the pool", nameof(array));
}
if (!status!.IsRented)
{
throw new InvalidOperationException($"Array has already been returned.\nOriginal StackTrace:{status.StackTrace}\n");
}
// Mark it as returned
status.IsRented = false;
status.StackTrace = Environment.StackTrace;
#else
if (array.Length != GetMaxSizeForBucket(bucketIndex))
{
throw new ArgumentException("Buffer is not from the pool", nameof(array));
}
#endif
ref var bucketArray = ref cacheBuckets[bucketIndex];
var prev = bucketArray.Array;