From 8c215c7a88df34dc125250c224c70cd307e087f1 Mon Sep 17 00:00:00 2001 From: Tommaso Sciortino Date: Fri, 3 Apr 2026 02:50:44 +0000 Subject: [PATCH] fix(core): remove broken PowerShell translation and fix native __write in Windows sandbox (#24571) --- .../core/src/sandbox/windows/GeminiSandbox.cs | 47 +++++++++++-------- .../windows/WindowsSandboxManager.test.ts | 34 ++++---------- .../sandbox/windows/WindowsSandboxManager.ts | 31 ++---------- 3 files changed, 40 insertions(+), 72 deletions(-) diff --git a/packages/core/src/sandbox/windows/GeminiSandbox.cs b/packages/core/src/sandbox/windows/GeminiSandbox.cs index acc7701e43..6275b701c4 100644 --- a/packages/core/src/sandbox/windows/GeminiSandbox.cs +++ b/packages/core/src/sandbox/windows/GeminiSandbox.cs @@ -158,8 +158,8 @@ public class GeminiSandbox { static int Main(string[] args) { if (args.Length < 3) { - Console.WriteLine("Usage: GeminiSandbox.exe [--forbidden-manifest ] [args...]"); - Console.WriteLine("Internal commands: __read , __write "); + Console.Error.WriteLine("Usage: GeminiSandbox.exe [--forbidden-manifest ] [args...]"); + Console.Error.WriteLine("Internal commands: __read , __write "); 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 action) { if (!ImpersonateLoggedOnUser(hToken)) { - Console.WriteLine("Error: ImpersonateLoggedOnUser failed (" + Marshal.GetLastWin32Error() + ")"); + Console.Error.WriteLine("Error: ImpersonateLoggedOnUser failed (" + Marshal.GetLastWin32Error() + ")"); return 1; } try { diff --git a/packages/core/src/sandbox/windows/WindowsSandboxManager.test.ts b/packages/core/src/sandbox/windows/WindowsSandboxManager.test.ts index 7bbe724c6a..a709592d02 100644 --- a/packages/core/src/sandbox/windows/WindowsSandboxManager.test.ts +++ b/packages/core/src/sandbox/windows/WindowsSandboxManager.test.ts @@ -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); }); }); diff --git a/packages/core/src/sandbox/windows/WindowsSandboxManager.ts b/packages/core/src/sandbox/windows/WindowsSandboxManager.ts index 6484d9406c..3328c2b918 100644 --- a/packages/core/src/sandbox/windows/WindowsSandboxManager.ts +++ b/packages/core/src/sandbox/windows/WindowsSandboxManager.ts @@ -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,