Files
orca/docs/windows-secure-file-acl-hardening.md
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

105 lines
5.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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):
```powershell
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:
```powershell
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.