mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
* 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>
105 lines
5.2 KiB
Markdown
105 lines
5.2 KiB
Markdown
# 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.
|