From 8d319166bdb3dc8d3499ea8b4d51a33efbd431f5 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:38:20 -0700 Subject: [PATCH] fix(wsl): publish the resolved WSL hosts gate state to child processes (kill switch follow-up) (#35543) * fix(wsl): publish the resolved WSL hosts gate state to child processes Children inherit ORCA_INTERNAL_WSL_HOSTS=1 even when a remote policy block closed the gate, and none inherit serve-only. Main now publishes its resolved state as ORCA_WSL_HOSTS_GATE_STATE at startup, and the shared serving check in a child trusts it over the internal gate env. A process that resolves the gate itself ignores an inherited value, so a relaunch never carries a stale state. ORCA_INTERNAL_WSL_HOSTS is left intact so a relaunched or updated Orca keeps fetching the policy. Co-Authored-By: Claude * fix(wsl): main ignores an inherited WSL hosts gate state from its first line A relaunch or the updater's installer inherits the parent's published ORCA_WSL_HOSTS_GATE_STATE. Main's entry now marks the gate as its own to resolve before preflight, so even a read before configure falls back to the internal gate env, and configure always overwrites the value. Tests pin that a stale inherited closed or open is ignored and overwritten. Co-Authored-By: Claude --------- Co-authored-by: Claude --- src/main/index.ts | 3 ++ src/main/wsl/wsl-host-gate.test.ts | 50 ++++++++++++++++++++++- src/main/wsl/wsl-host-gate.ts | 22 +++++----- src/shared/wsl-host-internal-gate.test.ts | 43 +++++++++++++++++++ src/shared/wsl-host-internal-gate.ts | 36 ++++++++++++++++ 5 files changed, 143 insertions(+), 11 deletions(-) create mode 100644 src/shared/wsl-host-internal-gate.test.ts diff --git a/src/main/index.ts b/src/main/index.ts index 865fd320ad4..75e19cffca9 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -37,6 +37,7 @@ import { leaseAppImageGenerationForCurrentProcess } from './cli/appimage-generat import { wakeRememberedWslHosts } from './wsl/wsl-host-early-wake' import { wslHostsServing } from './wsl/wsl-host-gate' import { configureWslHostsGateFromDisk } from './wsl/wsl-host-gate-startup' +import { ignoreInheritedWslHostsGateState } from '../shared/wsl-host-internal-gate' function openMainWindow(options: { revealOnDidFinishLoad?: boolean } = {}): BrowserWindow { return openMainWindowController(options) @@ -101,6 +102,8 @@ const handleMacAppActivation = createMacAppActivationHandler({ requestActivation: requestDesktopActivation }) +// A relaunch or installer inherits the parent's published WSL gate state; this process recomputes. +ignoreInheritedWslHostsGateState() // The uninstaller's holder release runs instead of the app, never alongside it. const preflightReady = !runWslHolderUninstallPreflight() && diff --git a/src/main/wsl/wsl-host-gate.test.ts b/src/main/wsl/wsl-host-gate.test.ts index e0999cb2586..995982c17ad 100644 --- a/src/main/wsl/wsl-host-gate.test.ts +++ b/src/main/wsl/wsl-host-gate.test.ts @@ -12,7 +12,11 @@ import { wslHostsWindowArguments, WSL_HOSTS_GATED_OFF_MESSAGE } from './wsl-host-gate' -import { isWslHostsServing } from '../../shared/wsl-host-internal-gate' +import { + ignoreInheritedWslHostsGateState, + isWslHostsServing, + WSL_HOSTS_GATE_STATE_ENV +} from '../../shared/wsl-host-internal-gate' const wslHost = { orcadDeployment: { kind: 'wsl' as const, distro: 'Ubuntu', localPort: 41000, remotePort: 6768 } @@ -161,4 +165,48 @@ describe('WSL hosts gate', () => { expect(() => assertWslHostReachable(sshHost)).not.toThrow() }) }) + + describe('publishing the resolved state to child processes', () => { + it.each([ + ['open', '1', facts(false, false), undefined], + ['serve-only', undefined, facts(true, false), undefined], + ['closed', undefined, facts(false, false), undefined], + ['closed', '1', facts(false, false), 'block' as const], + ['serve-only', '1', facts(false, true), 'block' as const] + ])('publishes %s', (state, gateEnv, saved, policy) => { + vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', gateEnv) + configureWslHostsGate({ ...saved, ...(policy ? { policy: () => policy } : {}) }) + + expect(process.env[WSL_HOSTS_GATE_STATE_ENV]).toBe(state) + }) + + // A relaunch or the updater's installer inherits the parent's value; main must recompute. + it.each([ + ['closed', '1', 'open', true], + ['open', undefined, 'closed', false] + ])( + 'ignores a stale inherited %s and overwrites it', + (inherited, gateEnv, recomputed, serving) => { + vi.stubEnv(WSL_HOSTS_GATE_STATE_ENV, inherited) + vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', gateEnv) + ignoreInheritedWslHostsGateState() + expect(isWslHostsServing(process.env)).toBe(gateEnv === '1') + + configureWslHostsGate(facts(false, false)) + + expect(wslHostsGateState()).toBe(recomputed) + expect(process.env[WSL_HOSTS_GATE_STATE_ENV]).toBe(recomputed) + expect(wslHostsServing()).toBe(serving) + expect(isWslHostsServing(process.env)).toBe(serving) + } + ) + + it('withdraws it on reset', () => { + vi.stubEnv('ORCA_INTERNAL_WSL_HOSTS', '1') + configureWslHostsGate(facts(false, false)) + resetWslHostsGateForTests() + + expect(process.env[WSL_HOSTS_GATE_STATE_ENV]).toBeUndefined() + }) + }) }) diff --git a/src/main/wsl/wsl-host-gate.ts b/src/main/wsl/wsl-host-gate.ts index 485fb344af0..33b8f61a33f 100644 --- a/src/main/wsl/wsl-host-gate.ts +++ b/src/main/wsl/wsl-host-gate.ts @@ -4,6 +4,7 @@ import { isWslHostsInternalGateOpen, noteWslHostsPolicyBlocked, noteWslHostsServeOnly, + publishWslHostsGateState, resolveWslHostsGateState, type WslHostsGateState } from '../../shared/wsl-host-internal-gate' @@ -58,22 +59,23 @@ export function configureWslHostsGate(facts: { policy?: () => WslHostsPolicyState }): void { notePolicy(readPolicy(facts.policy)) - if (wslHostsGateOpen()) { - promoted = null - noteWslHostsServeOnly(false) - return - } - promoted = { - hasSavedWslHost: readFact(facts.hasSavedWslHost), - hasWslMoveCutover: readFact(facts.hasWslMoveCutover) - } - noteWslHostsServeOnly(wslHostsGateState() === 'serve-only') + promoted = wslHostsGateOpen() + ? null + : { + hasSavedWslHost: readFact(facts.hasSavedWslHost), + hasWslMoveCutover: readFact(facts.hasWslMoveCutover) + } + const state = wslHostsGateState() + noteWslHostsServeOnly(state === 'serve-only') + // Children inherit ORCA_INTERNAL_WSL_HOSTS even when a block closed it; this tells them the truth. + publishWslHostsGateState(process.env, state) } export function resetWslHostsGateForTests(): void { promoted = null noteWslHostsServeOnly(false) notePolicy(null) + publishWslHostsGateState(process.env, null) } export function wslHostsGateState(): WslHostsGateState { diff --git a/src/shared/wsl-host-internal-gate.test.ts b/src/shared/wsl-host-internal-gate.test.ts new file mode 100644 index 00000000000..6b1ea03b222 --- /dev/null +++ b/src/shared/wsl-host-internal-gate.test.ts @@ -0,0 +1,43 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { + ignoreInheritedWslHostsGateState, + isWslHostsServing, + publishWslHostsGateState, + WSL_HOSTS_GATE_STATE_ENV +} from './wsl-host-internal-gate' + +describe('WSL hosts gate state inherited from main', () => { + afterEach(() => { + publishWslHostsGateState({}, null) + }) + + // A child of a blocked run inherits ORCA_INTERNAL_WSL_HOSTS=1 but must not serve WSL state. + it('lets a child trust the state its parent published over the internal gate env', () => { + const gate = { ORCA_INTERNAL_WSL_HOSTS: '1' } + expect(isWslHostsServing({ ...gate, [WSL_HOSTS_GATE_STATE_ENV]: 'closed' })).toBe(false) + expect(isWslHostsServing({ ...gate, [WSL_HOSTS_GATE_STATE_ENV]: 'open' })).toBe(true) + expect(isWslHostsServing({ [WSL_HOSTS_GATE_STATE_ENV]: 'serve-only' })).toBe(true) + }) + + it('falls back to the internal gate env when nothing valid was published', () => { + expect(isWslHostsServing({ ORCA_INTERNAL_WSL_HOSTS: '1' })).toBe(true) + expect(isWslHostsServing({ [WSL_HOSTS_GATE_STATE_ENV]: 'bogus' })).toBe(false) + }) + + it('ignores an inherited state once this process resolved the gate itself', () => { + const env: Record = { [WSL_HOSTS_GATE_STATE_ENV]: 'open' } + publishWslHostsGateState(env, 'closed') + + expect(env[WSL_HOSTS_GATE_STATE_ENV]).toBe('closed') + expect(isWslHostsServing({ [WSL_HOSTS_GATE_STATE_ENV]: 'open' })).toBe(false) + }) + + it("ignores an inherited state from main's entry on, before the gate is configured", () => { + ignoreInheritedWslHostsGateState() + + expect(isWslHostsServing({ [WSL_HOSTS_GATE_STATE_ENV]: 'open' })).toBe(false) + expect( + isWslHostsServing({ ORCA_INTERNAL_WSL_HOSTS: '1', [WSL_HOSTS_GATE_STATE_ENV]: 'closed' }) + ).toBe(true) + }) +}) diff --git a/src/shared/wsl-host-internal-gate.ts b/src/shared/wsl-host-internal-gate.ts index d571b005126..d2e2ae9c18c 100644 --- a/src/shared/wsl-host-internal-gate.ts +++ b/src/shared/wsl-host-internal-gate.ts @@ -37,8 +37,44 @@ export function noteWslHostsPolicyBlocked(blocked: boolean): void { policyBlockNoted = blocked } +/** + * Main publishes its resolved state here so the processes it spawns (CLI, servers, terminals) read + * the gate as main does: they inherit the internal gate env var but neither note above. + */ +export const WSL_HOSTS_GATE_STATE_ENV = 'ORCA_WSL_HOSTS_GATE_STATE' +// True once this process resolved the gate itself; an inherited value is then a parent's, so stale. +let resolvedHere = false + +export function publishWslHostsGateState( + env: Record, + state: WslHostsGateState | null +): void { + resolvedHere = state !== null + if (state === null) { + delete env[WSL_HOSTS_GATE_STATE_ENV] + } else { + env[WSL_HOSTS_GATE_STATE_ENV] = state + } +} + +/** Main's entry, before anything reads the gate: it recomputes its own, so an inherited value is stale. */ +export function ignoreInheritedWslHostsGateState(): void { + resolvedHere = true +} + +function inheritedGateState( + env: Readonly> +): WslHostsGateState | null { + const value = env[WSL_HOSTS_GATE_STATE_ENV] + return value === 'open' || value === 'serve-only' || value === 'closed' ? value : null +} + /** Open, or main found saved WSL work behind the closed gate. */ export function isWslHostsServing(env: Readonly>): boolean { + const inherited = resolvedHere ? null : inheritedGateState(env) + if (inherited) { + return inherited !== 'closed' + } return (isWslHostsInternalGateOpen(env) && !policyBlockNoted) || serveOnlyNoted }