mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
fix(windows): make the pre-window ACL gate act on the poison evidence it fired on
The gate fired on a poison marker — an earlier launch's DACL reading that nothing has retired — but withheld `probeConfirmedPoisoned` from the repair, so a repair marker recording an older success still short-circuited it. On the three-launch shape the gate exists for (repair succeeds; tree is re-poisoned; the next launch's probe records the poison but dies before writing its repair marker) the gate ran no icacls, deleted the poison marker that arms every later gate, un-suspected the tree so --in-process-gpu could engage, and told the user "Orca repaired the permissions." `applyInstallDirAclProbeVerdict` then swallowed that launch's own reading behind `if (poison) return`. Both callers of `startRepair` hold outstanding poison evidence, so the flag is now unconditional (renamed `poisonEvidenceOutstanding`) and `marker-hit` means only that the attempt budget is spent. The probe guard is narrowed to an in-flight gate repair: a reading taken after the gate finished re-arms the poison marker and downgrades a claimed repair. Also: withholding safe graphics now ends with the repair budget. A machine whose attempts are spent while the signature persists was denied safe graphics on every launch for the life of that appVersion — and had its marker deleted each time — including the healthy installs the probe's flag-blind ACE match over-matches, where the driver really is broken. Non-blocking, same lane: re-read `isQuitting` after the up-to-15s verdict wait, and skip the recovered-launch prompt when the ACL gate retired the marker read before whenReady.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { mkdtempSync } from 'node:fs'
|
||||
import { mkdtempSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -38,20 +38,30 @@ import {
|
||||
DEFAULT_GPU_CRASH_FALLBACK_WINDOW_MS,
|
||||
GpuCrashFallbackTracker
|
||||
} from '../crash-reporting/gpu-crash-fallback-decision'
|
||||
import { readGpuFallbackMarker } from './gpu-fallback-marker'
|
||||
import { handleGpuChildCrash } from './gpu-lifecycle'
|
||||
import { mainProcessState as state } from './main-process-state'
|
||||
import {
|
||||
readGpuFallbackMarker,
|
||||
writeGpuFallbackMarker,
|
||||
type GpuFallbackMarker
|
||||
} from './gpu-fallback-marker'
|
||||
import { handleGpuChildCrash, presentGpuFallbackRecoveredLaunchPrompt } from './gpu-lifecycle'
|
||||
import { gpuFallbackEnvironment, mainProcessState as state } from './main-process-state'
|
||||
import { writeInstallDirAclPoisonMarker } from './windows-install-dir-acl-poison-marker'
|
||||
import {
|
||||
isInstallDirAclRepairPending,
|
||||
noteWindowsInstallDirAclProbePending,
|
||||
repairKnownPoisonedInstallDirBeforeWindow,
|
||||
resetWindowsInstallDirAclRecoveryForTest,
|
||||
startWindowsInstallDirAclRepairIfPoisoned
|
||||
} from './windows-install-dir-acl-recovery'
|
||||
import { resetWindowsInstallDirAclRepairForTest } from './windows-install-dir-package-acl-repair'
|
||||
import {
|
||||
resetWindowsInstallDirAclRepairForTest,
|
||||
WINDOWS_INSTALL_DIR_ACL_REPAIR_MARKER_FILE,
|
||||
WINDOWS_INSTALL_DIR_ACL_REPAIR_SCHEME_VERSION
|
||||
} from './windows-install-dir-package-acl-repair'
|
||||
|
||||
const INSTALL_DIR = 'C:\\Users\\neil\\AppData\\Local\\Programs\\orca'
|
||||
|
||||
function recoveryOptions(): {
|
||||
function recoveryOptions(userDataPath?: string): {
|
||||
platform: 'win32'
|
||||
installDir: string
|
||||
appVersion: string
|
||||
@@ -62,7 +72,7 @@ function recoveryOptions(): {
|
||||
platform: 'win32',
|
||||
installDir: INSTALL_DIR,
|
||||
appVersion: '1.4.184',
|
||||
userDataPath: mkdtempSync(join(tmpdir(), 'orca-acl-gpu-guard-')),
|
||||
userDataPath: userDataPath ?? mkdtempSync(join(tmpdir(), 'orca-acl-gpu-guard-')),
|
||||
recordBreadcrumb: () => undefined
|
||||
}
|
||||
}
|
||||
@@ -100,11 +110,14 @@ function reportProbePoisoned(): { finishRepair: () => Promise<void> } {
|
||||
}
|
||||
|
||||
/** A repair that settles, so `poison.stage` leaves 'pending' for a terminal verdict. */
|
||||
async function reportProbePoisonedWithSettledRepair(exitCode: number): Promise<void> {
|
||||
async function reportProbePoisonedWithSettledRepair(
|
||||
exitCode: number,
|
||||
userDataPath?: string
|
||||
): Promise<void> {
|
||||
startWindowsInstallDirAclRepairIfPoisoned(
|
||||
{ status: 'ok', matchesPoisonSignature: true, wellKnownNameCheckReliable: true },
|
||||
{
|
||||
...recoveryOptions(),
|
||||
...recoveryOptions(userDataPath),
|
||||
runProcessFn: (async () => ({
|
||||
code: exitCode,
|
||||
signal: null,
|
||||
@@ -119,6 +132,33 @@ async function reportProbePoisonedWithSettledRepair(exitCode: number): Promise<v
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The pre-window gate meeting a spent repair budget: the tree is still marked poisoned and
|
||||
* Orca has no repair left to try. icacls must never be reached, so the runner throws.
|
||||
*/
|
||||
async function gateFindsRepairBudgetSpent(): Promise<void> {
|
||||
const options = recoveryOptions()
|
||||
writeFileSync(
|
||||
join(options.userDataPath, WINDOWS_INSTALL_DIR_ACL_REPAIR_MARKER_FILE),
|
||||
JSON.stringify({
|
||||
schemeVersion: WINDOWS_INSTALL_DIR_ACL_REPAIR_SCHEME_VERSION,
|
||||
installDir: INSTALL_DIR,
|
||||
appVersion: options.appVersion,
|
||||
attemptedAt: Date.now(),
|
||||
outcome: 'failed',
|
||||
attempts: 3
|
||||
})
|
||||
)
|
||||
writeInstallDirAclPoisonMarker(options.userDataPath, INSTALL_DIR, options.appVersion)
|
||||
const mode = await repairKnownPoisonedInstallDirBeforeWindow({
|
||||
...options,
|
||||
runProcessFn: (() => {
|
||||
throw new Error('the spent budget must not spawn icacls')
|
||||
}) as never
|
||||
})
|
||||
expect(mode).toBe('marker-hit')
|
||||
}
|
||||
|
||||
function reportProbeClean(): void {
|
||||
startWindowsInstallDirAclRepairIfPoisoned(
|
||||
{ status: 'ok', matchesPoisonSignature: false },
|
||||
@@ -246,6 +286,50 @@ describe('handleGpuChildCrash vs the install-dir ACL verdict', () => {
|
||||
expect(readGpuFallbackMarker(userData.path)).toBeNull()
|
||||
})
|
||||
|
||||
// The verdict wait can span the probe's whole 15s grace window, and the entry guard was
|
||||
// read before it. A quit that starts inside the wait must not be answered with a modal.
|
||||
it('does not prompt when the user quits during the verdict wait', async () => {
|
||||
noteWindowsInstallDirAclProbePending()
|
||||
await crashUpToThreshold()
|
||||
const decisive = handleGpuChildCrash('crashed', null, 600)
|
||||
state.isQuitting = true
|
||||
reportProbeClean()
|
||||
await decisive
|
||||
expect(showMessageBox).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Withholding is a bounded delay, not a permanent suppression. Once the repair budget is
|
||||
// spent no repair is coming on this launch or any later one, so pinning the tree as the
|
||||
// suspect forever denied safe graphics on EVERY launch for the life of that version — and
|
||||
// deleted the marker each time, so the machine also relaunched hardware accelerated. The
|
||||
// victims are a standard-user install icacls can never fix and, via the probe's flag-blind
|
||||
// ACE match, healthy installs whose driver genuinely is broken.
|
||||
it('offers safe graphics on every launch once the ACL repair budget is spent', async () => {
|
||||
for (let launch = 1; launch <= 3; launch += 1) {
|
||||
resetWindowsInstallDirAclRepairForTest()
|
||||
resetWindowsInstallDirAclRecoveryForTest()
|
||||
showMessageBox.mockClear()
|
||||
state.gpuCrashFallbackTracker = new GpuCrashFallbackTracker({
|
||||
windowMs: DEFAULT_GPU_CRASH_FALLBACK_WINDOW_MS,
|
||||
threshold: DEFAULT_GPU_CRASH_FALLBACK_THRESHOLD
|
||||
})
|
||||
await gateFindsRepairBudgetSpent()
|
||||
|
||||
await crashUpToThreshold()
|
||||
await handleGpuChildCrash('crashed', null, 600)
|
||||
expect(showMessageBox).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
})
|
||||
|
||||
// Still withheld while the budget has an attempt left: the repair is the better answer,
|
||||
// and this is the launch a next one can be rescued on.
|
||||
it('still withholds while the repair has an attempt left to spend', async () => {
|
||||
await reportProbePoisonedWithSettledRepair(1)
|
||||
await crashUpToThreshold()
|
||||
await handleGpuChildCrash('crashed', null, 600)
|
||||
expect(showMessageBox).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// recordGpuCrash reports the threshold crossing once and latches. Withholding consumes
|
||||
// that one report, so without a re-arm the same process could never engage again — a
|
||||
// machine whose tree is repaired and whose driver is genuinely broken would be stuck
|
||||
@@ -266,3 +350,49 @@ describe('handleGpuChildCrash vs the install-dir ACL verdict', () => {
|
||||
expect(showMessageBox).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// The safe-graphics marker is read before whenReady, and the pre-window ACL gate runs after
|
||||
// that read. Asking "keep safe graphics?" on a machine Orca has just repaired invites a
|
||||
// `userConfirmed: true` marker that pins software rendering on healthy hardware.
|
||||
describe('presentGpuFallbackRecoveredLaunchPrompt vs a marker retired since it was read', () => {
|
||||
const realPlatform = process.platform
|
||||
const window = { isDestroyed: () => false } as unknown as Parameters<
|
||||
typeof presentGpuFallbackRecoveredLaunchPrompt
|
||||
>[0]
|
||||
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true })
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
Object.defineProperty(process, 'platform', { value: realPlatform, configurable: true })
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
userData.path = mkdtempSync(join(tmpdir(), 'orca-acl-gpu-recovered-'))
|
||||
resetWindowsInstallDirAclRepairForTest()
|
||||
resetWindowsInstallDirAclRecoveryForTest()
|
||||
showMessageBox.mockClear()
|
||||
state.isQuitting = false
|
||||
const info = { engagedAt: Date.now(), crashesInWindow: 3, userConfirmed: false }
|
||||
writeGpuFallbackMarker(userData.path, info, {
|
||||
...gpuFallbackEnvironment(),
|
||||
platform: 'win32'
|
||||
})
|
||||
state.activeGpuFallbackMarker = readGpuFallbackMarker(userData.path) as GpuFallbackMarker
|
||||
})
|
||||
|
||||
it('asks while the marker is still on disk', async () => {
|
||||
showMessageBox.mockResolvedValueOnce({ response: 0 })
|
||||
await presentGpuFallbackRecoveredLaunchPrompt(window)
|
||||
expect(showMessageBox).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('stays silent once the install-DACL repair has cleared it', async () => {
|
||||
await reportProbePoisonedWithSettledRepair(0, userData.path)
|
||||
expect(readGpuFallbackMarker(userData.path)).toBeNull()
|
||||
|
||||
await presentGpuFallbackRecoveredLaunchPrompt(window)
|
||||
expect(showMessageBox).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,6 +17,7 @@ import { engageGpuFallbackAfterCrashBurst } from '../crash-reporting/gpu-fallbac
|
||||
import { recordCrashBreadcrumb } from '../crash-reporting/crash-breadcrumb-store'
|
||||
import { recordDurableCrashBreadcrumb } from '../crash-reporting/durable-crash-breadcrumb'
|
||||
import {
|
||||
isInstallDirAclRepairExhausted,
|
||||
isInstallDirAclRepairPending,
|
||||
isInstallDirAclSuspect,
|
||||
waitForInstallDirAclVerdict
|
||||
@@ -90,6 +91,11 @@ export async function presentGpuFallbackRecoveredLaunchPrompt(
|
||||
// One prompt per process. A failure leaves the on-disk marker unconfirmed so the next launch retries.
|
||||
state.activeGpuFallbackMarker = null
|
||||
const userDataPath = app.getPath('userData')
|
||||
// The marker was read before whenReady; the pre-window ACL gate can have retired it since.
|
||||
// Asking then would let a "keep it" answer pin software rendering on a machine Orca just fixed.
|
||||
if (!readActiveGpuFallbackMarker(userDataPath, gpuFallbackEnvironment())) {
|
||||
return
|
||||
}
|
||||
await handleGpuFallbackRecoveredLaunch({
|
||||
isQuitting: () => state.isQuitting,
|
||||
prompt: () => promptForGpuFallbackRecoveredLaunch(window),
|
||||
@@ -119,6 +125,18 @@ export async function presentGpuFallbackRecoveredLaunchPrompt(
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Why withholding ends with the repair budget: withholding only buys the ACL repair the
|
||||
* chance to land first. Once its attempts are spent no repair is coming on this launch or
|
||||
* any later one, so holding safe graphics back forever would deny the only recovery left —
|
||||
* on a genuinely poisoned tree Orca has already told the user the admin commands, and the
|
||||
* probe's flag-blind ACE match also over-matches healthy installs whose driver really is
|
||||
* the fault. It is a bounded delay, not a permanent suppression.
|
||||
*/
|
||||
function installDirAclWithholdsGpuFallback(): boolean {
|
||||
return isInstallDirAclSuspect() && !isInstallDirAclRepairExhausted()
|
||||
}
|
||||
|
||||
/**
|
||||
* Why: a poisoned install DACL kills the GPU child exactly like a bad driver, but safe
|
||||
* graphics does not rescue it and --in-process-gpu removes the GPU child, erasing the
|
||||
@@ -133,7 +151,7 @@ async function installDirAclClearsGpuFallback(
|
||||
userDataPath: string,
|
||||
crashesInWindow: number
|
||||
): Promise<boolean> {
|
||||
if (!isInstallDirAclSuspect()) {
|
||||
if (!installDirAclWithholdsGpuFallback()) {
|
||||
return true
|
||||
}
|
||||
const persisted = persistGpuFallbackMarker(userDataPath, {
|
||||
@@ -142,7 +160,7 @@ async function installDirAclClearsGpuFallback(
|
||||
userConfirmed: false
|
||||
})
|
||||
await waitForInstallDirAclVerdict()
|
||||
if (!isInstallDirAclSuspect()) {
|
||||
if (!installDirAclWithholdsGpuFallback()) {
|
||||
return true
|
||||
}
|
||||
// Why the marker survives a pending repair: withdrawing it here left a machine that
|
||||
@@ -189,6 +207,11 @@ export async function handleGpuChildCrash(
|
||||
if (!(await installDirAclClearsGpuFallback(userDataPath, result.crashesInWindow))) {
|
||||
return
|
||||
}
|
||||
// Re-read after that wait: it can span the probe's whole grace window, and a quit that
|
||||
// started inside it must not be answered with a modal and a relaunch.
|
||||
if (state.isQuitting) {
|
||||
return
|
||||
}
|
||||
await engageGpuFallbackAfterCrashBurst(
|
||||
{ reason, exitCode, crashesInWindow: result.crashesInWindow, engagedAt: Date.now() },
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import {
|
||||
describeInstallDirAclPoison,
|
||||
isBlockingInstallDirAclRepairInFlight,
|
||||
isInstallDirAclRepairExhausted,
|
||||
isInstallDirAclSuspect,
|
||||
noteWindowsInstallDirAclProbePending,
|
||||
repairKnownPoisonedInstallDirBeforeWindow,
|
||||
@@ -475,39 +476,115 @@ describe('repairKnownPoisonedInstallDirBeforeWindow', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// hasMarkerFor also matches a marker written by a SUCCESSFUL repair, so 'marker-hit'
|
||||
// on its own cannot tell a finished tree from one Orca gave up on.
|
||||
describe('a marker-hit on a tree a previous launch already repaired', () => {
|
||||
// The repair marker matches whatever the outcome, so on its own 'marker-hit' cannot tell a
|
||||
// finished tree from one Orca gave up on. Both callers hold outstanding poison evidence —
|
||||
// this launch's probe reading, or the persisted marker that armed the gate — so a recorded
|
||||
// success never stands in for the repair, and 'marker-hit' only ever means budget spent.
|
||||
describe('a repair marker recording a completed repair', () => {
|
||||
beforeEach(() => {
|
||||
resetWindowsInstallDirAclProbeForTest()
|
||||
resetWindowsInstallDirAclRepairForTest()
|
||||
resetWindowsInstallDirAclRecoveryForTest()
|
||||
})
|
||||
|
||||
it('reads as repaired, not as a repair Orca could not do', async () => {
|
||||
/** One launch: fresh module latches, then the gate runs against the userData on disk. */
|
||||
async function gateLaunch(
|
||||
userDataPath: string,
|
||||
run: Runner
|
||||
): Promise<Awaited<ReturnType<typeof repairKnownPoisonedInstallDirBeforeWindow>>> {
|
||||
resetWindowsInstallDirAclProbeForTest()
|
||||
resetWindowsInstallDirAclRepairForTest()
|
||||
resetWindowsInstallDirAclRecoveryForTest()
|
||||
return repairKnownPoisonedInstallDirBeforeWindow(recoveryOptions(userDataPath, run))
|
||||
}
|
||||
|
||||
// The three-launch shape the gate exists for, and the one it used to disarm itself on:
|
||||
// launch 1 repairs; the tree is re-poisoned (an installer, AV, or an icacls run that
|
||||
// silently no-opped); launch 2's probe records the poison but Chromium FATALs before the
|
||||
// repair can write its marker. Launch 3's gate then meets a poison marker and a repair
|
||||
// marker claiming success. Treating that as 'repaired' ran no icacls, deleted the poison
|
||||
// marker so no later gate ever fires again, un-suspected the tree so --in-process-gpu
|
||||
// could engage, and told the user their permissions were fixed.
|
||||
it('re-runs icacls when a poison marker outlives it', async () => {
|
||||
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-acl-repaired-hit-'))
|
||||
|
||||
// Launch 1: the gate repairs the tree and retires the poison marker.
|
||||
writeInstallDirAclPoisonMarker(userDataPath, INSTALL_DIR, APP_VERSION)
|
||||
expect(await gateLaunch(userDataPath, okRun)).toBe('repaired')
|
||||
expect(hasInstallDirAclPoisonMarker(userDataPath, INSTALL_DIR, APP_VERSION)).toBe(false)
|
||||
|
||||
// Launch 2: the probe reads the tree as poisoned again; the process dies mid-repair,
|
||||
// so the repair marker still records launch 1's success.
|
||||
resetWindowsInstallDirAclRecoveryForTest()
|
||||
resetWindowsInstallDirAclRepairForTest()
|
||||
startWindowsInstallDirAclRepairIfPoisoned(
|
||||
POISON_VERDICT,
|
||||
recoveryOptions(userDataPath, (() => new Promise<never>(() => undefined)) as Runner)
|
||||
)
|
||||
expect(hasInstallDirAclPoisonMarker(userDataPath, INSTALL_DIR, APP_VERSION)).toBe(true)
|
||||
|
||||
// Launch 3: the gate must repair, not congratulate itself on launch 1's work.
|
||||
const spent: ProcessSpec[] = []
|
||||
const mode = await gateLaunch(userDataPath, async (spec) => {
|
||||
spent.push(spec)
|
||||
return { code: 5, signal: null, stdout: '', stderr: 'Access is denied.', timedOut: false }
|
||||
})
|
||||
expect(mode).toBe('failed')
|
||||
expect(spent.map((spec) => spec.args?.[2])).toEqual([
|
||||
'*S-1-15-2-2:(OI)(CI)(RX)',
|
||||
'*S-1-15-2-2:(RX)'
|
||||
])
|
||||
expect(isInstallDirAclSuspect()).toBe(true)
|
||||
expect(describeInstallDirAclPoison()?.detail).toContain('could not repair them')
|
||||
expect(hasInstallDirAclPoisonMarker(userDataPath, INSTALL_DIR, APP_VERSION)).toBe(true)
|
||||
})
|
||||
|
||||
// The budget is what stops the retry above running forever; a spent one must still read
|
||||
// as "Orca could not fix this", never as a repair it never made.
|
||||
it('does not let the gate report a spent budget as a repair', async () => {
|
||||
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-acl-gate-budget-'))
|
||||
writeFileSync(
|
||||
join(userDataPath, WINDOWS_INSTALL_DIR_ACL_REPAIR_MARKER_FILE),
|
||||
JSON.stringify({
|
||||
schemeVersion: WINDOWS_INSTALL_DIR_ACL_REPAIR_SCHEME_VERSION,
|
||||
installDir: INSTALL_DIR,
|
||||
appVersion: APP_VERSION,
|
||||
attemptedAt: Date.now(),
|
||||
outcome: 'repaired',
|
||||
attempts: 3
|
||||
})
|
||||
)
|
||||
writeInstallDirAclPoisonMarker(userDataPath, INSTALL_DIR, APP_VERSION)
|
||||
const spent: ProcessSpec[] = []
|
||||
const mode = await gateLaunch(userDataPath, async (spec) => {
|
||||
spent.push(spec)
|
||||
return okRun(spec)
|
||||
})
|
||||
expect(mode).toBe('marker-hit')
|
||||
expect(spent).toHaveLength(0)
|
||||
expect(isInstallDirAclSuspect()).toBe(true)
|
||||
expect(isInstallDirAclRepairExhausted()).toBe(true)
|
||||
expect(describeInstallDirAclPoison()?.detail).toContain('could not repair them')
|
||||
// Still armed: nothing has proven this tree healthy, so a later launch still gates.
|
||||
expect(hasInstallDirAclPoisonMarker(userDataPath, INSTALL_DIR, APP_VERSION)).toBe(true)
|
||||
})
|
||||
|
||||
// The probe reads the tree AFTER the pre-window gate has finished with it, so a signature
|
||||
// still matching means the repair never landed however icacls exited.
|
||||
it('is overruled by a probe that reads the tree poisoned after the gate repaired it', async () => {
|
||||
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-acl-noop-icacls-'))
|
||||
writeInstallDirAclPoisonMarker(userDataPath, INSTALL_DIR, APP_VERSION)
|
||||
expect(
|
||||
await repairKnownPoisonedInstallDirBeforeWindow(recoveryOptions(userDataPath, okRun))
|
||||
).toBe('repaired')
|
||||
|
||||
// The state a launch killed between the repair and the marker clear leaves behind.
|
||||
writeInstallDirAclPoisonMarker(userDataPath, INSTALL_DIR, APP_VERSION)
|
||||
resetWindowsInstallDirAclRecoveryForTest()
|
||||
resetWindowsInstallDirAclRepairForTest()
|
||||
const spent: ProcessSpec[] = []
|
||||
expect(
|
||||
await repairKnownPoisonedInstallDirBeforeWindow(
|
||||
recoveryOptions(userDataPath, async (spec) => {
|
||||
spent.push(spec)
|
||||
return okRun(spec)
|
||||
})
|
||||
)
|
||||
).toBe('marker-hit')
|
||||
expect(spent).toHaveLength(0)
|
||||
expect(isInstallDirAclSuspect()).toBe(false)
|
||||
expect(describeInstallDirAclPoison()?.detail).toContain('Orca repaired the permissions')
|
||||
expect(hasInstallDirAclPoisonMarker(userDataPath, INSTALL_DIR, APP_VERSION)).toBe(false)
|
||||
startWindowsInstallDirAclRepairIfPoisoned(POISON_VERDICT, recoveryOptions(userDataPath, okRun))
|
||||
|
||||
expect(isInstallDirAclSuspect()).toBe(true)
|
||||
expect(isInstallDirAclRepairExhausted()).toBe(false)
|
||||
expect(describeInstallDirAclPoison()?.detail).toContain('could not repair them')
|
||||
// Re-armed: the next launch gates before it opens a window it cannot render.
|
||||
expect(hasInstallDirAclPoisonMarker(userDataPath, INSTALL_DIR, APP_VERSION)).toBe(true)
|
||||
})
|
||||
|
||||
// The opposite evidence: the probe has just READ this tree and found it poisoned, so a
|
||||
|
||||
@@ -115,6 +115,16 @@ export function isInstallDirAclRepairPending(): boolean {
|
||||
return poison?.stage === 'pending'
|
||||
}
|
||||
|
||||
/**
|
||||
* True once the repair has nothing left to try for this install and version: `marker-hit`
|
||||
* is reachable only through the spent attempt budget. The suspicion itself stands — the
|
||||
* dialog still names the cause and the admin commands — but a caller that was *withholding*
|
||||
* a recovery to give the repair first go has nothing left to wait for.
|
||||
*/
|
||||
export function isInstallDirAclRepairExhausted(): boolean {
|
||||
return poison?.stage === 'marker-hit'
|
||||
}
|
||||
|
||||
/** True while the pre-window gate is rewriting the very files a new renderer would load. */
|
||||
export function isBlockingInstallDirAclRepairInFlight(): boolean {
|
||||
return blockingRepairInFlight
|
||||
@@ -124,31 +134,24 @@ export function isBlockingInstallDirAclRepairInFlight(): boolean {
|
||||
function startRepair(
|
||||
installDir: string,
|
||||
options: WindowsInstallDirAclRecoveryOptions,
|
||||
{
|
||||
probeConfirmedPoisoned = false,
|
||||
onDone
|
||||
}: {
|
||||
probeConfirmedPoisoned?: boolean
|
||||
onDone?: (result: WindowsInstallDirAclRepairResult) => void
|
||||
} = {}
|
||||
onDone?: (result: WindowsInstallDirAclRepairResult) => void
|
||||
): boolean {
|
||||
writeInstallDirAclPoisonMarker(options.userDataPath, installDir, options.appVersion)
|
||||
const started = repairWindowsInstallDirPackageAcl({
|
||||
...options,
|
||||
installDir,
|
||||
probeConfirmedPoisoned,
|
||||
// Every caller here holds outstanding poison evidence — this launch's probe reading, or
|
||||
// the persisted marker that armed the gate — so a marker recording a completed repair
|
||||
// describes a re-poisoned tree, or an icacls run that silently no-opped. It must not
|
||||
// stand in for a repair. `marker-hit` therefore only ever means the budget is spent.
|
||||
poisonEvidenceOutstanding: true,
|
||||
onDone: (result) => {
|
||||
// A marker recording a completed repair is not a failure to repair: this tree is done.
|
||||
// `probeConfirmedPoisoned` keeps it off a tree the probe just read as poisoned:
|
||||
// there the marker stops claiming `alreadyRepaired`, so icacls runs again.
|
||||
const stage: RepairStage =
|
||||
result.mode === 'marker-hit' && result.alreadyRepaired ? 'repaired' : result.mode
|
||||
// A clean reading of the tree outranks this: there was nothing left to repair.
|
||||
if (!installDirReadClean) {
|
||||
poison = { installDir, stage }
|
||||
poison = { installDir, stage: result.mode }
|
||||
}
|
||||
logStartupMilestone('install-dir-acl-repair-done', { mode: result.mode })
|
||||
if (stage === 'repaired') {
|
||||
if (result.mode === 'repaired') {
|
||||
clearInstallDirAclPoisonMarker(options.userDataPath)
|
||||
// The GPU child deaths were never a driver fault, so safe graphics — and the
|
||||
// --in-process-gpu launch that hides the next crash's evidence — must not outlive the repair.
|
||||
@@ -200,14 +203,19 @@ function applyInstallDirAclProbeVerdict(
|
||||
}
|
||||
return
|
||||
}
|
||||
// The blocking pre-window gate may already own this launch's repair; restarting it
|
||||
// would reset the verdict to 'pending' against a repair that can no longer report.
|
||||
if (poison) {
|
||||
// The blocking pre-window gate still owns this launch's repair; restarting it would
|
||||
// reset the verdict to 'pending' against a repair that can no longer report.
|
||||
if (poison?.stage === 'pending') {
|
||||
return
|
||||
}
|
||||
startRepair(options.installDir ?? dirname(process.execPath), options, {
|
||||
probeConfirmedPoisoned: true
|
||||
})
|
||||
// This reading was taken after the gate finished, so it outranks the gate's own verdict:
|
||||
// a tree that still matches the signature was never repaired, whatever icacls exited.
|
||||
if (poison?.stage === 'repaired') {
|
||||
poison = { installDir: poison.installDir, stage: 'failed' }
|
||||
}
|
||||
// Re-writes the poison marker — re-arming the next launch's gate — even when the
|
||||
// once-per-process latch means no icacls can run again this launch.
|
||||
startRepair(options.installDir ?? dirname(process.execPath), options)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,13 +246,11 @@ export async function repairKnownPoisonedInstallDirBeforeWindow(
|
||||
options.timeoutMs ?? BLOCKING_REPAIR_BUDGET_MS
|
||||
)
|
||||
timer.unref?.()
|
||||
// No `probeConfirmedPoisoned`: the gate acts on a marker from an earlier launch,
|
||||
// not on a DACL reading of its own, so a recorded repair still outranks it.
|
||||
const started = startRepair(installDir, options, {
|
||||
onDone: (result) => {
|
||||
clearTimeout(timer)
|
||||
resolve(result.mode)
|
||||
}
|
||||
// The marker is an earlier launch's DACL reading that nothing has retired, so a
|
||||
// repair marker claiming success cannot stand in for the repair this launch owes.
|
||||
const started = startRepair(installDir, options, (result) => {
|
||||
clearTimeout(timer)
|
||||
resolve(result.mode)
|
||||
})
|
||||
// No dispatch means no `onDone`, so waiting out the whole budget would buy nothing.
|
||||
if (!started) {
|
||||
|
||||
@@ -64,11 +64,13 @@ export type WindowsInstallDirAclRepairOptions = {
|
||||
platform?: NodeJS.Platform
|
||||
isServeMode?: boolean
|
||||
/**
|
||||
* The DACL was read as poisoned just now, so a marker claiming a completed repair
|
||||
* describes a tree that has since been re-poisoned — or an icacls run that silently
|
||||
* no-opped. It stops outranking the evidence; the attempt budget still bounds retries.
|
||||
* A DACL reading found this tree poisoned and nothing has read it clean since — this
|
||||
* launch's probe, or a persisted poison marker from an earlier one. A marker claiming a
|
||||
* completed repair therefore describes a tree that has since been re-poisoned, or an
|
||||
* icacls run that silently no-opped: it stops outranking the reading. The attempt
|
||||
* budget still bounds retries.
|
||||
*/
|
||||
probeConfirmedPoisoned?: boolean
|
||||
poisonEvidenceOutstanding?: boolean
|
||||
/** Test seams. */
|
||||
runProcessFn?: typeof runProcess
|
||||
recordBreadcrumb?: typeof recordDurableCrashBreadcrumb
|
||||
@@ -146,7 +148,7 @@ function markerHitFor(args: WindowsInstallDirAclRepairArgs): { alreadyRepaired:
|
||||
if (!marker) {
|
||||
return null
|
||||
}
|
||||
if (marker.outcome === 'repaired' && args.probeConfirmedPoisoned !== true) {
|
||||
if (marker.outcome === 'repaired' && args.poisonEvidenceOutstanding !== true) {
|
||||
return { alreadyRepaired: true }
|
||||
}
|
||||
return (marker.attempts ?? 0) >= MAX_REPAIR_ATTEMPTS ? { alreadyRepaired: false } : null
|
||||
|
||||
Reference in New Issue
Block a user