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)
- 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).
- Allocate a large
vsg::Data (e.g. a 4 MB ubvec4Array2D) and feed it to vsg::Builder::createQuad (textured quad).
- 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).
Environment
Summary
Application crashes with an access violation inside
vsg::Object::unref(calling the vtable destructor on a corrupted/freed object) duringvsg::Builder::createQuad. The affected object's vtable/refcount bytes are overwritten with values that look likeIntrusiveAllocator::MemoryBlockslot 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)
vsgXchange::allreader tree inside a per-call helper function and destroy it each time an image is loaded (allocates ~hundreds of objects, then frees them).vsg::Data(e.g. a 4 MBubvec4Array2D) and feed it tovsg::Builder::createQuad(textured quad).Object::unrefof a still-referenced object during a latercreateQuadcall.Investigation notes
IntrusiveAllocator::deallocate(ptr, size)locates the owning block by pointer range (memoryBlocks.upper_bound(ptr)then--itr) and runsMemoryBlock::deallocate(ptr, size)before consultinglargeAllocations. A large allocation (made viaoperator newwhensize > 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.Builder::createQuadwith no matching unref in application code.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::deallocatevalidating thatptris 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).