Mutable: heap buffer overflow in GenerateAndAddLaplacianData (OpMeshBind.h) due to FMeshBufferSet::GetDataSize() used as memcpy size

Summary

UE::Mutable::Private::GenerateAndAddLaplacianData copies the Laplacian data into three newly created FMeshBufferSet objects using FMeshBufferSet::GetDataSize() as the memcpy size. GetDataSize() returns the element data size plus sizeof(FMeshBufferChannel) * NumChannels, but the destination Buffer.Data array holds only ElementSize * ElementCount bytes. Each memcpy therefore writes sizeof(FMeshBufferChannel) (16) bytes past the end of the destination and reads 16 bytes past the end of the source TArray. This is silent in most cases and becomes an access violation when the destination allocation ends close to an unmapped page.

Affected code (Engine/Plugins/Mutable/Source/MutableRuntime/Private/MuR/OpMeshBind.h, lines 924-937)

FMemory::Memcpy(
UniqueVertexMapBuffer.GetBufferData(0),
UniqueVertexMap.GetData(),
UniqueVertexMapBuffer.GetDataSize());

FMemory::Memcpy(
MeshLaplacianOffsetsBuffer.GetBufferData(0),
VertexRingsOffsets.GetData(),
MeshLaplacianOffsetsBuffer.GetDataSize());

FMemory::Memcpy(
MeshLaplacianDataBuffer.GetBufferData(0),
VertexRingsData.GetData(),
MeshLaplacianDataBuffer.GetDataSize());

FMeshBufferSet::GetDataSize() (MeshBufferSet.cpp):

Result += sizeof(FMeshBufferChannel) * Buffers[BufferIndex].Channels.Num();
if (!bIsDescriptor)
{
Result += Buffers[BufferIndex].ElementSize * ElementCount;
}

The destination is sized in SetElementCount as Buffer.Data.SetNumUninitialized(Buffer.ElementSize * Count), and GetBufferDataSize(int32) already returns exactly Data.Num() (with an ensure against ElementSize * ElementCount in editor builds). sizeof(FMeshBufferChannel) is 16 (two uint32 enums, int32, two uint16).

What Type of Bug are you experiencing?

Foundation (C++ Tools, Profiling, & Pipeline)

Steps to Reproduce

Not deterministic. Any mesh that goes through MeshBindShapeReshape with EMeshBindShapeFlags::ApplyLaplacian overflows by 16 bytes per buffer (3 buffers). Whether it crashes depends on heap layout (destination block located within 16 bytes of an unmapped page). In our project it reproduced on a packaged build while building a customized character (CO_Villager_Inst) with a Reshape node that has Apply Laplacian enabled, about 1 second after startup. A debug allocator with guard pages (e.g. page heap / Stomp allocator) should reproduce it on every run.

Expected Result

Copies are bounded by the destination buffer size.

Observed Result

Each copy overruns the destination by sizeof(FMeshBufferChannel) bytes (heap corruption, intermittent access violation).

Affects Versions

5.8

Platform(s)

Windows

For crash reports, include your callstack

Unhandled Exception: EXCEPTION_ACCESS_VIOLATION writing address 0x00007ff172cd8000
VCRUNTIME140!_NLG_Return2+0x9e3 (vmovdqu8 ymmword ptr [rcx+r9-20h]{k1},ymm1)
HyperEVO!UE::Mutable::Private::GenerateAndAddLaplacianData+0x6db
HyperEVO!UE::Mutable::Private::MeshBindShapeReshape+0x3af
HyperEVO!UE::Mutable::Private::CodeRunner::RunCode+0x554f
HyperEVO!UE::Mutable::Private::CodeRunner::Run+0x66c
… LowLevelTasks::FScheduler::WorkerLoop (Background Worker #2)

  • Faulting call: the memcpy for MeshLaplacianOffsetsBuffer (return address right after the second FMemory::Memcpy in the function).
  • Copy size register (rdi) = 0x1004 = 4100 bytes. 4100 = 16 + 4084, i.e. 1021 int32 elements plus one FMeshBufferChannel. The allocated destination holds 4084 bytes.
  • Destination starts at page boundary 0x7ff172cd7000; page 0x7ff172cd8000 is not mapped. The 16 extra bytes cross into it.