Files
orca/docs/windows-secure-file-acl-hardening.md
T
4d6c291eff fix(windows): stop main-thread PowerShell ACL storm on env-store reads (#5011)
* fix(windows): stop main-thread PowerShell storm on env-store reads

Two changes fix the v1.4.52+ Windows performance regression (#4901 regression
against #4840) where 49 powershell.exe processes were spawned in 27 seconds
during load, saturating the Electron main thread and causing black terminals
and runtimeEnvironments:call timeouts.

Root cause: `readEnvironmentStore` calls `hardenExistingSecureFile` on every
read. The env-store parent directory's mtime churns constantly (every secure
write updates it), so the mtime-keyed idempotency cache never matched →
`bestEffortRestrictWindowsPath` (powershell, ~1-1.5s synchronous) fired on
every call. After #4901, the remote-runtime tab-sync loop reads the store
~2×/s, turning sporadic mtime misses into a continuous main-thread storm.

Fix 1 – path-cached directory hardening: add `hardenedDirectoryPathsThisProcess
(Set<string>)` that caches directory hardening by PATH for the process lifetime.
A directory's required ACL does not change when its mtime changes; only file
hardening retains the metadata-keyed cache so post-rename inode changes are
detected correctly.

Fix 2 – async ACL application: replace `execFileSync(powershell.exe, ...)` with
`execFile` (fire-and-forget). PowerShell cold-start is ~1-1.5s; the function is
already named `bestEffortRestrictWindowsPath` so async/optimistic caching is
correct. `applySecurePathRestriction` returns `true` optimistically on win32 so
the cache entry is written before the background process completes.

Tests: new regression tests verify the directory is hardened exactly once even
when its mtime changes between calls, that unchanged files are not re-hardened,
and that ACL application goes through async execFile (not execFileSync).

* fix(windows): apply credential-file ACL synchronously on write path

Follow-up rigor on the env-store PowerShell ACL storm fix (#5006). The
read-path storm fix (path-cached async directory hardening + async file
re-harden) is retained, but switching ALL ACL application to async opened a
narrow Windows-only security window: because writeFileSync({mode}) is a no-op
on Windows, writeSecureFile returned with the credential file still carrying
the parent directory's inherited (broader) ACL for the ~1-1.5s PowerShell
cold-start, affecting the e2ee keypair, device registry, and runtime env auth
store.

Fix: apply the credential FILE's ACL synchronously (execFileSync) on the
infrequent write path, before the atomic rename publishes it, and cache the
path as hardened only on confirmed success so a failed apply retries. Keep the
DIRECTORY hardening async + path-cached for the process lifetime (that is what
killed the #4901/#5006 main-thread storm). The read path's existing-file
re-harden stays async + metadata-cached (fires at most once per file, no storm).

Also:
- Document the dir-path cache process-lifetime known limitation (deleted+
  recreated dir not re-hardened until restart).
- Remove the redundant double dir-cache write in writeSecureFile.
- Add docs/windows-secure-file-acl-hardening.md describing the sync-file/
  async-dir model and a manual Windows e2e test plan (the cross-platform
  Playwright harness runs on Linux and cannot reach the PowerShell path).

Tests (src/shared/secure-file.test.ts, 13 passing): credential file hardened
synchronously while dir stays async (no async file-ACL window); failed sync
file-ACL apply is not cached and retries; dir hardened exactly once across many
writes despite mtime churn; no PowerShell spawned on non-win32.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: keep POSIX secure directory hardening metadata-aware

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
2026-06-09 01:47:29 -07:00

5.2 KiB
Raw Blame History

Windows Secure-File ACL Hardening

Problem

Credential files Orca writes on Windows (runtime env auth store, device registry, e2ee keypair) must end up readable only by the current user, SYSTEM, and Administrators. On POSIX this is a one-line chmodSync, but writeFileSync's mode option is a no-op on Windows, so the NTFS ACL has to be rewritten by shelling out to PowerShell (Get-Acl / Set-Acl). PowerShell cold-start is ~1–1.5 s.

readEnvironmentStore calls hardenExistingSecureFile on every read. The env-store parent directory's mtime churns constantly (every secure write updates it), so an mtime-keyed idempotency cache never matches and a blocking PowerShell spawn fires on every call. After the remote-runtime tab-sync polling change, the store is read ~2×/s, turning sporadic mtime misses into a continuous main-thread storm (~1.8 powershell.exe spawns/sec) that saturates the Electron main thread and times out runtimeEnvironments:call. See #4901 / #5006.

Model

src/shared/secure-file.ts applies two different caching + execution strategies, chosen by whether the target is a directory and whether it is on the write path:

  • Directories — async + path-cached for the process lifetime. A directory's required ACL does not change when its mtime changes, so once a directory has been hardened in this process it is trusted for the rest of the process. Directory hardening uses fire-and-forget execFile so it never blocks the main thread. This is the change that kills the #4901 storm.

    • Known limitation: a directory that is deleted and recreated mid-process is not re-hardened until the next restart. The .orca secure dirs are not deleted at runtime, so this is acceptable.
  • Credential files on the write path — synchronous, cache only on success. Because writeFileSync({ mode }) is a no-op on Windows, a freshly written file carries the parent directory's inherited (broader) ACL. writeSecureFile must therefore restrict the file's ACL synchronously (via execFileSync) on the temp file and on the renamed target before it returns — otherwise the function would return with the credential briefly readable under inherited ACLs during the ~1–1.5 s PowerShell cold-start window. The write path is infrequent, so the synchronous cost is acceptable. The path is cached as hardened only on confirmed success, so a failed apply is retried on the next write.

  • Existing files on the read path — async + metadata-cached. hardenExistingSecureFile re-asserts the ACL on an already-existing file at most once per process (keyed on inode/size/timestamps so post-rename inode changes are detected). Async is safe here because it only re-asserts an ACL on a file that already exists; new files are hardened synchronously on the write path above. Because it fires at most once per file, it does not storm.

The net effect: the frequent read path never blocks the main thread, while the infrequent write path closes the async window so credential files are never published with a broader-than-intended ACL.

Requirements

  • Read-path directory and existing-file hardening must not spawn PowerShell more than once per path per process, regardless of mtime churn.
  • writeSecureFile must apply the credential file's ACL synchronously before returning; the file ACL must not be left to a background process.
  • A failed synchronous file-ACL apply must not crash the write and must not be cached as hardened (so it retries).
  • No PowerShell is spawned on non-win32 platforms.

Manual Windows end-to-end test plan

The automated e2e harness (pnpm test:e2e, Playwright electron-headless) runs on ubuntu-latest, where applySecurePathRestriction short-circuits to chmodSync and never reaches the PowerShell path. The ACL storm therefore cannot be reproduced in the cross-platform e2e harness; verify it manually on Windows.

Pre-req: a Windows client paired to a remote orca serve runtime.

Watcher (PowerShell, run before launching Orca):

while ($true) {
  $n = (Get-CimInstance Win32_Process -Filter "Name='powershell.exe'").Count
  "{0}  powershell.exe count = {1}" -f (Get-Date -Format HH:mm:ss), $n
  Start-Sleep -Milliseconds 500
}

Steps:

  1. Launch the stock v1.4.52/v1.4.53 build and open the remote workspace.

    • Before fix: the watcher oscillates ~1–2 powershell.exe processes/sec continuously through the load window; the app is unresponsive and the [web-session-tabs-sync] … RemoteRuntimeClientError: Timed out error appears in the console.
  2. Launch the fixed build and open the same remote workspace.

    • After fix: the watcher stays at 0 (no continuous powershell churn); the env-store directory is hardened at most once; the app loads without the session-tabs timeout.
  3. Write a credential (e.g. sign in / register a device so a secure file is written), then immediately inspect the file ACL:

    icacls "$env:APPDATA\orca\orca-environments.json"
    
    • Expected: only the current user, SYSTEM, and Administrators have access the instant the write completes (no inherited entries), confirming the synchronous file-ACL apply closed the async window.