From 43c2e41c079fccd21c689a5d10fbec3e9d3a7725 Mon Sep 17 00:00:00 2001 From: topkoa Date: Sun, 12 Jul 2026 21:24:04 -0400 Subject: [PATCH] fix(panes): a corrupt config entry must not take down the main process MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit savedFor() assumed paneWindows[paneId] was an object. It isn't necessarily: the desktop config is a JSON file on disk, hand-editable, and writable by any build. `{"camera_director": null}` passes the own-property check and then throws on `saved.bounds`. This runs in the MAIN process, at window-adoption time. A TypeError there is not a bad pane — it is the app failing. Anything that isn't a plain object now degrades to "nothing saved", which is exactly what an unreadable entry means. Same guard on `paneWindows` itself (a string or an array would have got past `?? {}`). persist() gets the same treatment, and for a sharper reason: it read the map, copied every entry forward, and wrote it back. A corrupt entry would have been faithfully preserved on every save — so a single hand-edit would keep crashing the next launch, forever. Corrupt entries are now dropped rather than carried. Signed-off-by: topkoa --- src/main/pane-hosts.ts | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/main/pane-hosts.ts b/src/main/pane-hosts.ts index dc56f60..c0fc94f 100644 --- a/src/main/pane-hosts.ts +++ b/src/main/pane-hosts.ts @@ -46,11 +46,26 @@ let getMainWindow: () => BrowserWindow | null = () => null; // ── Geometry ──────────────────────────────────────────────────────────────── +// The config file is untrusted: hand-edited, corrupt, or written by a build that +// disagrees with this one. It is read in the MAIN process, where a TypeError is not +// a bad pane — it is the app failing to start. +function savedPaneMap(): Record { + const map = getDesktopConfig().paneWindows; + if (!map || typeof map !== 'object' || Array.isArray(map)) return {}; + return map as Record; +} + function savedFor(paneId: string): SavedPaneWindow { - const saved = getDesktopConfig().paneWindows ?? {}; + const saved = savedPaneMap(); // Own-property check: a polluted or hand-edited config would otherwise hand back // a value off the prototype chain for a pane that was never saved at all. - return Object.prototype.hasOwnProperty.call(saved, paneId) ? saved[paneId] : {}; + if (!Object.prototype.hasOwnProperty.call(saved, paneId)) return {}; + const entry = saved[paneId]; + // `{"camera_director": null}` passes the own-property check and then explodes on + // `saved.bounds`. Anything that is not an object degrades to "nothing saved", + // which is exactly what an unreadable entry means. + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return {}; + return entry as SavedPaneWindow; } // A pane id reaches us from the RENDERER (it is the tail of the frame name a @@ -72,9 +87,15 @@ function persist(paneId: string, patch: SavedPaneWindow): void { // edited, corrupt, or written by an older build) cannot smuggle a prototype // into a map we then write keys onto. const all: Record = Object.create(null); - const saved = getDesktopConfig().paneWindows ?? {}; + const saved = savedPaneMap(); for (const key of Object.keys(saved)) { - if (!isUnsafePaneId(key)) all[key] = saved[key]; + if (isUnsafePaneId(key)) continue; + const entry = saved[key]; + // Don't carry a corrupt entry forward. Spreading `null` into the patch + // below would be silently fine; writing it back out would keep a value + // that crashes savedFor() on the next launch forever. + if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue; + all[key] = entry as SavedPaneWindow; } all[paneId] = { ...(all[paneId] ?? {}), ...patch }; setDesktopConfig({ paneWindows: { ...all } });