perf: stop host-status pings rebuilding title catalog and rereading server config (#39340)

* perf: stop host-status pings rebuilding the title catalog and rereading server config

Host-status updates walked every saved tab for title-refresh support before checking whether any host's support changed, and remote RPC/tunnel/worker checks reread, rehardened and reparsed the saved-server registry files on every call. Gate the title walk on an actual per-host capability flip, and reuse the merged registry read while a stat signature of its three files is unchanged.

* perf: invalidate registry read cache on in-process writes

Coarse-timestamp filesystems can reuse an inode within one tick, so a same-size rewrite could slip past the stat signature; writers in this process now bump a generation that is part of the signature.

* perf: key the registry read cache on file bytes, not stat metadata

A stat signature can miss a same-size rewrite on coarse-timestamp filesystems or a reused inode. Comparing the files' exact bytes (plus mode/ctime and the directory mode, which the uncached read re-hardens) means a hit can never serve data that differs from disk, from any writer.

* perf: only reuse a registry read once hardening holds and the path is unchanged

Reuse now requires the files' permissions to already be restricted (POSIX), so a transient chmod failure is retried on the next read as before; Windows hits keep the ACL verify-and-retry. Snapshots also record each path entry's identity, so a symlink re-pointed and restored mid-read cannot pin the value read through the other target.

* perf: build the cached registry value from the exact bytes it is keyed on

The value was computed by a second, independent read, so a directory or symlink swapped and restored around it could pin data the key's bytes never held. Each reader is split into read + parse-text, and on a miss the cache hardens, snapshots once, and parses that snapshot. Reuse also now requires the exact 0600/0700 modes the readers set, so an incomplete owner mode is repaired as before.

* perf: require the full hardened mode, including special bits, before reusing a registry read

* perf: keep Windows on the plain registry reader

Measured on a Windows host: a cache hit still has to re-verify ACLs, so it cost as much as the read it skipped (0.37 ms vs 0.33 ms). Windows now always uses the original reader, keeping its behavior byte-for-byte unchanged.
This commit is contained in:
Neil
2026-11-07 09:56:47 +00:00
committed by GitHub
parent a147277d75
commit aed52aaa9b
10 changed files with 585 additions and 53 deletions
@@ -5,6 +5,12 @@ import { startAiVaultTabTitleSync } from './ai-vault-tab-title-sync'
import { settleAiVaultTitleRequestBatches } from './ai-vault-tab-title-batches'
import { collectAiVaultTitleRequests } from './ai-vault-tab-title-requests'
import type * as MountedTerminalTabRegistry from '@/runtime/mounted-terminal-tab-registry'
import type * as TitleRequests from './ai-vault-tab-title-requests'
vi.mock('./ai-vault-tab-title-requests', async (importOriginal) => {
const actual = await importOriginal<typeof TitleRequests>()
return { ...actual, collectAiVaultTitleRequests: vi.fn(actual.collectAiVaultTitleRequests) }
})
vi.mock('@/runtime/mounted-terminal-tab-registry', async (importOriginal) => ({
...(await importOriginal<typeof MountedTerminalTabRegistry>()),
@@ -170,3 +176,62 @@ describe('session title freshness', () => {
expect(sent).toHaveLength(64)
})
})
describe('host-status title gating', () => {
function runtimeStatus(capabilities: string[], checkedAt: number) {
return new Map([
[
'server-1',
{
checkedAt,
status: {
runtimeId: 'server-1',
rendererGraphEpoch: 1,
graphStatus: 'ready' as const,
authoritativeWindowId: null,
liveTabCount: 1,
liveLeafCount: 1,
capabilities
}
}
]
])
}
it('skips the tab catalog walk when a status ping keeps title-refresh support', async () => {
const store = storeFor('runtime:server-1')
const resolveSessionTitles = vi.fn(async () => titleResult('codex', 'First name'))
stop = startAiVaultTabTitleSync({ ...store, resolveSessionTitles })
await vi.waitFor(() => expect(resolveSessionTitles).toHaveBeenCalledTimes(1))
await new Promise((resolve) => setTimeout(resolve, 0))
vi.mocked(collectAiVaultTitleRequests).mockClear()
store.update({ runtimeStatusByEnvironmentId: runtimeStatus([], 2) })
store.update({ runtimeStatusByEnvironmentId: runtimeStatus([], 3) })
expect(collectAiVaultTitleRequests).not.toHaveBeenCalled()
})
it('reconciles when a host gains title-refresh support', async () => {
const store = storeFor('runtime:server-1')
const resolveSessionTitles = vi.fn(async () => titleResult('codex', 'First name'))
const scheduleReconcile = vi.fn((callback: () => void) => {
queueMicrotask(callback)
return () => {}
})
stop = startAiVaultTabTitleSync({ ...store, resolveSessionTitles, scheduleReconcile })
await vi.waitFor(() => expect(resolveSessionTitles).toHaveBeenCalledTimes(1))
await new Promise((resolve) => setTimeout(resolve, 0))
scheduleReconcile.mockClear()
store.update({ runtimeStatusByEnvironmentId: runtimeStatus([], 2) })
expect(scheduleReconcile).not.toHaveBeenCalled()
store.update({
runtimeStatusByEnvironmentId: runtimeStatus(
[AI_VAULT_SESSION_TITLE_REFRESH_RUNTIME_CAPABILITY],
3
)
})
expect(scheduleReconcile).toHaveBeenCalledTimes(1)
})
})
@@ -21,15 +21,39 @@ export function supportsSessionTitleRefresh(
const host = request.executionHostId
return (
host === 'local' ||
(host.startsWith('runtime:') &&
Boolean(
lastVerifiedRuntimeStatus(
state.runtimeStatusByEnvironmentId?.get(host.slice(8))
)?.capabilities?.includes(AI_VAULT_SESSION_TITLE_REFRESH_RUNTIME_CAPABILITY)
))
(host.startsWith('runtime:') && runtimeSupportsSessionTitleRefresh(state, host.slice(8)))
)
}
function runtimeSupportsSessionTitleRefresh(state: AppState, environmentId: string): boolean {
return Boolean(
lastVerifiedRuntimeStatus(
state.runtimeStatusByEnvironmentId?.get(environmentId)
)?.capabilities?.includes(AI_VAULT_SESSION_TITLE_REFRESH_RUNTIME_CAPABILITY)
)
}
/** Execution hosts whose title-refresh support flipped. Host-status pings arrive constantly, so callers check this
* before paying for a full tab/conversation catalog walk. */
export function changedTitleRefreshHosts(state: AppState, previous: AppState): Set<string> {
const changed = new Set<string>()
const current = state.runtimeStatusByEnvironmentId
const prior = previous.runtimeStatusByEnvironmentId
if (current === prior) {
return changed
}
for (const environmentId of new Set([...(current?.keys() ?? []), ...(prior?.keys() ?? [])])) {
if (
current?.get(environmentId) !== prior?.get(environmentId) &&
runtimeSupportsSessionTitleRefresh(state, environmentId) !==
runtimeSupportsSessionTitleRefresh(previous, environmentId)
) {
changed.add(`runtime:${environmentId}`)
}
}
return changed
}
export function titleRefreshInterval(state: AppState, request: AiVaultTitleRequest): number {
const stored = state.tabsByWorktree[request.worktreeId]?.find(
(tab) => tab.id === request.tabId
@@ -14,6 +14,7 @@ import { aiVaultTitleSyncInputsChanged } from './ai-vault-tab-title-sync-inputs'
import {
aiVaultTitleRequestKey,
changedOwningPaneTitleRequests,
changedTitleRefreshHosts,
titleRefreshInterval,
supportsSessionTitleRefresh
} from './ai-vault-tab-title-freshness'
@@ -239,15 +240,18 @@ export function startAiVaultTabTitleSync(dependencies: SyncDependencies): () =>
dirty.add(key)
}
}
if (dirty.size > 0 || aiVaultTitleSyncInputsChanged(state, previous)) {
schedule()
return
}
// Why: host-status pings are frequent and usually leave capabilities alone; walking every saved tab on each
// one stalled typing in large workspaces, so only collect when some host's title-refresh support flipped.
const changedHosts = changedTitleRefreshHosts(state, previous)
if (
dirty.size > 0 ||
(state.runtimeStatusByEnvironmentId !== previous.runtimeStatusByEnvironmentId &&
collectAiVaultTitleRequests(state).some(
(request) =>
supportsSessionTitleRefresh(state, request) !==
supportsSessionTitleRefresh(previous, request)
)) ||
aiVaultTitleSyncInputsChanged(state, previous)
changedHosts.size > 0 &&
collectAiVaultTitleRequests(state).some((request) =>
changedHosts.has(request.executionHostId)
)
) {
schedule()
}
@@ -19,7 +19,7 @@ import { hardenExistingSecureFile } from '../../secure-file'
// Shipped builds strip unknown environment fields on routine writes; preserve newer state in this untouched sidecar.
const SIDECAR_FILE = 'orca-environment-sidecar.json'
const MAX_SIDECAR_FILE_BYTES = 1024 * 1024
export const MAX_SIDECAR_FILE_BYTES = 1024 * 1024
const SidecarBindingSchema = z.object({
createdAt: z.number().finite(),
@@ -61,7 +61,7 @@ const SidecarEnvelopeSchema = z.object({
})
export type RuntimeEnvironmentSidecarEntry = z.infer<typeof SidecarEntrySchema>
type RuntimeEnvironmentSidecar = z.infer<typeof SidecarSchema> & {
export type RuntimeEnvironmentSidecar = z.infer<typeof SidecarSchema> & {
/** Entries this build cannot read, kept verbatim; their environments must not be used. */
unreadable: Record<string, unknown>
}
@@ -95,12 +95,24 @@ export function readRuntimeEnvironmentSidecar(userDataPath: string): RuntimeEnvi
if (!existsSync(path)) {
return { version: 1, entries: {}, unreadable: {} }
}
let raw: Record<string, unknown>
let text: string
try {
hardenExistingSecureFile(path)
raw = SidecarEnvelopeSchema.parse(
JSON.parse(readNodeFileSyncWithinLimit(path, MAX_SIDECAR_FILE_BYTES).buffer.toString('utf8'))
).entries
text = readNodeFileSyncWithinLimit(path, MAX_SIDECAR_FILE_BYTES).buffer.toString('utf8')
} catch {
throw new RuntimeEnvironmentSidecarInvalidError(path)
}
return parseRuntimeEnvironmentSidecarText(path, text)
}
/** Parses bytes already read from `path`; the error matches a failed read of that file. */
export function parseRuntimeEnvironmentSidecarText(
path: string,
text: string
): RuntimeEnvironmentSidecar {
let raw: Record<string, unknown>
try {
raw = SidecarEnvelopeSchema.parse(JSON.parse(text)).entries
} catch {
throw new RuntimeEnvironmentSidecarInvalidError(path)
}
@@ -9,33 +9,50 @@ import {
type RuntimeEnvironmentStore
} from '../host-catalog/runtime-environments'
import {
getRuntimeEnvironmentSidecarPath,
MAX_SIDECAR_FILE_BYTES,
overlayRuntimeEnvironmentSidecar,
parseRuntimeEnvironmentSidecarText,
readRuntimeEnvironmentSidecar,
type RuntimeEnvironmentSidecar,
RuntimeEnvironmentSidecarInvalidError
} from './runtime-environment-sidecar'
import {
getEnvironmentStorePath,
MAX_RUNTIME_ENVIRONMENT_STORE_FILE_BYTES,
parseEnvironmentStoreRecordsText,
readEnvironmentStoreRecords,
backUpBlankEnvironmentStore,
remoteServerPairingStoreError,
RuntimeEnvironmentStoreError,
type EnvironmentStoreRecords,
type EnvironmentStoreRecordsReadOptions,
warnUnreadableEnvironmentState
} from './runtime-environment-store-records'
import {
getWslHostDeploymentStorePath,
MAX_WSL_HOST_DEPLOYMENTS_FILE_BYTES,
overlayWslHostDeployments,
parseWslHostDeploymentsText,
readWslHostDeployments,
type WslHostDeployments,
WslHostDeploymentStoreInvalidError
} from './wsl-host-deployment-store'
import { isWslHostsServing } from '../../wsl-host-internal-gate'
import { createSecureFileContentCache } from './runtime-environment-store-read-cache'
/** Readable records only; writeEnvironmentStore preserves unreadable corrupt/newer records verbatim. */
export function readPersistedEnvironmentStore(
userDataPath: string,
options: EnvironmentStoreRecordsReadOptions = {}
): RuntimeEnvironmentStore {
const { environments, unreadable } = readEnvironmentStoreRecords(userDataPath, options)
return persistedStoreFromRecords(userDataPath, readEnvironmentStoreRecords(userDataPath, options))
}
function persistedStoreFromRecords(
userDataPath: string,
{ environments, unreadable }: EnvironmentStoreRecords
): RuntimeEnvironmentStore {
if (unreadable.length > 0) {
warnUnreadableEnvironmentState(
`Skipping ${unreadable.length} unreadable server record(s) in ${getEnvironmentStorePath(userDataPath)} (ids: ${unreadable.map((record) => record.id ?? '<none>').join(', ')}); they may need a newer version of Orca and are kept on disk.`
@@ -53,11 +70,11 @@ export function readPersistedEnvironmentStore(
* it, so a newer build's file is kept as it is. With hosts off it hides nothing.
*/
function readWslHostDeploymentsForRegistry(
userDataPath: string
): ReturnType<typeof readWslHostDeployments> {
readDeployments: () => WslHostDeployments
): WslHostDeployments {
const gateOpen = isWslHostsServing(process.env)
try {
const read = readWslHostDeployments(userDataPath)
const read = readDeployments()
return gateOpen ? read : { deployments: read.deployments, unreadableEnvironmentIds: new Set() }
} catch (error) {
if (!(error instanceof WslHostDeploymentStoreInvalidError)) {
@@ -68,19 +85,78 @@ function readWslHostDeploymentsForRegistry(
}
}
type MergedEnvironmentStore = {
version: 1
environments: KnownRuntimeEnvironment[]
}
const mergedStoreCache = createSecureFileContentCache<MergedEnvironmentStore>()
export function readEnvironmentStore(
userDataPath: string,
options: EnvironmentStoreRecordsReadOptions = {}
): {
version: 1
environments: KnownRuntimeEnvironment[]
} {
const store = readPersistedEnvironmentStore(userDataPath, options)
let sidecar: ReturnType<typeof readRuntimeEnvironmentSidecar>
let wslDeployments: ReturnType<typeof readWslHostDeployments>
): MergedEnvironmentStore {
// Windows hits must re-verify ACLs anyway, which costs as much as the read it would skip (measured).
if (options.requireStoreFile || options.recoverBlankStore || process.platform === 'win32') {
return readEnvironmentStoreUncached(userDataPath, options)
}
const storePath = getEnvironmentStorePath(userDataPath)
const sidecarPath = getRuntimeEnvironmentSidecarPath(userDataPath)
const wslPath = getWslHostDeploymentStorePath(userDataPath)
return mergedStoreCache.read(
`${userDataPath}\0${isWslHostsServing(process.env)}`,
userDataPath,
[
{ path: storePath, maxBytes: MAX_RUNTIME_ENVIRONMENT_STORE_FILE_BYTES },
{ path: sidecarPath, maxBytes: MAX_SIDECAR_FILE_BYTES },
{ path: wslPath, maxBytes: MAX_WSL_HOST_DEPLOYMENTS_FILE_BYTES }
],
([storeText, sidecarText, wslText]) =>
mergeEnvironmentStore(userDataPath, {
records: () =>
storeText == null
? { environments: [], unreadable: [] }
: parseEnvironmentStoreRecordsText(storePath, storeText),
sidecar: () =>
sidecarText == null
? { version: 1, entries: {}, unreadable: {} }
: parseRuntimeEnvironmentSidecarText(sidecarPath, sidecarText),
wslDeployments: () =>
wslText == null
? { deployments: [], unreadableEnvironmentIds: new Set() }
: parseWslHostDeploymentsText(wslPath, wslText)
}),
() => readEnvironmentStoreUncached(userDataPath, options)
)
}
type EnvironmentStoreSources = {
records: () => EnvironmentStoreRecords
sidecar: () => RuntimeEnvironmentSidecar
wslDeployments: () => WslHostDeployments
}
function readEnvironmentStoreUncached(
userDataPath: string,
options: EnvironmentStoreRecordsReadOptions
): MergedEnvironmentStore {
return mergeEnvironmentStore(userDataPath, {
records: () => readEnvironmentStoreRecords(userDataPath, options),
sidecar: () => readRuntimeEnvironmentSidecar(userDataPath),
wslDeployments: () => readWslHostDeployments(userDataPath)
})
}
function mergeEnvironmentStore(
userDataPath: string,
sources: EnvironmentStoreSources
): MergedEnvironmentStore {
const store = persistedStoreFromRecords(userDataPath, sources.records())
let sidecar: RuntimeEnvironmentSidecar
let wslDeployments: WslHostDeployments
try {
sidecar = readRuntimeEnvironmentSidecar(userDataPath)
wslDeployments = readWslHostDeploymentsForRegistry(userDataPath)
sidecar = sources.sidecar()
wslDeployments = readWslHostDeploymentsForRegistry(sources.wslDeployments)
} catch (error) {
// Fail closed like the main file: a link we cannot read may pin a host key we must not drop.
if (
@@ -0,0 +1,138 @@
import * as fs from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { encodePairingOffer } from '../pairing/pairing'
import type * as BoundedReader from '../../node-bounded-file-reader'
import { __resetSecureFileHardenedPathsForTests } from '../../secure-file'
import { addEnvironmentFromPairingCode, listEnvironments } from './runtime-environment-store'
import { getEnvironmentStorePath } from './runtime-environment-store-records'
type ReadHook = ((path: string) => void) | undefined
const hooks = vi.hoisted((): { failChmod: boolean; beforeRead: ReadHook; afterRead: ReadHook } => ({
failChmod: false,
beforeRead: undefined,
afterRead: undefined
}))
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof fs>()
return {
...actual,
chmodSync: (...args: Parameters<typeof actual.chmodSync>) => {
if (hooks.failChmod) {
throw Object.assign(new Error('EPERM'), { code: 'EPERM' })
}
return actual.chmodSync(...args)
}
}
})
vi.mock('../../node-bounded-file-reader', async (importOriginal) => {
const actual = await importOriginal<typeof BoundedReader>()
return {
...actual,
readNodeFileSyncWithinLimit: (
...args: Parameters<typeof actual.readNodeFileSyncWithinLimit>
) => {
hooks.beforeRead?.(args[0])
try {
return actual.readNodeFileSyncWithinLimit(...args)
} finally {
hooks.afterRead?.(args[0])
}
}
}
})
let root: string
let userDataPath: string
beforeEach(() => {
root = fs.mkdtempSync(join(tmpdir(), 'orca-env-cache-integration-'))
userDataPath = join(root, 'userData')
fs.mkdirSync(userDataPath, { mode: 0o700 })
__resetSecureFileHardenedPathsForTests()
})
afterEach(() => {
hooks.failChmod = false
hooks.beforeRead = undefined
hooks.afterRead = undefined
fs.rmSync(root, { recursive: true, force: true })
})
function addServer(profile: string, name: string, seed: number): string {
return addEnvironmentFromPairingCode(profile, {
name,
pairingCode: encodePairingOffer({
v: 2,
publicKeyB64: Buffer.alloc(32, seed).toString('base64'),
deviceToken: `token-${seed}`,
endpoint: `wss://host-${seed}.example.com/runtime`
}),
now: 100 + seed
}).id
}
const names = (): string[] => listEnvironments(userDataPath).map((entry) => entry.name)
describe('saved-server registry read reuse', () => {
it('sees a registry rewritten by another writer between reads', () => {
addServer(userDataPath, 'Alpha', 1)
expect(names()).toEqual(['Alpha'])
expect(names()).toEqual(['Alpha'])
const path = getEnvironmentStorePath(userDataPath)
const store = JSON.parse(fs.readFileSync(path, 'utf8'))
store.environments[0].name = 'Bravo'
fs.writeFileSync(path, JSON.stringify(store), { mode: 0o600 })
expect(names()).toEqual(['Bravo'])
})
it('does not keep a value read from a directory swapped in and back out mid-read', () => {
addServer(userDataPath, 'Alpha', 1)
const other = join(root, 'other')
fs.mkdirSync(other, { mode: 0o700 })
addServer(other, 'Bravo', 2)
const parked = join(root, 'parked')
const storePath = getEnvironmentStorePath(userDataPath)
let storeReads = 0
hooks.beforeRead = (path) => {
// The first read checks for a reusable value; the second is the one a value is built from.
if (path === storePath && ++storeReads === 2) {
fs.renameSync(userDataPath, parked)
fs.renameSync(other, userDataPath)
}
}
hooks.afterRead = (path) => {
if (path === storePath && fs.existsSync(parked)) {
fs.renameSync(userDataPath, other)
fs.renameSync(parked, userDataPath)
}
}
// The racing read may report what it saw, but must not be reused once the original is back.
names()
hooks.beforeRead = undefined
hooks.afterRead = undefined
expect(names()).toEqual(['Alpha'])
expect(names()).toEqual(['Alpha'])
})
it.skipIf(process.platform === 'win32').each([
['readable by others', 0o644],
['owner read-only', 0o400],
['setuid', 0o4600]
])('retries hardening a registry left %s by a transient chmod failure', (_label, mode) => {
addServer(userDataPath, 'Alpha', 1)
const path = getEnvironmentStorePath(userDataPath)
fs.chmodSync(path, mode)
__resetSecureFileHardenedPathsForTests()
hooks.failChmod = true
names()
names()
expect(fs.statSync(path).mode & 0o7777).toBe(mode)
hooks.failChmod = false
names()
expect(fs.statSync(path).mode & 0o7777).toBe(0o600)
})
})
@@ -0,0 +1,60 @@
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createSecureFileContentCache } from './runtime-environment-store-read-cache'
let dir: string
let files: { path: string; maxBytes: number }[]
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'orca-env-cache-'))
files = [
{ path: join(dir, 'store.json'), maxBytes: 1024 },
{ path: join(dir, 'sidecar.json'), maxBytes: 1024 }
]
writeFileSync(files[0]!.path, '{"a":1}', { mode: 0o600 })
})
afterEach(() => {
rmSync(dir, { recursive: true, force: true })
})
// Windows never reuses reads (ACLs are invisible to the snapshot), so there is no cache to exercise.
describe.skipIf(process.platform === 'win32')('secure file content cache', () => {
it('computes from the bytes it keys on and reuses them only while they are unchanged', () => {
const cache = createSecureFileContentCache<{ texts: (string | null)[] }>()
const compute = vi.fn((texts: (string | null)[]) => ({ texts }))
const fallback = vi.fn(() => ({ texts: [] }))
const read = () => cache.read(dir, dir, files, compute, fallback)
read().texts.push('mutated')
expect(read()).toEqual({ texts: ['{"a":1}', null] })
expect(compute).toHaveBeenCalledTimes(1)
// Same size, new bytes: what a stat signature on a coarse-timestamp filesystem could miss.
writeFileSync(files[0]!.path, '{"a":2}')
expect(read()).toEqual({ texts: ['{"a":2}', null] })
writeFileSync(files[1]!.path, '{}', { mode: 0o600 })
expect(read()).toEqual({ texts: ['{"a":2}', '{}'] })
expect(read()).toEqual({ texts: ['{"a":2}', '{}'] })
expect(compute).toHaveBeenCalledTimes(3)
expect(fallback).not.toHaveBeenCalled()
})
it('never caches a failed compute and uses the plain reader for an unreadable file', () => {
const cache = createSecureFileContentCache<number>()
const compute = vi
.fn<() => number>()
.mockImplementationOnce(() => {
throw new Error('invalid')
})
.mockReturnValue(1)
const fallback = vi.fn(() => 2)
expect(() => cache.read(dir, dir, files, compute, fallback)).toThrow('invalid')
expect(cache.read(dir, dir, files, compute, fallback)).toBe(1)
writeFileSync(files[0]!.path, 'x'.repeat(2048))
expect(cache.read(dir, dir, files, compute, fallback)).toBe(2)
expect(cache.read(dir, dir, files, compute, fallback)).toBe(2)
expect(compute).toHaveBeenCalledTimes(2)
})
})
@@ -0,0 +1,122 @@
import { existsSync, statSync } from 'node:fs'
import { hardenExistingSecureFile } from '../../secure-file'
import { readNodeFileSyncWithinLimit } from '../../node-bounded-file-reader'
// Why: remote RPC, tunnel and worker-server checks resolve the saved-server registry many times per second;
// rechecking, rehardening, reparsing and revalidating unchanged files each time blocked the Electron main thread.
// A cached value is computed from exactly the bytes it is keyed on, so it can never differ from what those
// bytes parse to, however the files or their directory are swapped around the read.
const MAX_CACHED_KEYS = 8
const PROTECTED_FILE_MODE = 0o600
const PROTECTED_DIRECTORY_MODE = 0o700
export type CachedFile = { path: string; maxBytes: number }
type Snapshot = { directoryMode: number | null; files: ({ bytes: Buffer; mode: number } | null)[] }
function isMissing(error: unknown): boolean {
return error instanceof Error && 'code' in error && error.code === 'ENOENT'
}
function snapshot(directory: string, files: readonly CachedFile[]): Snapshot {
return {
directoryMode: statSync(directory, { throwIfNoEntry: false })?.mode ?? null,
files: files.map(({ path, maxBytes }) => {
try {
const { buffer, stats } = readNodeFileSyncWithinLimit(path, maxBytes)
return { bytes: buffer, mode: stats.mode }
} catch (error) {
if (isMissing(error)) {
return null
}
throw error
}
})
}
}
function sameBytes(a: Snapshot, b: Snapshot): boolean {
return a.files.every((file, index) => {
const other = b.files[index]
return file && other ? file.bytes.equals(other.bytes) : file === other
})
}
function samePresence(a: Snapshot, b: Snapshot): boolean {
return a.files.every((file, index) => (file === null) === (b.files[index] === null))
}
/**
* Readers re-harden on every read and retry after a failed chmod; reuse is allowed only while the modes they
* set are visibly in place (POSIX), so any drift or earlier failure is repaired by the next uncached read.
*/
function isProtected(snapshot: Snapshot): boolean {
// Windows protection lives in ACLs the snapshot cannot see; never claim it is in place.
if (process.platform === 'win32') {
return false
}
return (
(snapshot.directoryMode === null ||
(snapshot.directoryMode & 0o7777) === PROTECTED_DIRECTORY_MODE) &&
snapshot.files.every((file) => file === null || (file.mode & 0o7777) === PROTECTED_FILE_MODE)
)
}
export type SecureFileContentCache<T> = {
/**
* `compute` parses the given file texts (null = missing). `fallback` is the plain reader; it runs whenever
* the files cannot be snapshotted or hardened, so errors surface exactly as they did without the cache.
*/
read: (
key: string,
directory: string,
files: readonly CachedFile[],
compute: (texts: (string | null)[]) => T,
fallback: () => T
) => T
}
export function createSecureFileContentCache<T>(): SecureFileContentCache<T> {
const cache = new Map<string, { snapshot: Snapshot; value: T }>()
return {
read(key, directory, files, compute, fallback) {
let current: Snapshot
try {
current = snapshot(directory, files)
} catch {
cache.delete(key)
return fallback()
}
const cached = cache.get(key)
if (cached && sameBytes(cached.snapshot, current) && isProtected(current)) {
// Callers own their copy; one mutating a result must not leak into the next read.
return structuredClone(cached.value)
}
cache.delete(key)
let read: Snapshot
try {
// Same hardening the readers do before reading a present file.
for (const file of files) {
if (existsSync(file.path)) {
hardenExistingSecureFile(file.path)
}
}
read = snapshot(directory, files)
} catch {
return fallback()
}
if (!samePresence(current, read)) {
// A file appeared or vanished mid-read; let the plain reader handle that state.
return fallback()
}
const value = compute(read.files.map((file) => file?.bytes.toString('utf8') ?? null))
if (isProtected(read)) {
cache.set(key, { snapshot: read, value: structuredClone(value) })
if (cache.size > MAX_CACHED_KEYS) {
cache.delete(cache.keys().next().value!)
}
}
return value
}
}
}
@@ -104,22 +104,39 @@ export function readEnvironmentStoreRecords(
if (!options.requireStoreFile && !existsSync(path)) {
return { environments: [], unreadable: [] }
}
let records: unknown[]
let text: string
try {
hardenExistingSecureFile(path)
const text = readNodeFileSyncWithinLimit(
text = readNodeFileSyncWithinLimit(
path,
MAX_RUNTIME_ENVIRONMENT_STORE_FILE_BYTES
).buffer.toString('utf8')
if (options.recoverBlankStore && /^[\s\0]*$/.test(text)) {
return { environments: [], unreadable: [], blankFile: true }
}
} catch {
throw invalidEnvironmentStoreError(path)
}
if (options.recoverBlankStore && /^[\s\0]*$/.test(text)) {
return { environments: [], unreadable: [], blankFile: true }
}
return parseEnvironmentStoreRecordsText(path, text)
}
function invalidEnvironmentStoreError(path: string): RuntimeEnvironmentStoreError {
return new RuntimeEnvironmentStoreError(
'runtime_error',
`Could not read Orca environments at ${path}; the file is invalid.`
)
}
/** Parses bytes already read from `path`; the error matches a failed read of that file. */
export function parseEnvironmentStoreRecordsText(
path: string,
text: string
): EnvironmentStoreRecords {
let records: unknown[]
try {
records = StoreEnvelopeSchema.parse(JSON.parse(text)).environments
} catch {
throw new RuntimeEnvironmentStoreError(
'runtime_error',
`Could not read Orca environments at ${path}; the file is invalid.`
)
throw invalidEnvironmentStoreError(path)
}
const result: EnvironmentStoreRecords = { environments: [], unreadable: [] }
for (const raw of records) {
@@ -133,7 +150,6 @@ export function readEnvironmentStoreRecords(
return result
}
/** Copies (never moves) a blank store aside before a write replaces it, so readers never see the name missing. */
export function backUpBlankEnvironmentStore(userDataPath: string): void {
const path = getEnvironmentStorePath(userDataPath)
const backupPath = `${path}.corrupt-${Date.now()}-${process.pid}`
@@ -15,7 +15,7 @@ import { OrcadWslDeploymentLinkSchema } from '../host-catalog/wsl-host-deploymen
// Isolate WSL records so older builds cannot reject the entire pairing store.
const WSL_HOST_DEPLOYMENTS_FILE = 'orca-wsl-host-deployments.json'
const MAX_WSL_HOST_DEPLOYMENTS_FILE_BYTES = 256 * 1024
export const MAX_WSL_HOST_DEPLOYMENTS_FILE_BYTES = 256 * 1024
/** A managed orcad in a WSL distro. The environment's name is its label; runtime state lives elsewhere. */
/** A Remove that could not prove the host's server stopped (E11); the host stays until one does. */
@@ -94,16 +94,22 @@ function readStoredRecords(userDataPath: string): StoredRecord[] {
if (!existsSync(path)) {
return []
}
let raws: unknown[]
let text: string
try {
hardenExistingSecureFile(path)
raws = EnvelopeSchema.parse(
JSON.parse(
readNodeFileSyncWithinLimit(path, MAX_WSL_HOST_DEPLOYMENTS_FILE_BYTES).buffer.toString(
'utf8'
)
)
).deployments
text = readNodeFileSyncWithinLimit(path, MAX_WSL_HOST_DEPLOYMENTS_FILE_BYTES).buffer.toString(
'utf8'
)
} catch {
throw new WslHostDeploymentStoreInvalidError(path)
}
return parseStoredRecordsText(path, text)
}
function parseStoredRecordsText(path: string, text: string): StoredRecord[] {
let raws: unknown[]
try {
raws = EnvelopeSchema.parse(JSON.parse(text)).deployments
} catch {
throw new WslHostDeploymentStoreInvalidError(path)
}
@@ -131,8 +137,17 @@ function readStoredRecords(userDataPath: string): StoredRecord[] {
/** Throws WslHostDeploymentStoreInvalidError when the file itself is unreadable. */
export function readWslHostDeployments(userDataPath: string): WslHostDeployments {
return deploymentsFromRecords(readStoredRecords(userDataPath))
}
/** Parses bytes already read from `path`; throws WslHostDeploymentStoreInvalidError like a failed read. */
export function parseWslHostDeploymentsText(path: string, text: string): WslHostDeployments {
return deploymentsFromRecords(parseStoredRecordsText(path, text))
}
function deploymentsFromRecords(records: StoredRecord[]): WslHostDeployments {
const result: WslHostDeployments = { deployments: [], unreadableEnvironmentIds: new Set() }
for (const record of readStoredRecords(userDataPath)) {
for (const record of records) {
if (record.deployment) {
result.deployments.push(record.deployment)
} else if (record.environmentId !== undefined) {