diff --git a/src/main/startup/gpu-lifecycle-install-dir-acl-guard.test.ts b/src/main/startup/gpu-lifecycle-install-dir-acl-guard.test.ts index a2e545a4cbd..f83186b46ef 100644 --- a/src/main/startup/gpu-lifecycle-install-dir-acl-guard.test.ts +++ b/src/main/startup/gpu-lifecycle-install-dir-acl-guard.test.ts @@ -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 } { } /** A repair that settles, so `poison.stage` leaves 'pending' for a terminal verdict. */ -async function reportProbePoisonedWithSettledRepair(exitCode: number): Promise { +async function reportProbePoisonedWithSettledRepair( + exitCode: number, + userDataPath?: string +): Promise { 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 { + 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() + }) +}) diff --git a/src/main/startup/gpu-lifecycle.ts b/src/main/startup/gpu-lifecycle.ts index 5629b063f12..57856cc5654 100644 --- a/src/main/startup/gpu-lifecycle.ts +++ b/src/main/startup/gpu-lifecycle.ts @@ -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 { - 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() }, { diff --git a/src/main/startup/windows-install-dir-acl-recovery.test.ts b/src/main/startup/windows-install-dir-acl-recovery.test.ts index 4a9fd13d6f2..79a4dbc498f 100644 --- a/src/main/startup/windows-install-dir-acl-recovery.test.ts +++ b/src/main/startup/windows-install-dir-acl-recovery.test.ts @@ -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>> { + 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(() => 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 diff --git a/src/main/startup/windows-install-dir-acl-recovery.ts b/src/main/startup/windows-install-dir-acl-recovery.ts index a901b42ec7c..0c09c390e70 100644 --- a/src/main/startup/windows-install-dir-acl-recovery.ts +++ b/src/main/startup/windows-install-dir-acl-recovery.ts @@ -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) { diff --git a/src/main/startup/windows-install-dir-package-acl-repair.ts b/src/main/startup/windows-install-dir-package-acl-repair.ts index e95e0563649..6bd6505a2cb 100644 --- a/src/main/startup/windows-install-dir-package-acl-repair.ts +++ b/src/main/startup/windows-install-dir-package-acl-repair.ts @@ -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