From ae123c547c609f5b309af9f7a76d699deccbcc39 Mon Sep 17 00:00:00 2001 From: Gal Zahavi <38544478+galz10@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:14:35 -0700 Subject: [PATCH] fix(sandbox): implement Windows Mandatory Integrity Control for GeminiSandbox (#24057) --- .../src/policy/policies/sandbox-default.toml | 5 +-- .../src/sandbox/linux/LinuxSandboxManager.ts | 2 +- .../src/sandbox/macos/MacOsSandboxManager.ts | 2 +- .../core/src/sandbox/windows/GeminiSandbox.cs | 32 ++++++++++++++++--- .../windows/WindowsSandboxManager.test.ts | 14 ++++---- .../sandbox/windows/WindowsSandboxManager.ts | 16 ++++++---- .../core/src/services/sandboxManager.test.ts | 9 ++++++ packages/core/src/utils/shell-utils.ts | 18 +++++++++-- 8 files changed, 75 insertions(+), 23 deletions(-) diff --git a/packages/core/src/policy/policies/sandbox-default.toml b/packages/core/src/policy/policies/sandbox-default.toml index 0d8467d596..933d85cf9e 100644 --- a/packages/core/src/policy/policies/sandbox-default.toml +++ b/packages/core/src/policy/policies/sandbox-default.toml @@ -7,13 +7,14 @@ allowOverrides = false [modes.default] network = false readonly = true -approvedTools = [] +approvedTools = ['cat', 'ls', 'grep', 'head', 'tail', 'less', 'Get-Content', 'dir', 'type', 'findstr', 'Get-ChildItem', 'echo'] allowOverrides = true [modes.accepting_edits] network = false readonly = false -approvedTools = ['sed', 'grep', 'awk', 'perl', 'cat', 'echo'] +approvedTools = ['sed', 'grep', 'awk', 'perl', 'cat', 'echo', 'Add-Content', 'Set-Content'] allowOverrides = true [commands] + diff --git a/packages/core/src/sandbox/linux/LinuxSandboxManager.ts b/packages/core/src/sandbox/linux/LinuxSandboxManager.ts index 5543a9024b..2167e28740 100644 --- a/packages/core/src/sandbox/linux/LinuxSandboxManager.ts +++ b/packages/core/src/sandbox/linux/LinuxSandboxManager.ts @@ -187,7 +187,7 @@ export class LinuxSandboxManager implements SandboxManager { : false; const workspaceWrite = !isReadonlyMode || isApproved; const networkAccess = - this.options.modeConfig?.network ?? req.policy?.networkAccess ?? false; + this.options.modeConfig?.network || req.policy?.networkAccess || false; const persistentPermissions = allowOverrides ? this.options.policyManager?.getCommandPermissions(commandName) diff --git a/packages/core/src/sandbox/macos/MacOsSandboxManager.ts b/packages/core/src/sandbox/macos/MacOsSandboxManager.ts index 0c147ea03b..212fafed83 100644 --- a/packages/core/src/sandbox/macos/MacOsSandboxManager.ts +++ b/packages/core/src/sandbox/macos/MacOsSandboxManager.ts @@ -78,7 +78,7 @@ export class MacOsSandboxManager implements SandboxManager { const workspaceWrite = !isReadonlyMode || isApproved; const defaultNetwork = - this.options.modeConfig?.network ?? req.policy?.networkAccess ?? false; + this.options.modeConfig?.network || req.policy?.networkAccess || false; // Fetch persistent approvals for this command const commandName = await getCommandName(req.command, req.args); diff --git a/packages/core/src/sandbox/windows/GeminiSandbox.cs b/packages/core/src/sandbox/windows/GeminiSandbox.cs index eff5ec703a..acc7701e43 100644 --- a/packages/core/src/sandbox/windows/GeminiSandbox.cs +++ b/packages/core/src/sandbox/windows/GeminiSandbox.cs @@ -58,6 +58,13 @@ public class GeminiSandbox { public ulong OtherTransferCount; } + [StructLayout(LayoutKind.Sequential)] + struct JOBOBJECT_NET_RATE_CONTROL_INFORMATION { + public ulong MaxBandwidth; + public uint ControlFlags; + public byte DscpTag; + } + [DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName); @@ -70,6 +77,9 @@ public class GeminiSandbox { [DllImport("advapi32.dll", SetLastError = true)] static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle); + [DllImport("advapi32.dll", SetLastError = true)] + static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, IntPtr lpTokenAttributes, uint ImpersonationLevel, uint TokenType, out IntPtr phNewToken); + [DllImport("advapi32.dll", SetLastError = true)] static extern bool CreateRestrictedToken(IntPtr ExistingTokenHandle, uint Flags, uint DisableSidCount, IntPtr SidsToDisable, uint DeletePrivilegeCount, IntPtr PrivilegesToDelete, uint RestrictedSidCount, IntPtr SidsToRestrict, out IntPtr NewTokenHandle); @@ -143,6 +153,8 @@ public class GeminiSandbox { private const int TokenIntegrityLevel = 25; private const uint SE_GROUP_INTEGRITY = 0x00000020; + private const uint TOKEN_ALL_ACCESS = 0xF01FF; + private const uint DISABLE_MAX_PRIVILEGE = 0x1; static int Main(string[] args) { if (args.Length < 3) { @@ -182,14 +194,14 @@ public class GeminiSandbox { IntPtr lowIntegritySid = IntPtr.Zero; try { - // 1. Create Restricted Token - if (!OpenProcessToken(GetCurrentProcess(), 0x0002 /* TOKEN_DUPLICATE */ | 0x0008 /* TOKEN_QUERY */ | 0x0080 /* TOKEN_ADJUST_DEFAULT */, out hToken)) { + // 1. Duplicate Primary Token + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, out hToken)) { Console.WriteLine("Error: OpenProcessToken failed (" + Marshal.GetLastWin32Error() + ")"); return 1; } - // Flags: 0x1 (DISABLE_MAX_PRIVILEGE) - if (!CreateRestrictedToken(hToken, 1, 0, IntPtr.Zero, 0, IntPtr.Zero, 0, IntPtr.Zero, out hRestrictedToken)) { + // 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() + ")"); return 1; } @@ -223,6 +235,18 @@ public class GeminiSandbox { SetInformationJobObject(hJob, 9 /* JobObjectExtendedLimitInformation */, lpJobLimits, (uint)Marshal.SizeOf(jobLimits)); Marshal.FreeHGlobal(lpJobLimits); + if (!networkAccess) { + JOBOBJECT_NET_RATE_CONTROL_INFORMATION netLimits = new JOBOBJECT_NET_RATE_CONTROL_INFORMATION(); + netLimits.MaxBandwidth = 1; + netLimits.ControlFlags = 0x1 | 0x2; // ENABLE | MAX_BANDWIDTH + netLimits.DscpTag = 0; + + IntPtr lpNetLimits = Marshal.AllocHGlobal(Marshal.SizeOf(netLimits)); + Marshal.StructureToPtr(netLimits, lpNetLimits, false); + SetInformationJobObject(hJob, 32 /* JobObjectNetRateControlInformation */, lpNetLimits, (uint)Marshal.SizeOf(netLimits)); + Marshal.FreeHGlobal(lpNetLimits); + } + // 4. Handle Internal Commands or External Process if (command == "__read") { if (argIndex + 1 >= args.Length) { diff --git a/packages/core/src/sandbox/windows/WindowsSandboxManager.test.ts b/packages/core/src/sandbox/windows/WindowsSandboxManager.test.ts index 9fb1522000..79e9f50ebf 100644 --- a/packages/core/src/sandbox/windows/WindowsSandboxManager.test.ts +++ b/packages/core/src/sandbox/windows/WindowsSandboxManager.test.ts @@ -158,7 +158,7 @@ describe('WindowsSandboxManager', () => { expect(icaclsArgs).toContainEqual([ persistentPath, '/setintegritylevel', - 'Low', + '(OI)(CI)Low', ]); }); @@ -227,13 +227,13 @@ describe('WindowsSandboxManager', () => { expect(icaclsArgs).toContainEqual([ path.resolve(testCwd), '/setintegritylevel', - 'Low', + '(OI)(CI)Low', ]); expect(icaclsArgs).toContainEqual([ path.resolve(allowedPath), '/setintegritylevel', - 'Low', + '(OI)(CI)Low', ]); } finally { fs.rmSync(allowedPath, { recursive: true, force: true }); @@ -273,7 +273,7 @@ describe('WindowsSandboxManager', () => { expect(icaclsArgs).toContainEqual([ path.resolve(extraWritePath), '/setintegritylevel', - 'Low', + '(OI)(CI)Low', ]); } finally { fs.rmSync(extraWritePath, { recursive: true, force: true }); @@ -308,7 +308,7 @@ describe('WindowsSandboxManager', () => { expect(icaclsArgs).not.toContainEqual([ uncPath, '/setintegritylevel', - 'Low', + '(OI)(CI)Low', ]); }, ); @@ -343,12 +343,12 @@ describe('WindowsSandboxManager', () => { expect(icaclsArgs).toContainEqual([ longPath, '/setintegritylevel', - 'Low', + '(OI)(CI)Low', ]); expect(icaclsArgs).toContainEqual([ devicePath, '/setintegritylevel', - 'Low', + '(OI)(CI)Low', ]); }, ); diff --git a/packages/core/src/sandbox/windows/WindowsSandboxManager.ts b/packages/core/src/sandbox/windows/WindowsSandboxManager.ts index e9bc2b7f8f..16d952ea1b 100644 --- a/packages/core/src/sandbox/windows/WindowsSandboxManager.ts +++ b/packages/core/src/sandbox/windows/WindowsSandboxManager.ts @@ -236,6 +236,10 @@ export class WindowsSandboxManager implements SandboxManager { false, }; + const defaultNetwork = + this.options.modeConfig?.network || req.policy?.networkAccess || false; + const networkAccess = defaultNetwork || mergedAdditional.network; + // 1. Handle filesystem permissions for Low Integrity // Grant "Low Mandatory Level" write access to the workspace. // If not in readonly mode OR it's a strictly approved pipeline, allow workspace writes @@ -251,7 +255,7 @@ export class WindowsSandboxManager implements SandboxManager { await this.grantLowIntegrityAccess(this.options.workspace); } - // Grant "Low Mandatory Level" read access to allowedPaths. + // Grant "Low Mandatory Level" read/write access to allowedPaths. const allowedPaths = sanitizePaths(req.policy?.allowedPaths) || []; for (const allowedPath of allowedPaths) { await this.grantLowIntegrityAccess(allowedPath); @@ -342,10 +346,6 @@ export class WindowsSandboxManager implements SandboxManager { // GeminiSandbox.exe --forbidden-manifest [args...] const program = this.helperPath; - const defaultNetwork = - this.options.modeConfig?.network ?? req.policy?.networkAccess ?? false; - const networkAccess = defaultNetwork || mergedAdditional.network; - const args = [ networkAccess ? '1' : '0', req.cwd, @@ -395,7 +395,11 @@ export class WindowsSandboxManager implements SandboxManager { } try { - await spawnAsync('icacls', [resolvedPath, '/setintegritylevel', 'Low']); + await spawnAsync('icacls', [ + resolvedPath, + '/setintegritylevel', + '(OI)(CI)Low', + ]); this.allowedCache.add(resolvedPath); } catch (e) { debugLogger.log( diff --git a/packages/core/src/services/sandboxManager.test.ts b/packages/core/src/services/sandboxManager.test.ts index 02e16fd5e9..0454581aac 100644 --- a/packages/core/src/services/sandboxManager.test.ts +++ b/packages/core/src/services/sandboxManager.test.ts @@ -385,5 +385,14 @@ describe('SandboxManager', () => { expect(manager).toBeInstanceOf(expected); }, ); + + it('should return WindowsSandboxManager if sandboxing is enabled on win32', () => { + vi.spyOn(os, 'platform').mockReturnValue('win32'); + const manager = createSandboxManager( + { enabled: true }, + { workspace: '/workspace' }, + ); + expect(manager).toBeInstanceOf(WindowsSandboxManager); + }); }); }); diff --git a/packages/core/src/utils/shell-utils.ts b/packages/core/src/utils/shell-utils.ts index 11e17ca358..e2a240a0b0 100644 --- a/packages/core/src/utils/shell-utils.ts +++ b/packages/core/src/utils/shell-utils.ts @@ -408,7 +408,9 @@ function hasPromptCommandTransform(root: Node): boolean { return false; } -function parseBashCommandDetails(command: string): CommandParseResult | null { +export function parseBashCommandDetails( + command: string, +): CommandParseResult | null { if (treeSitterInitializationError) { debugLogger.debug( 'Bash parser not initialized:', @@ -557,7 +559,19 @@ export function parseCommandDetails( const configuration = getShellConfiguration(); if (configuration.shell === 'powershell') { - return parsePowerShellCommandDetails(command, configuration.executable); + const result = parsePowerShellCommandDetails( + command, + configuration.executable, + ); + if (!result || result.hasError) { + // Fallback to bash parser which is usually good enough for simple commands + // and doesn't rely on the host PowerShell environment restrictions (e.g., ConstrainedLanguage) + const bashResult = parseBashCommandDetails(command); + if (bashResult && !bashResult.hasError) { + return bashResult; + } + } + return result; } if (configuration.shell === 'bash') {