fix(core): remove broken PowerShell translation and fix native __write in Windows sandbox (#24571)

This commit is contained in:
Tommaso Sciortino
2026-04-03 02:50:44 +00:00
committed by GitHub
parent fe4fcf89d9
commit 8c215c7a88
3 changed files with 40 additions and 72 deletions

View File

@@ -158,8 +158,8 @@ public class GeminiSandbox {
static int Main(string[] args) {
if (args.Length < 3) {
Console.WriteLine("Usage: GeminiSandbox.exe <network:0|1> <cwd> [--forbidden-manifest <path>] <command> [args...]");
Console.WriteLine("Internal commands: __read <path>, __write <path>");
Console.Error.WriteLine("Usage: GeminiSandbox.exe <network:0|1> <cwd> [--forbidden-manifest <path>] <command> [args...]");
Console.Error.WriteLine("Internal commands: __read <path>, __write <path>");
return 1;
}
@@ -183,7 +183,7 @@ public class GeminiSandbox {
}
if (argIndex >= args.Length) {
Console.WriteLine("Error: Missing command");
Console.Error.WriteLine("Error: Missing command");
return 1;
}
@@ -196,13 +196,13 @@ public class GeminiSandbox {
try {
// 1. Duplicate Primary Token
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, out hToken)) {
Console.WriteLine("Error: OpenProcessToken failed (" + Marshal.GetLastWin32Error() + ")");
Console.Error.WriteLine("Error: OpenProcessToken failed (" + Marshal.GetLastWin32Error() + ")");
return 1;
}
// Create a restricted token to strip administrative privileges
if (!CreateRestrictedToken(hToken, DISABLE_MAX_PRIVILEGE, 0, IntPtr.Zero, 0, IntPtr.Zero, 0, IntPtr.Zero, out hRestrictedToken)) {
Console.WriteLine("Error: CreateRestrictedToken failed (" + Marshal.GetLastWin32Error() + ")");
Console.Error.WriteLine("Error: CreateRestrictedToken failed (" + Marshal.GetLastWin32Error() + ")");
return 1;
}
@@ -217,7 +217,7 @@ public class GeminiSandbox {
try {
Marshal.StructureToPtr(tml, pTml, false);
if (!SetTokenInformation(hRestrictedToken, TokenIntegrityLevel, pTml, (uint)tmlSize)) {
Console.WriteLine("Error: SetTokenInformation failed (" + Marshal.GetLastWin32Error() + ")");
Console.Error.WriteLine("Error: SetTokenInformation failed (" + Marshal.GetLastWin32Error() + ")");
return 1;
}
} finally {
@@ -250,7 +250,7 @@ public class GeminiSandbox {
// 4. Handle Internal Commands or External Process
if (command == "__read") {
if (argIndex + 1 >= args.Length) {
Console.WriteLine("Error: Missing path for __read");
Console.Error.WriteLine("Error: Missing path for __read");
return 1;
}
string path = args[argIndex + 1];
@@ -269,24 +269,31 @@ public class GeminiSandbox {
});
} else if (command == "__write") {
if (argIndex + 1 >= args.Length) {
Console.WriteLine("Error: Missing path for __write");
Console.Error.WriteLine("Error: Missing path for __write");
return 1;
}
string path = args[argIndex + 1];
CheckForbidden(path, forbiddenPaths);
return RunInImpersonation(hRestrictedToken, () => {
try {
using (StreamReader reader = new StreamReader(Console.OpenStandardInput(), System.Text.Encoding.UTF8))
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
using (StreamWriter writer = new StreamWriter(fs, System.Text.Encoding.UTF8)) {
writer.Write(reader.ReadToEnd());
try {
using (MemoryStream ms = new MemoryStream()) {
// Buffer stdin before impersonation (as restricted token can't read the inherited pipe).
using (Stream stdin = Console.OpenStandardInput()) {
stdin.CopyTo(ms);
}
return 0;
} catch (Exception e) {
Console.Error.WriteLine("Error writing file: " + e.Message);
return 1;
return RunInImpersonation(hRestrictedToken, () => {
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None)) {
ms.Position = 0;
ms.CopyTo(fs);
}
return 0;
});
}
});
} catch (Exception e) {
Console.Error.WriteLine("Error during __write: " + e.Message);
return 1;
}
}
// External Process
@@ -337,7 +344,7 @@ public class GeminiSandbox {
private static int RunInImpersonation(IntPtr hToken, Func<int> action) {
if (!ImpersonateLoggedOnUser(hToken)) {
Console.WriteLine("Error: ImpersonateLoggedOnUser failed (" + Marshal.GetLastWin32Error() + ")");
Console.Error.WriteLine("Error: ImpersonateLoggedOnUser failed (" + Marshal.GetLastWin32Error() + ")");
return 1;
}
try {

View File

@@ -495,7 +495,7 @@ describe('WindowsSandboxManager', () => {
}
});
it('should translate __write to PowerShell safely using environment variables', async () => {
it('should pass __write directly to native helper', async () => {
const filePath = path.join(testCwd, 'test.txt');
fs.writeFileSync(filePath, '');
const req: SandboxRequest = {
@@ -508,16 +508,11 @@ describe('WindowsSandboxManager', () => {
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);
expect(result.args[4]).toBe('__write');
expect(result.args[5]).toBe(filePath);
});
it('should safely handle special characters in __write path using environment variables', async () => {
it('should safely handle special characters in __write path', async () => {
const maliciousPath = path.join(testCwd, 'foo"; echo bar; ".txt');
fs.writeFileSync(maliciousPath, '');
const req: SandboxRequest = {
@@ -529,16 +524,12 @@ describe('WindowsSandboxManager', () => {
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);
// Native commands pass arguments directly; the binary handles quoting via QuoteArgument
expect(result.args[4]).toBe('__write');
expect(result.args[5]).toBe(maliciousPath);
});
it('should translate __read to PowerShell safely using environment variables', async () => {
it('should pass __read directly to native helper', async () => {
const filePath = path.join(testCwd, 'test.txt');
fs.writeFileSync(filePath, 'hello');
const req: SandboxRequest = {
@@ -550,12 +541,7 @@ describe('WindowsSandboxManager', () => {
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);
expect(result.args[4]).toBe('__read');
expect(result.args[5]).toBe(filePath);
});
});

View File

@@ -217,32 +217,10 @@ 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;
const command = req.command;
const args = req.args;
// 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 }',
];
}
// Native commands __read and __write are passed directly to GeminiSandbox.exe
const isYolo = this.options.modeConfig?.yolo ?? false;
@@ -427,9 +405,6 @@ export class WindowsSandboxManager implements SandboxManager {
];
const finalEnv = { ...sanitizedEnv };
if (targetPathEnv !== undefined) {
finalEnv['GEMINI_TARGET_PATH'] = targetPathEnv;
}
return {
program,