stablyai/orca · critical

Access denied: invalid submodule path

Error message

Access denied: invalid submodule path

What it means

resolveSubmoduleWorktreePath builds a submodule's own worktree path from a parent worktree plus a relative submodule path. It throws this message when submodulePath is empty, contains a NUL byte, or is an absolute path — none of those can be a valid relative submodule entry, and accepting them would let a caller target arbitrary filesystem locations. This is the input-shape guard; the escape guard at :508 is the second layer.

Source

Thrown at src/main/git/status.ts:503

            timeout: GIT_BRANCH_LINE_TOTAL_TIMEOUT_MS
          }).then((result) => result.stdout),
        ...(options.signal ? { signal: options.signal } : {})
      })
  }
}

function getStatusLineStatsCacheKey(worktreePath: string, options: GitRuntimeOptions = {}): string {
  // Why: identical paths can map to different WSL-distro filesystems, so key stats by Git's execution host.
  return `${options.wslDistro ?? 'native'}\0${worktreePath}`
}

/**
 * Resolve a submodule's own worktree path from a parent worktree + relative
 * submodule path, rejecting anything that escapes the parent.
 */
export function resolveSubmoduleWorktreePath(worktreePath: string, submodulePath: string): string {
  if (!submodulePath || submodulePath.includes('\0') || path.isAbsolute(submodulePath)) {
    throw new Error('Access denied: invalid submodule path')
  }
  const resolved = path.resolve(worktreePath, submodulePath)
  const rel = path.relative(worktreePath, resolved)
  if (!rel || rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
    throw new Error('Access denied: submodule path escapes the selected worktree')
  }
  return resolved
}

/**
 * Run a plain status inside a submodule's own worktree (lazy "expand submodule"
 * flow). Entry paths are relative to the submodule root; the renderer prefixes them.
 */
export async function getSubmoduleStatus(
  worktreePath: string,
  submodulePath: string,
  options: GetStatusOptions & { staged?: boolean } = {}
): Promise<GitStatusResult> {

View on GitHub (pinned to 1136503c6a)

Solutions

  1. Pass a non-empty, relative submodule path as it appears in .gitmodules / git submodule status.
  2. Reject empty or NUL-containing paths at the caller before invoking the function.
  3. If you have an absolute path, compute the relative path against the parent worktree first (path.relative).
  4. Do not feed user-typed arbitrary paths directly — source them from the submodule listing.

Example fix

// before
resolveSubmoduleWorktreePath(worktreePath, rawEntry)

// after: validate shape and relativize
if (!rawEntry || rawEntry.includes('\0') || path.isAbsolute(rawEntry)) {
  throw new Error('Submodule path must be a non-empty relative path.')
}
resolveSubmoduleWorktreePath(worktreePath, rawEntry)
Defensive patterns

Strategy: validation

Validate before calling

import path from 'node:path'

function isValidSubmodulePath(submodulePath: string): boolean {
  return Boolean(submodulePath) && !submodulePath.includes('\0') && !path.isAbsolute(submodulePath)
}

if (!isValidSubmodulePath(submodulePath)) throw new Error('Submodule path must be a non-empty relative path.')

Type guard

function isInvalidSubmodulePath(error: unknown): boolean {
  return error instanceof Error && error.message === 'Access denied: invalid submodule path'
}

Try / catch

if (!isValidSubmodulePath(submodulePath)) {
  throw new Error('Refuse to load submodule: path is empty, absolute, or contains a NUL byte.')
}
try { return resolveSubmoduleWorktreePath(worktreePath, submodulePath) }
catch (error) { if (isInvalidSubmodulePath(error)) { showStatus('Cannot open submodule: invalid path.'); return null } throw error }

Prevention

When it happens

Trigger: Calling resolveSubmoduleWorktreePath(worktreePath, submodulePath) (directly, or via getSubmoduleStatus / loadDiff routing) with submodulePath = '', a path containing '\0', or an absolute path like '/etc' or 'C:\Windows'.

Common situations: A UI entry that fed an empty submodule path because the selection was not yet ready; a malformed .gitmodules entry that produced an empty path; adversarial or corrupted path input from an untrusted source; a path that was pre-resolved to absolute by a caller that did not realise the function expects a relative path.

Understand the failure class

Related errors


AI-assisted analysis of stablyai/orca@1136503c6a (2026-08-12). Data as JSON: /api/errors/4c9944a019086d7f. Report an issue: GitHub.