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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Neil
2026-10-28 19:38:20 -07:00
committed by GitHub
co-authored by Claude
parent 6037d91f4b
commit 8d319166bd
5 changed files with 143 additions and 11 deletions
+3
View File
@@ -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() &&
+49 -1
View File
@@ -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()
})
})
})
+12 -10
View File
@@ -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 {
+43
View File
@@ -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<string, string | undefined> = { [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)
})
})
+36
View File
@@ -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<string, string | undefined>,
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<Record<string, string | undefined>>
): 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<Record<string, string | undefined>>): boolean {
const inherited = resolvedHere ? null : inheritedGateState(env)
if (inherited) {
return inherited !== 'closed'
}
return (isWslHostsInternalGateOpen(env) && !policyBlockNoted) || serveOnlyNoted
}