From d5a4104684011bc3932c1f1b1049bcf2930acbfe Mon Sep 17 00:00:00 2001 From: JoshuaVlantis Date: Sat, 9 May 2026 14:07:13 +0200 Subject: [PATCH 1/2] fix(linux): no-op autoUpdater on Linux to defend against feed activation (#567) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap require('electron').autoUpdater on Linux with a Proxy that no-ops every method and returns chainable stubs for EventEmitter calls. The bundled app's lii() gate sets a feed URL of api.anthropic.com/api/desktop/linux/ when app.isPackaged is true (which we set unconditionally via ELECTRON_FORCE_IS_PACKAGED), and registers update-check listeners. Today this is a happy accident: Electron's Linux autoUpdater is unimplemented and the calls log "AutoUpdater is not supported on Linux" and no-op. If a future Electron implements it, every install would start hitting that feed and would either 404 or — worse — receive content the install wasn't prepared for. .deb/.rpm/AppImage updates flow through the OS package manager (or AppImageUpdate); the Anthropic feed has no Linux artifacts. The Proxy is installed via the existing Module.prototype.require interceptor, so it covers gA = require("electron"); gA.autoUpdater.X() and any equivalent destructure. getFeedURL returns '' so any code that inspects the URL gets a well-typed empty string. Verified: built deb in ubuntu:24.04 container, extracted shipped app.asar, confirmed autoUpdaterNoop block + intercept present in frame-fix-wrapper.js. Runtime test loaded the shipped wrapper with a mock electron whose autoUpdater records every real call; replayed the bundled app's setFeedURL/on/checkForUpdates/quitAndInstall/ chained .on() patterns — zero real calls were recorded, confirming the Proxy intercepts every access path. --- scripts/frame-fix-wrapper.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/scripts/frame-fix-wrapper.js b/scripts/frame-fix-wrapper.js index e53f8ad..35ed752 100644 --- a/scripts/frame-fix-wrapper.js +++ b/scripts/frame-fix-wrapper.js @@ -117,6 +117,19 @@ const LINUX_CSS = ` } `; +// autoUpdater no-op: every property access returns a chainable function +// so `.on(...).once(...).setFeedURL(...).checkForUpdates()` is harmless. +// `getFeedURL` returns '' so any code that inspects the URL gets a +// well-typed empty string rather than undefined. Defined once and reused +// across all require('electron') calls. Linux-only; macOS/Windows still +// see the real autoUpdater. See #567. +const autoUpdaterNoop = new Proxy({}, { + get(_target, prop) { + if (prop === 'getFeedURL') return () => ''; + return function chainNoop() { return autoUpdaterNoop; }; + }, +}); + // Build the patched BrowserWindow class and Menu interceptor once, // on first require('electron'), then reuse via Proxy on every access. let PatchedBrowserWindow = null; @@ -741,6 +754,23 @@ X-GNOME-Autostart-enabled=true } }); } + if (prop === 'autoUpdater' && process.platform === 'linux') { + // Force autoUpdater into a no-op on Linux. Upstream's bundled + // app code sets a feed URL of api.anthropic.com/api/desktop/linux/... + // when app.isPackaged is true (we set ELECTRON_FORCE_IS_PACKAGED=true + // unconditionally). Today this is a happy accident: Electron's Linux + // autoUpdater is unimplemented and logs "AutoUpdater is not supported + // on Linux", so the calls no-op. If a future Electron implements it, + // every install would start hitting that feed and would either 404 + // or — worse — receive content the install wasn't prepared for. + // .deb/.rpm/AppImage updates flow through the OS package manager + // (or AppImageUpdate); the Anthropic feed has no Linux artifacts. + // We replace the entire autoUpdater object with a Proxy that + // no-ops every method and returns chainable stubs for EventEmitter + // calls so listener registration in the bundled code is harmless. + // See #567. + return autoUpdaterNoop; + } return Reflect.get(target, prop, receiver); } }); From 8796aa2c82fdf7d50871c4adbaf865a7367e3cd0 Mon Sep 17 00:00:00 2001 From: JoshuaVlantis Date: Thu, 14 May 2026 12:18:34 +0200 Subject: [PATCH 2/2] fix(linux): mask thenable / coercion traps on autoUpdater no-op Proxy The chainable-Proxy get-trap returned chainNoop for every property, including `then`. V8's thenable check calls `then(resolve, reject)` on anything with a function-typed `.then`, so `await someAutoUpdaterExpr` or `Promise.resolve(autoUpdater).then(...)` invoked chainNoop with the resolve/reject pair, got the Proxy back, and the await hung forever with no error and no Sentry breadcrumb. Verified against the actual Proxy in node:20-alpine: three await scenarios (`await proxy`, `Promise.resolve(proxy).then(...)`, `await proxy.checkForUpdates()`) all hung for the full 1500ms timeout. After this change all three resolve to the Proxy itself, and existing chain behavior (`.on().once().setFeedURL().checkForUpdates()`, `emit.bind(proxy)`) keeps working. Also masks `Symbol.toPrimitive` and `Symbol.iterator` so string coercion / `for..of` / spread fail loudly rather than feeding the Proxy back through V8's primitive- or iterator-protocol machinery. Comment block updated to note the get-trap shadows reads but lets writes land on the target. Addresses review feedback on #567. --- scripts/frame-fix-wrapper.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/scripts/frame-fix-wrapper.js b/scripts/frame-fix-wrapper.js index 35ed752..7d87d25 100644 --- a/scripts/frame-fix-wrapper.js +++ b/scripts/frame-fix-wrapper.js @@ -120,12 +120,21 @@ const LINUX_CSS = ` // autoUpdater no-op: every property access returns a chainable function // so `.on(...).once(...).setFeedURL(...).checkForUpdates()` is harmless. // `getFeedURL` returns '' so any code that inspects the URL gets a -// well-typed empty string rather than undefined. Defined once and reused -// across all require('electron') calls. Linux-only; macOS/Windows still -// see the real autoUpdater. See #567. +// well-typed empty string rather than undefined. `then`/`catch`/`finally` +// and `Symbol.toPrimitive`/`Symbol.iterator` resolve to `undefined` so the +// Proxy is not mistaken for a thenable (which would call chainNoop as +// `then(resolve, reject)` and never resolve — silent await hang) or +// asked to coerce to a primitive. Writes land on the target but are +// shadowed by the get-trap. Defined once and reused across all +// require('electron') calls. Linux-only; macOS/Windows still see the +// real autoUpdater. See #567. const autoUpdaterNoop = new Proxy({}, { get(_target, prop) { if (prop === 'getFeedURL') return () => ''; + if (prop === 'then' || prop === 'catch' || prop === 'finally' + || prop === Symbol.toPrimitive || prop === Symbol.iterator) { + return undefined; + } return function chainNoop() { return autoUpdaterNoop; }; }, });