From b9dfce3c662616257eb3ef171ef6186bd8b11dc9 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 6 Feb 2026 21:13:06 -0800 Subject: [PATCH] Fix M2 particle emitter crash: correct struct size, FBlock format, and add safety caps The particle emitter parser had three bugs causing OOM crashes during loading: - Struct size was 496 bytes instead of correct WotLK 476 (0x1DC), misaligning multi-emitter models - FBlocks were read as 20-byte M2TrackDisk instead of 16-byte FBlockDisk (no interp/seq prefix) - parseAnimTrack had no cap on sequence count, allowing garbage data to allocate billions of entries --- src/pipeline/m2_loader.cpp | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/pipeline/m2_loader.cpp b/src/pipeline/m2_loader.cpp index 733cbe19..48f5a05a 100644 --- a/src/pipeline/m2_loader.cpp +++ b/src/pipeline/m2_loader.cpp @@ -129,6 +129,15 @@ struct M2TrackDisk { uint32_t ofsKeys; }; +// FBlock header (on-disk, 16 bytes) — particle lifetime curves +// Like M2TrackDisk but WITHOUT interpolationType/globalSequence prefix +struct FBlockDisk { + uint32_t nTimestamps; + uint32_t ofsTimestamps; + uint32_t nKeys; + uint32_t ofsKeys; +}; + // Full M2 bone structure (on-disk, 88 bytes) struct M2BoneDisk { int32_t keyBoneId; // 4 @@ -296,6 +305,8 @@ void parseAnimTrack(const std::vector& data, if (disk.nTimestamps == 0 || disk.nKeys == 0) return; uint32_t numSubArrays = disk.nTimestamps; + // Sanity cap: no model has >4096 animation sequences; garbage counts cause OOM + if (numSubArrays > 4096) return; track.sequences.resize(numSubArrays); for (uint32_t i = 0; i < numSubArrays; i++) { @@ -370,15 +381,17 @@ void parseAnimTrack(const std::vector& data, } } -// Parse an FBlock (particle lifetime curve) from a 20-byte on-disk header. -// FBlocks use the same layout as M2TrackDisk but timestamps/values are flat arrays. +// Parse an FBlock (particle lifetime curve) from a 16-byte on-disk header. +// FBlocks are like M2Track but WITHOUT the interpolationType/globalSequence prefix. void parseFBlock(const std::vector& data, uint32_t offset, M2FBlock& fb, int valueType) { // valueType: 0 = color (3 bytes RGB), 1 = alpha (uint16), 2 = scale (float pair) - if (offset + 20 > data.size()) return; + if (offset + sizeof(FBlockDisk) > data.size()) return; - M2TrackDisk disk = readValue(data, offset); + FBlockDisk disk = readValue(data, offset); if (disk.nTimestamps == 0 || disk.nKeys == 0) return; + // Sanity cap: particle FBlocks typically have 3 keyframes + if (disk.nTimestamps > 1024 || disk.nKeys > 1024) return; // FBlock timestamps are uint16 (not sub-arrays), stored directly if (disk.ofsTimestamps + disk.nTimestamps * sizeof(uint16_t) > data.size()) return; @@ -656,8 +669,8 @@ M2Model M2Loader::load(const std::vector& m2Data) { model.attachmentLookup = readArray(m2Data, header.ofsAttachmentLookup, header.nAttachmentLookup); } - // Parse particle emitters (WotLK M2ParticleOld: 0x1F0 = 496 bytes per emitter) - static constexpr uint32_t EMITTER_STRUCT_SIZE = 0x1F0; + // Parse particle emitters (WotLK M2ParticleOld: 0x1DC = 476 bytes per emitter) + static constexpr uint32_t EMITTER_STRUCT_SIZE = 0x1DC; if (header.nParticleEmitters > 0 && header.ofsParticleEmitters > 0 && header.nParticleEmitters < 256 && static_cast(header.ofsParticleEmitters) + @@ -702,10 +715,10 @@ M2Model M2Loader::load(const std::vector& m2Data) { parseTrack(0xDC, em.emissionAreaWidth); parseTrack(0xF0, em.deceleration); - // Parse FBlocks (color, alpha, scale) + // Parse FBlocks (color, alpha, scale) — FBlocks are 16 bytes each parseFBlock(m2Data, base + 0x104, em.particleColor, 0); - parseFBlock(m2Data, base + 0x118, em.particleAlpha, 1); - parseFBlock(m2Data, base + 0x12C, em.particleScale, 2); + parseFBlock(m2Data, base + 0x114, em.particleAlpha, 1); + parseFBlock(m2Data, base + 0x124, em.particleScale, 2); model.particleEmitters.push_back(std::move(em)); }