From 23aba785ccbc88f67e5a736b38be91d3e4726903 Mon Sep 17 00:00:00 2001 From: "Robert August Vincent II (Bob)" Date: Tue, 7 Jul 2026 13:32:58 -0400 Subject: [PATCH] fix: prevent Manage Fonts OOM crash with many SD fonts installed Opening Settings > Fonts > Manage Fonts aborted during manifest parsing on devices with many SD-card font families installed. At the parse peak the full font registry, the parsed manifest JsonDocument, and the families_ list were all resident at once, exhausting the heap; with exceptions disabled the failing allocation calls abort(). - onEnter() now releases the whole font registry via releaseForNetwork() (not just the active glyph font), matching the KOReader sync/auth pre-network release. - fetchAndParseManifest() parses in two passes: build families_ from the JSON, free the JsonDocument, then load the registry to resolve installed/update state, so the JSON and registry never coexist. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 6 + .../settings/FontDownloadActivity.cpp | 125 ++++++++++-------- 2 files changed, 77 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed643a87de..a87bc5e4b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Fixed + +- Manage Fonts no longer crashes (abort) while loading the font list on devices with many SD-card font families installed. The font registry is now released before the network request, and the manifest is parsed without keeping the parsed JSON and the font registry in memory at the same time. + ## [v1.4.0] - 2026-07-04 ### Added diff --git a/src/activities/settings/FontDownloadActivity.cpp b/src/activities/settings/FontDownloadActivity.cpp index 67c8b092e3..8f3e5ca4c1 100644 --- a/src/activities/settings/FontDownloadActivity.cpp +++ b/src/activities/settings/FontDownloadActivity.cpp @@ -113,7 +113,12 @@ FontDownloadActivity::FontDownloadActivity(GfxRenderer& renderer, MappedInputMan void FontDownloadActivity::onEnter() { Activity::onEnter(); - sdFontSystem.releaseLoadedFont(renderer); + // Free the whole SD font registry, not just the active glyph font, before the + // network + manifest work. With many families installed the registry, the + // parsed manifest, and the families_ list would otherwise all be resident at + // once and exhaust the heap, aborting during manifest parse. Matches the + // pre-network release done by the KOReader sync/auth activities. + sdFontSystem.releaseForNetwork(renderer); WiFi.mode(WIFI_STA); startActivityForResult(std::make_unique(renderer, mappedInput), [this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); }); @@ -196,74 +201,86 @@ bool FontDownloadActivity::fetchAndParseManifest() { return false; } - JsonDocument doc; - DeserializationError err = deserializeJson(doc, manifestFile); - manifestFile.close(); - Storage.remove(MANIFEST_TMP); - - if (err) { - LOG_ERR("FONT", "Manifest parse error: %s", err.c_str()); - errorMessage_ = "Invalid font manifest"; - return false; - } + // The parsed manifest and the installed-font registry are each large when many + // families are installed. Keep the JsonDocument in its own scope and defer + // loading the registry until after it is freed (see second pass below), so the + // two are never resident at the same time. Their coexistence here is what + // aborted during parse on low-heap devices with many SD fonts installed. + { + JsonDocument doc; + DeserializationError err = deserializeJson(doc, manifestFile); + manifestFile.close(); + Storage.remove(MANIFEST_TMP); - int version = doc["version"] | 0; - if (version != FONTS_MANIFEST_VERSION) { - LOG_ERR("FONT", "Unsupported manifest version: %d", version); - errorMessage_ = "Unsupported manifest version"; - return false; - } + if (err) { + LOG_ERR("FONT", "Manifest parse error: %s", err.c_str()); + errorMessage_ = "Invalid font manifest"; + return false; + } - baseUrl_ = doc["baseUrl"] | ""; - families_.clear(); - fontInstaller_.refreshRegistry(); + int version = doc["version"] | 0; + if (version != FONTS_MANIFEST_VERSION) { + LOG_ERR("FONT", "Unsupported manifest version: %d", version); + errorMessage_ = "Unsupported manifest version"; + return false; + } - JsonArray familiesArr = doc["families"].as(); - families_.reserve(familiesArr.size()); + baseUrl_ = doc["baseUrl"] | ""; + families_.clear(); - for (JsonObject fObj : familiesArr) { - ManifestFamily family; - family.name = fObj["name"] | ""; - family.description = fObj["description"] | ""; - family.languages = fObj["languages"] | ""; + JsonArray familiesArr = doc["families"].as(); + families_.reserve(familiesArr.size()); - for (JsonVariant s : fObj["styles"].as()) { - family.styles.push_back(s.as()); - } + for (JsonObject fObj : familiesArr) { + ManifestFamily family; + family.name = fObj["name"] | ""; + family.description = fObj["description"] | ""; + family.languages = fObj["languages"] | ""; - family.totalSize = 0; - for (JsonObject fileObj : fObj["files"].as()) { - ManifestFile file; - file.name = fileObj["name"] | ""; - file.size = fileObj["size"] | 0; - if (!parseManifestPointSize(file.name.c_str(), file.pointSize)) { - LOG_ERR("FONT", "Malformed manifest file entry: invalid filename %s", file.name.c_str()); - errorMessage_ = "Invalid font manifest"; - return false; + for (JsonVariant s : fObj["styles"].as()) { + family.styles.push_back(s.as()); } - if (!CrossPointSettings::isSdFontPointSizeAllowedForRange(file.pointSize, SETTINGS.sdFontSizeRange)) { - continue; - } + family.totalSize = 0; + for (JsonObject fileObj : fObj["files"].as()) { + ManifestFile file; + file.name = fileObj["name"] | ""; + file.size = fileObj["size"] | 0; + if (!parseManifestPointSize(file.name.c_str(), file.pointSize)) { + LOG_ERR("FONT", "Malformed manifest file entry: invalid filename %s", file.name.c_str()); + errorMessage_ = "Invalid font manifest"; + return false; + } + + if (!CrossPointSettings::isSdFontPointSizeAllowedForRange(file.pointSize, SETTINGS.sdFontSizeRange)) { + continue; + } + + if (!fileObj["crc32"].is()) { + LOG_ERR("FONT", "Malformed manifest file entry: missing or invalid crc32 for %s", file.name.c_str()); + errorMessage_ = "Invalid font manifest"; + return false; + } + file.crc32 = fileObj["crc32"].as(); - if (!fileObj["crc32"].is()) { - LOG_ERR("FONT", "Malformed manifest file entry: missing or invalid crc32 for %s", file.name.c_str()); - errorMessage_ = "Invalid font manifest"; - return false; + family.totalSize += file.size; + family.files.push_back(std::move(file)); } - file.crc32 = fileObj["crc32"].as(); - family.totalSize += file.size; - family.files.push_back(std::move(file)); - } + if (family.files.empty()) { + continue; + } - if (family.files.empty()) { - continue; + families_.push_back(std::move(family)); } + } // JsonDocument freed here, before the registry is loaded below. + // Second pass: load the installed-font registry and resolve installed/update + // state now that the manifest JsonDocument has been released, keeping peak + // heap usage down on devices with many SD fonts installed. + fontInstaller_.refreshRegistry(); + for (auto& family : families_) { resolveInstalledFamilyName(family); - - families_.push_back(std::move(family)); } LOG_DBG("FONT", "Manifest loaded: %zu families", families_.size());