Skip to content

Commit

Permalink
fix: Fixes ObjectPropertyList double return issue (#1969)
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
kamronbatman committed Oct 8, 2024
1 parent 4b3b283 commit b9d63e4
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 8 deletions.
50 changes: 48 additions & 2 deletions Projects/Server/Buffers/STArrayPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -39,6 +49,12 @@ public override T[] Rent(int minimumLength)
if (buffer is not null)
{
cachedBuckets[bucketIndex].Array = null;
#if DEBUG_ARRAYPOOL
_rentedArrays.AddOrUpdate(
buffer,
new RentReturnStatus { IsRented = true }
);
#endif
return buffer;
}
}
Expand All @@ -52,6 +68,12 @@ public override T[] Rent(int minimumLength)
buffer = b.TryPop();
if (buffer is not null)
{
#if DEBUG_ARRAYPOOL
_rentedArrays.AddOrUpdate(
buffer,
new RentReturnStatus { IsRented = true }
);
#endif
return buffer;
}
}
Expand All @@ -70,8 +92,16 @@ public override T[] Rent(int minimumLength)
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)
Expand All @@ -91,10 +121,26 @@ public override void Return(T[]? array, bool clearArray = false)
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;
Expand Down
19 changes: 13 additions & 6 deletions Projects/Server/PropertyList/ObjectPropertyList.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
* Copyright 2019-2023 - ModernUO Development Team *
* Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: ObjectPropertyList.cs *
* *
Expand Down Expand Up @@ -79,8 +79,9 @@ public void Reset()
_stringNumbersIndex = 0;
Header = 0;
HeaderArgs = null;
STArrayPool<char>.Shared.Return(_arrayToReturnToPool);
_pos = 0;

Dispose();
}

private void Flush()
Expand Down Expand Up @@ -499,13 +500,19 @@ private void GrowCore(uint requiredMinCapacity)

public void Dispose()
{
STArrayPool<char>.Shared.Return(_arrayToReturnToPool);
_arrayToReturnToPool = null;
if (_arrayToReturnToPool != null)
{
STArrayPool<char>.Shared.Return(_arrayToReturnToPool);
_arrayToReturnToPool = null;
}
}

~ObjectPropertyList()
{
STArrayPool<char>.Shared.Return(_arrayToReturnToPool);
_arrayToReturnToPool = null;
if (_arrayToReturnToPool != null)
{
STArrayPool<char>.Shared.Return(_arrayToReturnToPool);
_arrayToReturnToPool = null;
}
}
}
8 changes: 8 additions & 0 deletions Projects/Server/Utilities/Utility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1460,4 +1460,12 @@ public static int C32216(this int c32)

return (r << 10) | (g << 5) | b;
}

public static void AddOrUpdate<TKey, TValue>(this ConditionalWeakTable<TKey, TValue> table, TKey key, TValue value)
where TKey : class
where TValue : class
{
table.Remove(key);
table.Add(key, value);
}
}

0 comments on commit b9d63e4

Please sign in to comment.