mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
test(codex): canonicalize harness containment guards and retry cleanup
Resolve symlinks before the disposable-root containment checks so a symlinked temp parent cannot smuggle the throwaway home inside the primary home, and give the final cleanup rm Windows retry/force so a briefly lingering codex handle cannot strand the credential-bearing root.
This commit is contained in:
@@ -57,6 +57,15 @@ function isWithin(candidate, parent) {
|
||||
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
|
||||
}
|
||||
|
||||
async function resolveRealPath(candidate) {
|
||||
try {
|
||||
return await realpath(candidate)
|
||||
} catch {
|
||||
// Why: containment guards must still run for paths that do not exist yet.
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
export function createValidationEnv(inheritedEnv, layout) {
|
||||
const env = { ...inheritedEnv }
|
||||
for (const key of RESTRICTED_ENV_KEYS) {
|
||||
@@ -78,10 +87,16 @@ export async function createValidationLayout(options = {}) {
|
||||
const primaryHome = path.resolve(options.primaryHome ?? os.homedir())
|
||||
const envTempParent = process.env.ORCA_CODEX_VALIDATION_TEMP_PARENT?.trim()
|
||||
const tempParent = path.resolve(options.tempParent ?? (envTempParent || os.tmpdir()))
|
||||
// Why: guards must compare canonical paths — a symlinked temp parent must
|
||||
// not smuggle the disposable root inside the primary home.
|
||||
const [primaryHomeReal, tempParentReal] = await Promise.all([
|
||||
resolveRealPath(primaryHome),
|
||||
resolveRealPath(tempParent)
|
||||
])
|
||||
// Why: on Windows the default %TEMP% lives inside %USERPROFILE%, which the
|
||||
// disposable-home guard below rightly refuses. Fail before creating anything
|
||||
// and point at the overrides instead of aborting with an opaque guard error.
|
||||
if (samePath(tempParent, primaryHome) || isWithin(tempParent, primaryHome)) {
|
||||
if (samePath(tempParentReal, primaryHomeReal) || isWithin(tempParentReal, primaryHomeReal)) {
|
||||
throw new Error(
|
||||
`Refusing to place the disposable validation root inside the primary home (${primaryHome}). ` +
|
||||
'Pass --temp-parent <dir> or set ORCA_CODEX_VALIDATION_TEMP_PARENT to a directory outside it.'
|
||||
@@ -94,7 +109,8 @@ export async function createValidationLayout(options = {}) {
|
||||
mkdir(homeDir, { recursive: true, mode: 0o700 }),
|
||||
mkdir(userDataDir, { recursive: true, mode: 0o700 })
|
||||
])
|
||||
if (samePath(primaryHome, homeDir) || isWithin(homeDir, primaryHome)) {
|
||||
const homeDirReal = await resolveRealPath(homeDir)
|
||||
if (samePath(primaryHomeReal, homeDirReal) || isWithin(homeDirReal, primaryHomeReal)) {
|
||||
throw new Error('Refusing to place the disposable validation home inside the primary home')
|
||||
}
|
||||
return { primaryHome, tempRoot, homeDir, userDataDir }
|
||||
@@ -528,7 +544,14 @@ async function main() {
|
||||
if (options.keep) {
|
||||
console.warn(`Credential-bearing disposable root kept at ${layout.tempRoot}`)
|
||||
} else {
|
||||
await rm(layout.tempRoot, { recursive: true })
|
||||
// Why: a detached codex descendant can briefly hold a Windows handle
|
||||
// in the disposable home; retry so cleanup never strands credentials.
|
||||
await rm(layout.tempRoot, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
maxRetries: 10,
|
||||
retryDelay: 250
|
||||
})
|
||||
console.log('Removed the credential-bearing disposable root.')
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, rm, symlink } from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
@@ -99,6 +99,35 @@ describe('Codex real-account validation harness', () => {
|
||||
expect(path.dirname(layout.tempRoot)).toBe(tempParent)
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'refuses a symlinked temp parent that resolves inside the primary home',
|
||||
async () => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), 'orca-symlink-guard-'))
|
||||
cleanupPaths.push(root)
|
||||
const primaryHome = path.join(root, 'primary-home')
|
||||
const insidePrimary = path.join(primaryHome, 'nested-temp')
|
||||
const link = path.join(root, 'temp-link')
|
||||
await mkdir(insidePrimary, { recursive: true })
|
||||
await symlink(insidePrimary, link)
|
||||
|
||||
const { error } = runValidationModule<{ error: string | null }>(
|
||||
`
|
||||
const { createValidationLayout } = await import(process.argv[1])
|
||||
try {
|
||||
const layout = await createValidationLayout({ primaryHome: process.argv[2] })
|
||||
console.log(JSON.stringify({ error: null, layout }))
|
||||
} catch (caught) {
|
||||
console.log(JSON.stringify({ error: caught.message }))
|
||||
}
|
||||
`,
|
||||
[primaryHome],
|
||||
{ ORCA_CODEX_VALIDATION_TEMP_PARENT: link }
|
||||
)
|
||||
|
||||
expect(error).toContain('Refusing to place the disposable validation root')
|
||||
}
|
||||
)
|
||||
|
||||
it('refuses a temp parent inside the primary home and points at the overrides', () => {
|
||||
// Why: matches Windows, where the default %TEMP% lives inside %USERPROFILE%.
|
||||
const { error } = runValidationModule<{ error: string | null }>(
|
||||
|
||||
Reference in New Issue
Block a user