mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-22 02:54:31 -07:00
fix(core): resolve Plan Mode deadlock during plan file creation due to sandbox restrictions (#24047)
This commit is contained in:
@@ -125,7 +125,9 @@ describe('WindowsSandboxManager', () => {
|
||||
});
|
||||
|
||||
it('should handle persistent permissions from policyManager', async () => {
|
||||
const persistentPath = path.resolve('/persistent/path');
|
||||
const persistentPath = path.join(testCwd, 'persistent_path');
|
||||
fs.mkdirSync(persistentPath, { recursive: true });
|
||||
|
||||
const mockPolicyManager = {
|
||||
getCommandPermissions: vi.fn().mockReturnValue({
|
||||
fileSystem: { write: [persistentPath] },
|
||||
@@ -466,4 +468,68 @@ describe('WindowsSandboxManager', () => {
|
||||
fs.rmSync(conflictPath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should translate __write to PowerShell safely using environment variables', async () => {
|
||||
const filePath = path.join(testCwd, 'test.txt');
|
||||
fs.writeFileSync(filePath, '');
|
||||
const req: SandboxRequest = {
|
||||
command: '__write',
|
||||
args: [filePath],
|
||||
cwd: testCwd,
|
||||
env: {},
|
||||
};
|
||||
|
||||
const result = await manager.prepareCommand(req);
|
||||
|
||||
// [network, cwd, --forbidden-manifest, manifestPath, command, ...args]
|
||||
expect(result.args[4]).toBe('PowerShell.exe');
|
||||
expect(result.args[7]).toBe('-Command');
|
||||
const psCommand = result.args[8];
|
||||
expect(psCommand).toBe(
|
||||
'& { $Input | Out-File -FilePath $env:GEMINI_TARGET_PATH -Encoding utf8 }',
|
||||
);
|
||||
expect(result.env['GEMINI_TARGET_PATH']).toBe(filePath);
|
||||
});
|
||||
|
||||
it('should safely handle special characters in __write path using environment variables', async () => {
|
||||
const maliciousPath = path.join(testCwd, 'foo"; echo bar; ".txt');
|
||||
fs.writeFileSync(maliciousPath, '');
|
||||
const req: SandboxRequest = {
|
||||
command: '__write',
|
||||
args: [maliciousPath],
|
||||
cwd: testCwd,
|
||||
env: {},
|
||||
};
|
||||
|
||||
const result = await manager.prepareCommand(req);
|
||||
|
||||
expect(result.args[4]).toBe('PowerShell.exe');
|
||||
const psCommand = result.args[8];
|
||||
expect(psCommand).toBe(
|
||||
'& { $Input | Out-File -FilePath $env:GEMINI_TARGET_PATH -Encoding utf8 }',
|
||||
);
|
||||
// The malicious path should be injected safely via environment variable, not interpolated in args
|
||||
expect(result.env['GEMINI_TARGET_PATH']).toBe(maliciousPath);
|
||||
});
|
||||
|
||||
it('should translate __read to PowerShell safely using environment variables', async () => {
|
||||
const filePath = path.join(testCwd, 'test.txt');
|
||||
fs.writeFileSync(filePath, 'hello');
|
||||
const req: SandboxRequest = {
|
||||
command: '__read',
|
||||
args: [filePath],
|
||||
cwd: testCwd,
|
||||
env: {},
|
||||
};
|
||||
|
||||
const result = await manager.prepareCommand(req);
|
||||
|
||||
expect(result.args[4]).toBe('PowerShell.exe');
|
||||
expect(result.args[7]).toBe('-Command');
|
||||
const psCommand = result.args[8];
|
||||
expect(psCommand).toBe(
|
||||
'& { Get-Content -LiteralPath $env:GEMINI_TARGET_PATH -Raw }',
|
||||
);
|
||||
expect(result.env['GEMINI_TARGET_PATH']).toBe(filePath);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -212,8 +212,35 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
// Reject override attempts in plan mode
|
||||
verifySandboxOverrides(allowOverrides, req.policy);
|
||||
|
||||
let command = req.command;
|
||||
let args = req.args;
|
||||
let targetPathEnv: string | undefined;
|
||||
|
||||
// Translate virtual commands for sandboxed file system access
|
||||
if (command === '__read') {
|
||||
// Use PowerShell for safe argument passing via env var
|
||||
targetPathEnv = args[0] || '';
|
||||
command = 'PowerShell.exe';
|
||||
args = [
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-Command',
|
||||
'& { Get-Content -LiteralPath $env:GEMINI_TARGET_PATH -Raw }',
|
||||
];
|
||||
} else if (command === '__write') {
|
||||
// Use PowerShell for piping stdin to a file via env var
|
||||
targetPathEnv = args[0] || '';
|
||||
command = 'PowerShell.exe';
|
||||
args = [
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-Command',
|
||||
'& { $Input | Out-File -FilePath $env:GEMINI_TARGET_PATH -Encoding utf8 }',
|
||||
];
|
||||
}
|
||||
|
||||
// Fetch persistent approvals for this command
|
||||
const commandName = await getCommandName(req.command, req.args);
|
||||
const commandName = await getCommandName(command, args);
|
||||
const persistentPermissions = allowOverrides
|
||||
? this.options.policyManager?.getCommandPermissions(commandName)
|
||||
: undefined;
|
||||
@@ -243,7 +270,7 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
}
|
||||
|
||||
const defaultNetwork =
|
||||
this.options.modeConfig?.network || req.policy?.networkAccess || false;
|
||||
this.options.modeConfig?.network ?? req.policy?.networkAccess ?? false;
|
||||
const networkAccess = defaultNetwork || mergedAdditional.network;
|
||||
|
||||
// 1. Handle filesystem permissions for Low Integrity
|
||||
@@ -251,8 +278,8 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
// If not in readonly mode OR it's a strictly approved pipeline, allow workspace writes
|
||||
const isApproved = allowOverrides
|
||||
? await isStrictlyApproved(
|
||||
req.command,
|
||||
req.args,
|
||||
command,
|
||||
args,
|
||||
this.options.modeConfig?.approvedTools,
|
||||
)
|
||||
: false;
|
||||
@@ -261,24 +288,48 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
await this.grantLowIntegrityAccess(this.options.workspace);
|
||||
}
|
||||
|
||||
// Grant "Low Mandatory Level" access to includeDirectories.
|
||||
const includeDirs = sanitizePaths(this.options.includeDirectories) || [];
|
||||
for (const includeDir of includeDirs) {
|
||||
await this.grantLowIntegrityAccess(includeDir);
|
||||
}
|
||||
|
||||
// Grant "Low Mandatory Level" read/write access to allowedPaths.
|
||||
const allowedPaths = sanitizePaths(req.policy?.allowedPaths) || [];
|
||||
for (const allowedPath of allowedPaths) {
|
||||
await this.grantLowIntegrityAccess(allowedPath);
|
||||
const resolved = await tryRealpath(allowedPath);
|
||||
if (!fs.existsSync(resolved)) {
|
||||
throw new Error(
|
||||
`Sandbox request rejected: Allowed path does not exist: ${resolved}. ` +
|
||||
'On Windows, granular sandbox access can only be granted to existing paths to avoid broad parent directory permissions.',
|
||||
);
|
||||
}
|
||||
await this.grantLowIntegrityAccess(resolved);
|
||||
}
|
||||
|
||||
// Grant "Low Mandatory Level" write access to additional permissions write paths.
|
||||
const additionalWritePaths =
|
||||
sanitizePaths(mergedAdditional.fileSystem?.write) || [];
|
||||
for (const writePath of additionalWritePaths) {
|
||||
await this.grantLowIntegrityAccess(writePath);
|
||||
const resolved = await tryRealpath(writePath);
|
||||
if (!fs.existsSync(resolved)) {
|
||||
throw new Error(
|
||||
`Sandbox request rejected: Additional write path does not exist: ${resolved}. ` +
|
||||
'On Windows, granular sandbox access can only be granted to existing paths to avoid broad parent directory permissions.',
|
||||
);
|
||||
}
|
||||
await this.grantLowIntegrityAccess(resolved);
|
||||
}
|
||||
|
||||
// 2. Collect secret files and apply protective ACLs
|
||||
// On Windows, we explicitly deny access to secret files for Low Integrity
|
||||
// processes to ensure they cannot be read or written.
|
||||
const secretsToBlock: string[] = [];
|
||||
const searchDirs = new Set([this.options.workspace, ...allowedPaths]);
|
||||
const searchDirs = new Set([
|
||||
this.options.workspace,
|
||||
...allowedPaths,
|
||||
...includeDirs,
|
||||
]);
|
||||
for (const dir of searchDirs) {
|
||||
try {
|
||||
// We use maxDepth 3 to catch common nested secrets while keeping performance high.
|
||||
@@ -352,19 +403,24 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
// GeminiSandbox.exe <network:0|1> <cwd> --forbidden-manifest <path> <command> [args...]
|
||||
const program = this.helperPath;
|
||||
|
||||
const args = [
|
||||
const finalArgs = [
|
||||
networkAccess ? '1' : '0',
|
||||
req.cwd,
|
||||
'--forbidden-manifest',
|
||||
manifestPath,
|
||||
req.command,
|
||||
...req.args,
|
||||
command,
|
||||
...args,
|
||||
];
|
||||
|
||||
const finalEnv = { ...sanitizedEnv };
|
||||
if (targetPathEnv !== undefined) {
|
||||
finalEnv['GEMINI_TARGET_PATH'] = targetPathEnv;
|
||||
}
|
||||
|
||||
return {
|
||||
program,
|
||||
args,
|
||||
env: sanitizedEnv,
|
||||
args: finalArgs,
|
||||
env: finalEnv,
|
||||
cwd: req.cwd,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user