Skip to content

Commit db56c48

Browse files
kjkclaude
andcommitted
Stop cache trimming while settings reload
ReloadSettings frees gSettings and then reads the settings file. A file over 1 MB makes that read a large allocation, which asks the caches to trim; the render cache's check for a comic book in a facing view reads gSettings->comicBookUI and crashed on the null pointer. Turn trimming off before the settings are freed. LoadSettings sets the level again from the new settings. crash: 2026-10-13-13-57-ce12 (c0000005 at DisplayModel::ShouldTreatLandscapeAsSpread, src/DisplayModel.cpp:653) prompt: /fix-crashes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 8e8e37d commit db56c48

3 files changed

Lines changed: 89 additions & 0 deletions

File tree

‎src/shared/AppSettings.cpp‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,9 @@ static void ReloadSettings(bool force = false) {
10721072
#endif
10731073

10741074
FileHistorySetStates(nullptr);
1075+
// no cache trim until LoadSettings() restores it: reading the file
1076+
// allocates and the trim's canFree callbacks read gSettings
1077+
gSaveMemory = 0;
10751078
CleanUpSettings();
10761079

10771080
ok = LoadSettings();
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Reloading a large settings file while a comic book is open crashed
2+
// (crash 2026-10-13-13-57-ce12): ReloadSettings frees gSettings, the read of a
3+
// file over 1 MB then asks the caches to trim, and the render cache's
4+
// "is this page visible nearby" check read gSettings->comicBookUI.
5+
6+
import { copyFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7+
import { join } from "node:path";
8+
import { ControlCommand } from "./control.ts";
9+
import { ROOT, pollUntil, runStandalone, SLOW_BUILD_FACTOR, tmpPath } from "./util.ts";
10+
import { killAndWait, launchControlled } from "./win-automation.ts";
11+
12+
const kTag = "reload-settings-memory-trim";
13+
// kLargeAllocationSize in Base.h: smaller reads don't trim
14+
const kLargeAllocationSize = 1024 * 1024;
15+
16+
// SaveMemory = 100 trims on every large allocation
17+
function settingsText(showToc: boolean): string {
18+
const padLine = "# " + "x".repeat(98) + "\n";
19+
const pad = padLine.repeat(Math.ceil((2 * kLargeAllocationSize) / padLine.length));
20+
return `UiLanguage = en\nCheckForUpdates = false\nRestoreSession = false\nSaveMemory = 100\nShowToc = ${showToc}\n${pad}`;
21+
}
22+
23+
function loadCount(logPath: string): number {
24+
try {
25+
return readFileSync(logPath, "utf8").split("LoadSettings(").length - 1;
26+
} catch {
27+
return 0;
28+
}
29+
}
30+
31+
function waitForLoads(logPath: string, n: number): Promise<number> {
32+
return pollUntil(
33+
async () => loadCount(logPath),
34+
(count) => count >= n,
35+
{
36+
timeoutMs: 10000 * SLOW_BUILD_FACTOR,
37+
error: (count) => `${kTag}: ${count} settings loads, want ${n}`,
38+
},
39+
);
40+
}
41+
42+
export async function testit(): Promise<void> {
43+
const appdata = tmpPath(kTag);
44+
rmSync(appdata, { recursive: true, force: true });
45+
mkdirSync(appdata, { recursive: true });
46+
const settingsPath = join(appdata, "SumatraPDF-settings.txt");
47+
const logPath = join(appdata, "log.txt");
48+
const cbz = join(appdata, "comic.cbz");
49+
copyFileSync(join(ROOT, "tests", "issue-1201.cbz"), cbz);
50+
writeFileSync(settingsPath, settingsText(false));
51+
52+
// facing: single page view never asks the comic book settings
53+
const { proc, client } = await launchControlled([
54+
"-appdata",
55+
appdata,
56+
"-log-to-file",
57+
logPath,
58+
"-view",
59+
"facing",
60+
cbz,
61+
]);
62+
try {
63+
await waitForLoads(logPath, 1);
64+
// the render cache needs a bitmap to consider freeing
65+
await client.waitForRenderIdle();
66+
67+
const before = loadCount(logPath);
68+
writeFileSync(settingsPath, settingsText(true));
69+
await waitForLoads(logPath, before + 1);
70+
71+
const ping = await client.request(ControlCommand.Ping);
72+
if (ping[0] !== "pong") {
73+
throw new Error(`${kTag}: ping after reload: ${JSON.stringify(ping)}`);
74+
}
75+
} finally {
76+
client.close();
77+
await killAndWait(proc);
78+
rmSync(appdata, { recursive: true, force: true });
79+
}
80+
}
81+
82+
if (import.meta.main) {
83+
await runStandalone(testit);
84+
}

‎tests/run-almost-all.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ import { testit as advSettingsFreshDefaults } from "./adv-settings-fresh-default
377377
import { testit as issue6137AdvSettings } from "./issue-6137-adv-settings.ts";
378378
import { testit as advSettingsHomeReload } from "./adv-settings-home-reload.ts";
379379
import { testit as advSettingsExternalReload } from "./adv-settings-external-reload.ts";
380+
import { testit as reloadSettingsMemoryTrim } from "./reload-settings-memory-trim.ts";
380381
import { testit as commandPaletteExternalReload } from "./command-palette-external-reload.ts";
381382
import { testit as tocTitleFallback } from "./toc-title-fallback.ts";
382383
import { testit as tocShowOnOpen } from "./toc-show-on-open.ts";
@@ -790,6 +791,7 @@ export const tests: NamedTest[] = [
790791
["issue-6137-adv-settings", issue6137AdvSettings],
791792
["adv-settings-home-reload", advSettingsHomeReload],
792793
["adv-settings-external-reload", advSettingsExternalReload],
794+
["reload-settings-memory-trim", reloadSettingsMemoryTrim],
793795
["command-palette-external-reload", commandPaletteExternalReload],
794796
["toc-title-fallback", tocTitleFallback],
795797
["toc-show-on-open", tocShowOnOpen],

0 commit comments

Comments
 (0)