mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-04 02:11:11 -07:00
fix(sandbox): implement Windows Mandatory Integrity Control for GeminiSandbox (#24057)
This commit is contained in:
@@ -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]
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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 <network:0|1> <cwd> --forbidden-manifest <path> <command> [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(
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user