fix(core): gracefully degrade global config dir path validation on EACCES

If the user's home directory or global `.gemini` config directory has
restrictive permissions that prevent `realpathSync` from succeeding, the
CLI should not crash. Instead, it now gracefully degrades by omitting
the global config directory from the valid security boundaries for plan
directories, allowing the CLI to continue operating securely within the
project root.
This commit is contained in:
Mahima Shanware
2026-04-07 05:12:30 +00:00
parent 432df7982c
commit 7c0cfd53df
2 changed files with 11 additions and 8 deletions
+10 -8
View File
@@ -2271,18 +2271,20 @@ export class Config implements McpContext, AgentLoopContext {
}
const realProjectRoot = this.storage.getRealProjectRoot();
let realGlobalGeminiDir: string;
let realGlobalGeminiDir: string | undefined;
try {
realGlobalGeminiDir = resolveToRealPath(Storage.getGlobalGeminiDir());
} catch {
realGlobalGeminiDir = path.resolve(Storage.getGlobalGeminiDir());
// If we can't securely resolve the global config dir (e.g. strict EACCES permissions on ~/),
// we gracefully degrade by not allowing it as a valid security boundary for plans.
realGlobalGeminiDir = undefined;
}
// 1. Lexical security check (before any filesystem mutation or return)
const lexicalPlansDir = path.resolve(plansDir);
if (
!isSubpath(realProjectRoot, lexicalPlansDir) &&
!isSubpath(realGlobalGeminiDir, lexicalPlansDir)
(!realGlobalGeminiDir || !isSubpath(realGlobalGeminiDir, lexicalPlansDir))
) {
throw new SecurityError(
`Security violation: Plan directory '${lexicalPlansDir}' is outside both the project root '${realProjectRoot}' and the global configuration directory.`,
@@ -2302,15 +2304,15 @@ export class Config implements McpContext, AgentLoopContext {
let realPlansDir: string;
try {
realPlansDir = resolveToRealPath(plansDir);
} catch {
// Fallback to path.resolve if the directory doesn't exist yet (e.g. mkdirSync failed)
// so that the security check can still be performed on the absolute path.
realPlansDir = path.resolve(plansDir);
} catch (e: unknown) {
throw new SecurityError(
`Security violation: Could not securely resolve plan directory '${plansDir}'. System error: ${e instanceof Error ? e.message : String(e)}`,
);
}
if (
!isSubpath(realProjectRoot, realPlansDir) &&
!isSubpath(realGlobalGeminiDir, realPlansDir)
(!realGlobalGeminiDir || !isSubpath(realGlobalGeminiDir, realPlansDir))
) {
throw new SecurityError(
`Security violation: Resolved plan directory '${realPlansDir}' is outside both the project root '${realProjectRoot}' and the global configuration directory.`,
@@ -237,6 +237,7 @@ describe('ToolRegistry', () => {
beforeEach(() => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.realpathSync).mockImplementation((p) => p.toString());
vi.mocked(fs.statSync).mockReturnValue({
isDirectory: () => true,
} as fs.Stats);