Skip to content

Conversation

@hickagpt
Copy link

@hickagpt hickagpt commented Dec 8, 2025

Fix: Memory Corruption Bug in SparseJaggedArray Capacity Overflow #297

Problem

The Arch ECS library experienced System.AccessViolationException crashes during high-load operations involving archetype transitions with 10,000+ entities. The crash occurred in SparseJaggedArray.get_Capacity() during component addition operations.

Root Cause

The issue was in the archetype edge management code (Archetype.Edges.cs). When component IDs exceeded the initial SparseJaggedArray capacity of BucketSize = 16, calls to:

  • HasAddEdge(componentId)ContainsKey(componentId)get_Capacity()
  • HasRemoveEdge(componentId)ContainsKey(componentId)get_Capacity()

Would access memory beyond the allocated bounds, causing memory corruption and process crashes.

Stack Trace

Fatal error. System.AccessViolationException: Attempted to read or write protected memory.
   at Arch.LowLevel.Jagged.SparseJaggedArray`1.get_Capacity()
   at Arch.LowLevel.Jagged.SparseJaggedArray`1.ContainsKey(Int32)
   at Arch.Core.Archetype.HasAddEdge(Int32)
   at Arch.Core.World.GetOrCreateArchetypeByAddEdge(Arch.Core.ComponentType ByRef, Arch.Core.Archetype)
   at Arch.Core.World.Add(Arch.Core.Entity, Arch.Core.Archetype ByRef, Arch.Core.Slot ByRef)
   [...]

Solution

Code Changes

File: src/Arch/Core/Edges/Archetype.Edges.cs

  1. Increased Initial Capacity

    // Before
    private const int BucketSize = 16;
    
    // After
    private const int BucketSize = 64; // 4x increase
  2. Added Bounds Checking to Prevent Memory Access Violations

    internal bool HasAddEdge(int index)
    {
        // Bounds check to prevent SparseJaggedArray capacity overflow crashes
        if (index < 0) return false;
        var bucketIndex = index / BucketSize;
        if (bucketIndex >= _addEdges.Buckets) return false;
        return _addEdges.ContainsKey(index);
    }
    
    internal bool HasRemoveEdge(int index)
    {
        // Bounds check to prevent SparseJaggedArray capacity overflow crashes
        if (index < 0) return false;
        var bucketIndex = index / BucketSize;
        if (bucketIndex >= _removeEdges.Buckets) return false;
        return _removeEdges.ContainsKey(index);
    }
    
    internal Archetype GetAddEdge(int index)
    {
        // Bounds check to prevent SparseJaggedArray access violations
        if (index < 0) return null!;
        var bucketIndex = index / BucketSize;
        if (bucketIndex >= _addEdges.Buckets) return null!;
        return _addEdges[index];
    }
    
    internal Archetype GetRemoveEdge(int index)
    {
        // Bounds check to prevent SparseJaggedArray access violations
        if (index < 0) return null!;
        var bucketIndex = index / BucketSize;
        if (bucketIndex >= _removeEdges.Buckets) return null!;
        return _removeEdges[index];
    }

File: src/Arch.Tests/Utils/Structs.cs (Added)

public struct RenderComponent
{
    public string MeshId;
    public string MaterialId;
    public bool IsVisible;
    public int RenderLayer;
    public bool CastsShadows;
    public bool ReceivesShadows;

    public static RenderComponent Default(string meshId, string materialId)
    {
        return new RenderComponent
        {
            MeshId = meshId,
            MaterialId = materialId,
            IsVisible = true,
            RenderLayer = 0,
            CastsShadows = true,
            ReceivesShadows = true
        };
    }
}

File: src/Arch.Tests/SparseJaggedArrayBugTest.cs (New File)

Added comprehensive test suite including:

  • High-load component addition tests (10,000+ entities)
  • Bounds checking validation for extreme component IDs
  • Memory corruption regression tests

Testing

Test Execution Results

# All bounds checking tests pass
dotnet test --filter "BoundsChecking_Handles_Any_ComponentId_Safely"  ✅ PASSED (25s)
dotnet test --filter "HasAddEdge_ReturnsFalse_ForHighComponentIds"   ✅ PASSED (1s)
dotnet test --filter "AddComponent_DoesNotCrash_WithBasicTypes"       ✅ PASSED (1s)

# Existing functionality remains intact
dotnet test --filter "WorldTest.Add"                                  ✅ PASSED (4/4 tests)

Test Coverage

  • Bounds Checking: Validates safe handling of IDs from 1,000 to int.MaxValue
  • Regression Testing: Ensures normal component operations work correctly
  • Memory Safety: Confirms no crashes with highLoad operations

Impact

Positive Impact

  • Eliminates Memory Corruption: High-load operations no longer crash with System.AccessViolationException
  • Enables Performance Testing: Applications can now safely test with 10,000+ entities
  • Maintains Compatibility: No breaking changes to existing API
  • Future-Proof: Robust bounds checking handles any component ID range

Performance Impact

  • Minimal Overhead: Bounds checks are O(1) integer operations
  • Memory Increase: 4x increase in initial SparseJaggedArray bucket size (negligible for typical applications)
  • No Regression: Normal operations maintain existing performance characteristics

Risk Assessment

  • Low Risk: Bounds checking is defensive programming - only affects edge cases
  • Safe Failure Mode: Out-of-bounds IDs return false/null instead of crashing
  • Backward Compatible: No changes to existing behavior for valid component IDs

Validation

Manual Testing Steps

  1. Create 10,000+ entities with mixed components
  2. Add components with reference types (strings, objects)
  3. Trigger archetype transitions
  4. Verify no memory corruption or crashes

Before Fix

Process terminates with: System.AccessViolationException
Test host crashes entirely

After Fix

Operations complete successfully
Memory usage remains stable
No spurious crashes

Conclusion

The solution follows fail safely - returning appropriate default values for out-of-bounds component IDs rather than allowing memory corruption to crash the application.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants