fix(codex): validate linked worktree trust roots

This commit is contained in:
Brennan Benson
2026-07-20 16:20:35 -07:00
parent 9c5bffd66e
commit dd97923bb6
4 changed files with 61 additions and 18 deletions
+33 -2
View File
@@ -137,7 +137,7 @@ describe('markCopilotFolderTrusted', () => {
})
describe('markCodexProjectTrusted', () => {
it('trusts the main repository root for a linked worktree', () => {
it('trusts the main repository root for a linked worktree without reading commondir', () => {
const fixtureRoot = mkdtempSync(join(tmpdir(), 'orca-codex-linked-ws-'))
const repository = join(fixtureRoot, 'repo')
const workspace = join(fixtureRoot, 'worktrees', 'feature')
@@ -146,7 +146,7 @@ describe('markCodexProjectTrusted', () => {
mkdirSync(worktreeGitDir, { recursive: true })
mkdirSync(workspace, { recursive: true })
writeFileSync(join(workspace, '.git'), `gitdir: ${worktreeGitDir}\n`, 'utf-8')
writeFileSync(join(worktreeGitDir, 'commondir'), '../..\n', 'utf-8')
writeFileSync(join(worktreeGitDir, 'gitdir'), join(workspace, '.git'), 'utf-8')
markCodexProjectTrusted(workspace)
@@ -171,6 +171,37 @@ describe('markCodexProjectTrusted', () => {
}
})
it('does not broaden trust through arbitrary or adversarial Git metadata', () => {
const fixtureRoot = mkdtempSync(join(tmpdir(), 'orca-codex-untrusted-gitdir-'))
const workspace = join(fixtureRoot, 'workspace')
const arbitraryGitDir = join(fixtureRoot, 'metadata', 'feature')
const unrelatedRoot = join(fixtureRoot, 'unrelated')
try {
mkdirSync(arbitraryGitDir, { recursive: true })
mkdirSync(workspace, { recursive: true })
mkdirSync(unrelatedRoot, { recursive: true })
writeFileSync(join(workspace, '.git'), `gitdir: ${arbitraryGitDir}\n`, 'utf-8')
writeFileSync(join(arbitraryGitDir, 'commondir'), join(unrelatedRoot, '.git'), 'utf-8')
markCodexProjectTrusted(workspace)
const structuredGitDir = join(unrelatedRoot, '.git', 'worktrees', 'feature')
mkdirSync(structuredGitDir, { recursive: true })
writeFileSync(join(workspace, '.git'), `gitdir: ${structuredGitDir}\n`, 'utf-8')
writeFileSync(join(structuredGitDir, 'gitdir'), join(unrelatedRoot, '.git'), 'utf-8')
markCodexProjectTrusted(workspace)
const written = readFileSync(join(testState.fakeHomeDir, '.codex', 'config.toml'), 'utf-8')
expect(written).toContain(
`[projects."${escapeTomlBasicString(realpathSync.native(workspace))}"]`
)
expect(written).not.toContain(
`[projects."${escapeTomlBasicString(realpathSync.native(unrelatedRoot))}"]`
)
} finally {
rmSync(fixtureRoot, { recursive: true, force: true })
}
})
it('writes ~/.codex/config.toml with the project marked trusted', () => {
const workspace = mkdtempSync(join(tmpdir(), 'orca-codex-ws-'))
try {
+25 -9
View File
@@ -1,6 +1,6 @@
import { existsSync, mkdirSync, readFileSync, realpathSync } from 'node:fs'
import { homedir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { basename, dirname, join, resolve } from 'node:path'
import { writeFileAtomically } from './codex-accounts/fs-utils'
import { getOrcaManagedCodexHomePath } from './codex/codex-home-paths'
import { upsertProjectTrustLevel } from './codex/config-toml-trust'
@@ -120,18 +120,34 @@ export function markCodexProjectTrusted(workspacePath: string): void {
function resolveCodexProjectTrustRoot(workspacePath: string): string {
const absPath = canonicalize(workspacePath)
try {
const gitDirReference = readFileSync(join(absPath, '.git'), 'utf-8')
const gitDirMatch = /^gitdir:\s*(.+?)\s*$/im.exec(gitDirReference)
if (!gitDirMatch?.[1]) {
const gitDirReference = readFileSync(join(absPath, '.git'), 'utf-8').trim()
if (!gitDirReference.startsWith('gitdir:')) {
return absPath
}
const gitDir = resolve(absPath, gitDirMatch[1])
const commonDirReference = readFileSync(join(gitDir, 'commondir'), 'utf-8').trim()
if (!commonDirReference) {
const gitDirPath = gitDirReference.slice('gitdir:'.length).trim()
if (!gitDirPath) {
return absPath
}
// Why: Codex resolves linked-worktree trust through commondir to the main repo root.
return canonicalize(dirname(resolve(gitDir, commonDirReference)))
const gitDir = resolve(absPath, gitDirPath)
const worktreesDir = dirname(gitDir)
if (basename(worktreesDir) !== 'worktrees') {
return absPath
}
// Why: workspace-controlled .git metadata must not broaden trust without Git's reciprocal link.
const gitDirBacklink = readFileSync(join(gitDir, 'gitdir'), 'utf-8').trim()
if (!gitDirBacklink) {
return absPath
}
const resolvedBacklink = resolve(gitDir, gitDirBacklink)
const workspaceGitFile = join(absPath, '.git')
if (
resolvedBacklink !== workspaceGitFile &&
canonicalize(resolvedBacklink) !== canonicalize(workspaceGitFile)
) {
return absPath
}
// Why: mirror Codex's validated .git/worktrees/<name> traversal instead of trusting arbitrary commondir contents.
return canonicalize(dirname(dirname(worktreesDir)))
} catch {
return absPath
}
+1 -1
View File
@@ -619,7 +619,7 @@ function getLocalOrcaCodexHomeEnvKeysToDelete(env: Record<string, string>): stri
return keysToDelete
}
type GetSelectedCodexHomePath = (
export type GetSelectedCodexHomePath = (
target?: CodexAccountSelectionTarget,
launchEnv?: NodeJS.ProcessEnv,
launchContext?: { workspacePath?: string; launchAgent?: TuiAgent }
@@ -12,7 +12,7 @@ import type {
import { registerRepoHandlers } from '../ipc/repos'
import { registerWorktreeHandlers } from '../ipc/worktrees'
import { registerWorkspaceCleanupHandlers } from '../ipc/workspace-cleanup'
import { getLocalPtyProvider, registerPtyHandlers } from '../ipc/pty'
import { getLocalPtyProvider, registerPtyHandlers, type GetSelectedCodexHomePath } from '../ipc/pty'
import { registerDaemonManagementHandlers } from '../ipc/pty-management'
import { registerSshHandlers } from '../ipc/ssh'
import { registerRemoteWorkspaceHandlers } from '../ipc/remote-workspace'
@@ -39,7 +39,6 @@ import type { RuntimeMobileSessionTabMove } from '../../shared/runtime-types'
import { isNativeFileDropPayload, type NativeFileDropPayload } from '../../shared/native-file-drop'
import { requestMobileMarkdownFromRenderer } from './mobile-markdown-request-relay'
import { requestTerminalTabCloseFromRenderer } from './terminal-tab-close-request-relay'
import type { CodexAccountSelectionTarget } from '../codex-accounts/runtime-selection'
import type { ClaudeAccountSelectionTarget } from '../claude-accounts/runtime-selection'
import { runWorktreeChangeInvalidators } from '../ipc/worktree-change-invalidators'
import {
@@ -66,10 +65,7 @@ export function attachMainWindowServices(
mainWindow: BrowserWindow,
store: Store,
runtime: OrcaRuntimeService,
getSelectedCodexHomePath?: (
target?: CodexAccountSelectionTarget,
launchEnv?: NodeJS.ProcessEnv
) => string | null,
getSelectedCodexHomePath?: GetSelectedCodexHomePath,
prepareClaudeAuth?: (
target?: ClaudeAccountSelectionTarget
) => Promise<ClaudeRuntimeAuthPreparation>,