Skip to content

IntrusiveAllocator: heap corruption when large allocations interleave with MemoryBlock slot reuse (crash in Object::unref) #1733

Description

@HouWei-17

Environment

  • VSG 1.1.15 (vcpkg, x64-windows), MSVC 19.44, Windows 10/11
  • Vulkan driver: NVIDIA (RTX 3060 Ti, driver 595.79)

Summary

Application crashes with an access violation inside vsg::Object::unref (calling the vtable destructor on a corrupted/freed object) during vsg::Builder::createQuad. The affected object's vtable/refcount bytes are overwritten with values that look like IntrusiveAllocator::MemoryBlock slot metadata (observed pattern: 0x0002 0x0c91 0xffffffff). The corruption is deterministic for a given build but moves/disappears when the heap layout changes (e.g. when a debugger or procdump is attached), which points to free-list corruption.

Repro pattern (full application; minimal repro not yet isolated)

  1. Create a vsgXchange::all reader tree inside a per-call helper function and destroy it each time an image is loaded (allocates ~hundreds of objects, then frees them).
  2. Allocate a large vsg::Data (e.g. a 4 MB ubvec4Array2D) and feed it to vsg::Builder::createQuad (textured quad).
  3. Repeat loads / rebuild the scene graph. Crash occurs in Object::unref of a still-referenced object during a later createQuad call.

Investigation notes

  • IntrusiveAllocator::deallocate(ptr, size) locates the owning block by pointer range (memoryBlocks.upper_bound(ptr) then --itr) and runs MemoryBlock::deallocate(ptr, size) before consulting largeAllocations. A large allocation (made via operator new when size > maximumAllocationSize) whose address happens to fall within a MemoryBlock's address range will be treated as a slot in that block, marking an unrelated slot free and corrupting the free list.
  • Instrumentation showed a live object's reference count dropping 2 -> 0 during Builder::createQuad with no matching unref in application code.
  • Workaround that eliminates the crash: replace Allocator::instance() with a pass-through allocator using plain ::operator new/delete (no MemoryBlocks). With that, no corruption is observed.

Expected

Either the deallocation path should never interpret large-allocation pointers as block slots, or large allocations should be range-checked against the actual MemoryBlock extents before slot math. A defensive check would also help: IntrusiveAllocator::deallocate validating that ptr is inside [block.start, block.start + block.size) before computing a slot index.

Additional context

Reproducing reliably in Release builds; Debug builds mask the corruption (different heap layout / allocator validation).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions