From 5776911af0dce75ad866d86f05f8899f926fdf21 Mon Sep 17 00:00:00 2001 From: Nick <2506116+k5njm@users.noreply.github.com> Date: Tue, 7 Jul 2026 01:06:18 -0500 Subject: [PATCH] fix: FontDecompressor OOM aborts on the render path (-fno-exceptions makes vector resize fatal) (#2526) --- lib/EpdFont/FontDecompressor.cpp | 46 ++++++++++++++++++-------------- lib/EpdFont/FontDecompressor.h | 17 ++++++++---- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/lib/EpdFont/FontDecompressor.cpp b/lib/EpdFont/FontDecompressor.cpp index adfbb628..786f7d3e 100644 --- a/lib/EpdFont/FontDecompressor.cpp +++ b/lib/EpdFont/FontDecompressor.cpp @@ -33,12 +33,24 @@ void FontDecompressor::freePageBuffer() { } void FontDecompressor::freeHotGroup() { - hotGroup.clear(); - hotGroup.shrink_to_fit(); + free(hotGroup); + hotGroup = nullptr; + hotGroupCapacity = 0; hotGroupFont = nullptr; hotGroupIndex = UINT16_MAX; - hotGlyphBuf.clear(); - hotGlyphBuf.shrink_to_fit(); + free(hotGlyphBuf); + hotGlyphBuf = nullptr; + hotGlyphBufCapacity = 0; +} + +bool FontDecompressor::ensureCapacity(uint8_t*& buf, uint32_t& capacity, uint32_t needed) { + if (capacity >= needed) return true; + // Grow-only, free-then-malloc: every caller fully rewrites the buffer after a grow, so the + // old contents are dead -- freeing first gives the allocator its best shot on a tight heap. + free(buf); + buf = static_cast(malloc(needed)); // owned by FontDecompressor, freed in freeHotGroup() + capacity = buf ? needed : 0; + return buf != nullptr; } uint16_t FontDecompressor::getGroupIndex(const EpdFontData* fontData, uint32_t glyphIndex) { @@ -170,24 +182,20 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep } // Check if hot group already has this group decompressed — if not, decompress it - if (!(!hotGroup.empty() && hotGroupFont == fontData && hotGroupIndex == groupIndex)) { + if (!(hotGroup != nullptr && hotGroupFont == fontData && hotGroupIndex == groupIndex)) { stats.cacheMisses++; const EpdFontGroup& group = fontData->groups[groupIndex]; - hotGroup.resize(group.uncompressedSize); - if (hotGroup.empty()) { + // ensureCapacity may free the buffer, so the cached-group identity dies with it either way. + hotGroupFont = nullptr; + hotGroupIndex = UINT16_MAX; + if (!ensureCapacity(hotGroup, hotGroupCapacity, group.uncompressedSize)) { LOG_ERR("FDC", "Failed to allocate %u bytes for hot group %u", group.uncompressedSize, groupIndex); - hotGroupFont = nullptr; - hotGroupIndex = UINT16_MAX; stats.getBitmapTimeUs += micros() - tStart; return nullptr; } - if (!decompressGroup(fontData, groupIndex, hotGroup.data(), group.uncompressedSize)) { - hotGroup.clear(); - hotGroup.shrink_to_fit(); - hotGroupFont = nullptr; - hotGroupIndex = UINT16_MAX; + if (!decompressGroup(fontData, groupIndex, hotGroup, group.uncompressedSize)) { stats.getBitmapTimeUs += micros() - tStart; return nullptr; } @@ -200,18 +208,16 @@ const uint8_t* FontDecompressor::getBitmap(const EpdFontData* fontData, const Ep } // Compact just the requested glyph from byte-aligned data into scratch buffer - if (glyph->dataLength > hotGlyphBuf.size()) { - hotGlyphBuf.resize(glyph->dataLength); - } - if (hotGlyphBuf.empty()) { + if (!ensureCapacity(hotGlyphBuf, hotGlyphBufCapacity, glyph->dataLength)) { + LOG_ERR("FDC", "Failed to allocate %u bytes for glyph scratch", (unsigned)glyph->dataLength); stats.getBitmapTimeUs += micros() - tStart; return nullptr; } uint32_t alignedOff = getAlignedOffset(fontData, groupIndex, glyphIndex); - compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf.data(), glyph->width, glyph->height); + compactSingleGlyph(&hotGroup[alignedOff], hotGlyphBuf, glyph->width, glyph->height); stats.getBitmapTimeUs += micros() - tStart; - return hotGlyphBuf.data(); + return hotGlyphBuf; } // --- Prewarm: pre-decompress glyph bitmaps for a page of text --- diff --git a/lib/EpdFont/FontDecompressor.h b/lib/EpdFont/FontDecompressor.h index 54e75a86..6ac27647 100644 --- a/lib/EpdFont/FontDecompressor.h +++ b/lib/EpdFont/FontDecompressor.h @@ -2,8 +2,6 @@ #include -#include - #include "EpdFontData.h" class FontDecompressor { @@ -67,13 +65,22 @@ class FontDecompressor { // Hot group: last decompressed group (byte-aligned) for non-prewarmed fallback path. // Kept in byte-aligned format; individual glyphs are compacted on demand into hotGlyphBuf. + // Nothrow high-water malloc buffers, NOT std::vector: getBitmap() runs on the render path, + // and under -fno-exceptions a vector resize that hits OOM abort()s the firmware instead of + // failing (field crash: hotGroup.resize() -> std::bad_alloc -> abort with ~11 KB free). + // ensureCapacity() returns false on OOM so the caller can skip the glyph gracefully. const EpdFontData* hotGroupFont = nullptr; uint16_t hotGroupIndex = UINT16_MAX; - std::vector hotGroup; + uint8_t* hotGroup = nullptr; // owned; freed in freeHotGroup()/dtor + uint32_t hotGroupCapacity = 0; // Scratch buffer for compacting a single glyph from the hot group. - // Valid until the next getBitmap() call. - std::vector hotGlyphBuf; + // Valid until the next getBitmap() call. Same ownership/OOM contract as hotGroup. + uint8_t* hotGlyphBuf = nullptr; + uint32_t hotGlyphBufCapacity = 0; + + // Grow (never shrink) an owned buffer to at least `needed` bytes; false on OOM, buffer freed. + static bool ensureCapacity(uint8_t*& buf, uint32_t& capacity, uint32_t needed); void freePageBuffer(); void freeHotGroup();