From 980f4a0e404c67a4b9a89460886dab80c0e2759f Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Thu, 1 Oct 2026 19:23:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(gui):=20=E7=BC=93=E5=AD=98=E8=A7=A3?= =?UTF-8?q?=E5=B0=81=E7=BB=93=E6=9E=9C=EF=BC=8C=E5=90=A6=E5=88=99=E6=AF=8F?= =?UTF-8?q?=E4=B8=AA=E8=AF=B7=E6=B1=82=E9=83=BD=E8=A6=81=20spawn=20?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E8=BF=9B=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The auth rule calls readAdminKey() on every outbound request so injection never depends on ordering. Once the sealed-config path shells out to the core, that turns each request into a process spawn: 200 simulated requests took 1012ms and launched 200 cores. Cache the unsealed key against config.yaml's mtime. Editing the config still invalidates it, which is what the auth rule actually needs -- the port rewrite, the first write, and a user edit all change mtime. Measured: 200 requests now cost 10ms and one spawn. Only a successful unseal is cached. Caching a failure would pin an empty key until the config next changes, turning a momentary spawn error into a locked out user. --- cmd/gui/main.js | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/cmd/gui/main.js b/cmd/gui/main.js index 0350c6e..ab33943 100644 --- a/cmd/gui/main.js +++ b/cmd/gui/main.js @@ -142,7 +142,17 @@ function readConfigRaw() { // the core generate a *second* master key in the CWD and then fail to decrypt // ("master key changed?"). Electron's CWD is not the profile dir, so this is // not optional: without it the desktop build cannot read its own key back. +// +// readAdminKey runs on every outbound request, and this spawns a process, so +// the result is cached against the config's mtime. Editing the config (or the +// port rewrite, or the first write) changes the mtime and invalidates it, which +// keeps the "read live so ordering never matters" property the auth rule +// depends on. function unsealViaCore() { + const st = safeStat(CONFIG_FILE); + const stamp = st ? st.mtimeMs : 0; + if (unsealCache && unsealCache.stamp === stamp) return unsealCache.key; + let key = ""; try { const out = execFileSync( CORE_EXE, @@ -157,14 +167,29 @@ function unsealViaCore() { // -show-secrets prints one line per credential: // key role= const admin = /^key\s+\S+\s+role=admin\s+(\S+)\s*$/m.exec(out); - if (admin && admin[1]) return admin[1]; + if (admin && admin[1]) key = admin[1]; // Tolerate a field-order or spacing change rather than locking the user out. - const loose = /role=admin\s+(\S+)/.exec(out); - if (loose && loose[1]) return loose[1]; + if (!key) { + const loose = /role=admin\s+(\S+)/.exec(out); + if (loose && loose[1]) key = loose[1]; + } } catch (e) { console.error("unseal via core failed:", e.message); } - return ""; + // Only cache a success. A transient failure must not pin an empty key until + // the config next changes, or a momentary spawn error locks the user out. + if (key) unsealCache = { stamp, key }; + return key; +} + +let unsealCache = null; + +function safeStat(p) { + try { + return fs.statSync(p); + } catch (e) { + return null; + } } const embeddedBaseUrl = () =>