mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
feat(core): implement native Windows sandboxing with restricted tokens
This commit is contained in:
@@ -29,6 +29,7 @@ const VALID_SANDBOX_COMMANDS: ReadonlyArray<SandboxConfig['command']> = [
|
|||||||
'sandbox-exec',
|
'sandbox-exec',
|
||||||
'runsc',
|
'runsc',
|
||||||
'lxc',
|
'lxc',
|
||||||
|
'windows-native',
|
||||||
];
|
];
|
||||||
|
|
||||||
function isSandboxCommand(
|
function isSandboxCommand(
|
||||||
@@ -95,6 +96,8 @@ function getSandboxCommand(
|
|||||||
// note: runsc is NOT auto-detected, it must be explicitly specified
|
// note: runsc is NOT auto-detected, it must be explicitly specified
|
||||||
if (os.platform() === 'darwin' && commandExists.sync('sandbox-exec')) {
|
if (os.platform() === 'darwin' && commandExists.sync('sandbox-exec')) {
|
||||||
return 'sandbox-exec';
|
return 'sandbox-exec';
|
||||||
|
} else if (os.platform() === 'win32') {
|
||||||
|
return 'windows-native';
|
||||||
} else if (commandExists.sync('docker') && sandbox === true) {
|
} else if (commandExists.sync('docker') && sandbox === true) {
|
||||||
return 'docker';
|
return 'docker';
|
||||||
} else if (commandExists.sync('podman') && sandbox === true) {
|
} else if (commandExists.sync('podman') && sandbox === true) {
|
||||||
@@ -128,7 +131,8 @@ export async function loadSandboxConfig(
|
|||||||
process.env['GEMINI_SANDBOX_IMAGE_DEFAULT'] ??
|
process.env['GEMINI_SANDBOX_IMAGE_DEFAULT'] ??
|
||||||
packageJson?.config?.sandboxImageUri;
|
packageJson?.config?.sandboxImageUri;
|
||||||
|
|
||||||
return command && image
|
return command &&
|
||||||
|
(image || command === 'sandbox-exec' || command === 'windows-native')
|
||||||
? { enabled: true, allowedPaths: [], networkAccess: false, command, image }
|
? { enabled: true, allowedPaths: [], networkAccess: false, command, image }
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
|
import * as os from 'node:os';
|
||||||
import { inspect } from 'node:util';
|
import { inspect } from 'node:util';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -45,6 +46,7 @@ import {
|
|||||||
NoopSandboxManager,
|
NoopSandboxManager,
|
||||||
type SandboxManager,
|
type SandboxManager,
|
||||||
} from '../services/sandboxManager.js';
|
} from '../services/sandboxManager.js';
|
||||||
|
import { WindowsSandboxManager } from '../services/windowsSandboxManager.js';
|
||||||
import {
|
import {
|
||||||
initializeTelemetry,
|
initializeTelemetry,
|
||||||
DEFAULT_TELEMETRY_TARGET,
|
DEFAULT_TELEMETRY_TARGET,
|
||||||
@@ -74,6 +76,7 @@ import {
|
|||||||
StandardFileSystemService,
|
StandardFileSystemService,
|
||||||
type FileSystemService,
|
type FileSystemService,
|
||||||
} from '../services/fileSystemService.js';
|
} from '../services/fileSystemService.js';
|
||||||
|
import { SandboxedFileSystemService } from '../services/sandboxedFileSystemService.js';
|
||||||
import {
|
import {
|
||||||
TrackerCreateTaskTool,
|
TrackerCreateTaskTool,
|
||||||
TrackerUpdateTaskTool,
|
TrackerUpdateTaskTool,
|
||||||
@@ -454,7 +457,7 @@ export interface SandboxConfig {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
allowedPaths: string[];
|
allowedPaths: string[];
|
||||||
networkAccess: boolean;
|
networkAccess: boolean;
|
||||||
command?: 'docker' | 'podman' | 'sandbox-exec' | 'runsc' | 'lxc';
|
command?: 'docker' | 'podman' | 'sandbox-exec' | 'runsc' | 'lxc' | 'windows-native';
|
||||||
image?: string;
|
image?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,7 +468,7 @@ export const ConfigSchema = z.object({
|
|||||||
allowedPaths: z.array(z.string()).default([]),
|
allowedPaths: z.array(z.string()).default([]),
|
||||||
networkAccess: z.boolean().default(false),
|
networkAccess: z.boolean().default(false),
|
||||||
command: z
|
command: z
|
||||||
.enum(['docker', 'podman', 'sandbox-exec', 'runsc', 'lxc'])
|
.enum(['docker', 'podman', 'sandbox-exec', 'runsc', 'lxc', 'windows-native'])
|
||||||
.optional(),
|
.optional(),
|
||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
})
|
})
|
||||||
@@ -835,7 +838,6 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
this.approvedPlanPath = undefined;
|
this.approvedPlanPath = undefined;
|
||||||
this.embeddingModel =
|
this.embeddingModel =
|
||||||
params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL;
|
params.embeddingModel ?? DEFAULT_GEMINI_EMBEDDING_MODEL;
|
||||||
this.fileSystemService = new StandardFileSystemService();
|
|
||||||
this.sandbox = params.sandbox
|
this.sandbox = params.sandbox
|
||||||
? {
|
? {
|
||||||
enabled: params.sandbox.enabled ?? false,
|
enabled: params.sandbox.enabled ?? false,
|
||||||
@@ -979,6 +981,7 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
pager: params.shellExecutionConfig?.pager ?? 'cat',
|
pager: params.shellExecutionConfig?.pager ?? 'cat',
|
||||||
sanitizationConfig: this.sanitizationConfig,
|
sanitizationConfig: this.sanitizationConfig,
|
||||||
sandboxManager: this.sandboxManager,
|
sandboxManager: this.sandboxManager,
|
||||||
|
sandboxConfig: this.sandbox,
|
||||||
};
|
};
|
||||||
this.truncateToolOutputThreshold =
|
this.truncateToolOutputThreshold =
|
||||||
params.truncateToolOutputThreshold ??
|
params.truncateToolOutputThreshold ??
|
||||||
@@ -1094,7 +1097,24 @@ export class Config implements McpContext, AgentLoopContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._geminiClient = new GeminiClient(this);
|
this._geminiClient = new GeminiClient(this);
|
||||||
this._sandboxManager = new NoopSandboxManager();
|
if (
|
||||||
|
os.platform() === 'win32' &&
|
||||||
|
(this.sandbox?.enabled || this.sandbox?.command === 'windows-native')
|
||||||
|
) {
|
||||||
|
this._sandboxManager = new WindowsSandboxManager();
|
||||||
|
} else {
|
||||||
|
this._sandboxManager = new NoopSandboxManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.sandbox?.enabled && this._sandboxManager) {
|
||||||
|
this.fileSystemService = new SandboxedFileSystemService(
|
||||||
|
this._sandboxManager,
|
||||||
|
this.cwd,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.fileSystemService = new StandardFileSystemService();
|
||||||
|
}
|
||||||
|
|
||||||
this.shellExecutionConfig.sandboxManager = this._sandboxManager;
|
this.shellExecutionConfig.sandboxManager = this._sandboxManager;
|
||||||
this.modelRouterService = new ModelRouterService(this);
|
this.modelRouterService = new ModelRouterService(this);
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export interface SandboxRequest {
|
|||||||
/** Optional sandbox-specific configuration. */
|
/** Optional sandbox-specific configuration. */
|
||||||
config?: {
|
config?: {
|
||||||
sanitizationConfig?: Partial<EnvironmentSanitizationConfig>;
|
sanitizationConfig?: Partial<EnvironmentSanitizationConfig>;
|
||||||
|
allowedPaths?: string[];
|
||||||
|
networkAccess?: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { type FileSystemService } from './fileSystemService.js';
|
||||||
|
import { type SandboxManager } from './sandboxManager.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A FileSystemService implementation that performs operations through a sandbox.
|
||||||
|
*/
|
||||||
|
export class SandboxedFileSystemService implements FileSystemService {
|
||||||
|
constructor(
|
||||||
|
private sandboxManager: SandboxManager,
|
||||||
|
private cwd: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async readTextFile(filePath: string): Promise<string> {
|
||||||
|
const prepared = await this.sandboxManager.prepareCommand({
|
||||||
|
command: '__read',
|
||||||
|
args: [filePath],
|
||||||
|
cwd: this.cwd,
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(prepared.program, prepared.args, {
|
||||||
|
cwd: this.cwd,
|
||||||
|
env: prepared.env,
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
let error = '';
|
||||||
|
|
||||||
|
child.stdout?.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr?.on('data', (data) => {
|
||||||
|
error += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve(output);
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Failed to read file via sandbox: ${error || 'Unknown error'}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeTextFile(filePath: string, content: string): Promise<void> {
|
||||||
|
const prepared = await this.sandboxManager.prepareCommand({
|
||||||
|
command: '__write',
|
||||||
|
args: [filePath],
|
||||||
|
cwd: this.cwd,
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn(prepared.program, prepared.args, {
|
||||||
|
cwd: this.cwd,
|
||||||
|
env: prepared.env,
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stdin?.write(content);
|
||||||
|
child.stdin?.end();
|
||||||
|
|
||||||
|
let error = '';
|
||||||
|
child.stderr?.on('data', (data) => {
|
||||||
|
error += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Failed to write file via sandbox: ${error || 'Unknown error'}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Security.Principal;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
public class GeminiSandbox {
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct STARTUPINFO {
|
||||||
|
public uint cb;
|
||||||
|
public string lpReserved;
|
||||||
|
public string lpDesktop;
|
||||||
|
public string lpTitle;
|
||||||
|
public uint dwX;
|
||||||
|
public uint dwY;
|
||||||
|
public uint dwXSize;
|
||||||
|
public uint dwYSize;
|
||||||
|
public uint dwXCountChars;
|
||||||
|
public uint dwYCountChars;
|
||||||
|
public uint dwFillAttribute;
|
||||||
|
public uint dwFlags;
|
||||||
|
public ushort wShowWindow;
|
||||||
|
public ushort cbReserved2;
|
||||||
|
public IntPtr lpReserved2;
|
||||||
|
public IntPtr hStdInput;
|
||||||
|
public IntPtr hStdOutput;
|
||||||
|
public IntPtr hStdError;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct PROCESS_INFORMATION {
|
||||||
|
public IntPtr hProcess;
|
||||||
|
public IntPtr hThread;
|
||||||
|
public uint dwProcessId;
|
||||||
|
public uint dwThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct JOBOBJECT_BASIC_LIMIT_INFORMATION {
|
||||||
|
public Int64 PerProcessUserTimeLimit;
|
||||||
|
public Int64 PerJobUserTimeLimit;
|
||||||
|
public uint LimitFlags;
|
||||||
|
public UIntPtr MinimumWorkingSetSize;
|
||||||
|
public UIntPtr MaximumWorkingSetSize;
|
||||||
|
public uint ActiveProcessLimit;
|
||||||
|
public UIntPtr Affinity;
|
||||||
|
public uint PriorityClass;
|
||||||
|
public uint SchedulingClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct IO_COUNTERS {
|
||||||
|
public ulong ReadOperationCount;
|
||||||
|
public ulong WriteOperationCount;
|
||||||
|
public ulong OtherOperationCount;
|
||||||
|
public ulong ReadTransferCount;
|
||||||
|
public ulong WriteTransferCount;
|
||||||
|
public ulong OtherTransferCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
|
||||||
|
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
|
||||||
|
public IO_COUNTERS IoInfo;
|
||||||
|
public UIntPtr ProcessMemoryLimit;
|
||||||
|
public UIntPtr JobMemoryLimit;
|
||||||
|
public UIntPtr PeakProcessMemoryUsed;
|
||||||
|
public UIntPtr PeakJobMemoryUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct SID_AND_ATTRIBUTES {
|
||||||
|
public IntPtr Sid;
|
||||||
|
public uint Attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct TOKEN_MANDATORY_LABEL {
|
||||||
|
public SID_AND_ATTRIBUTES Label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum JobObjectInfoClass {
|
||||||
|
ExtendedLimitInformation = 9
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern IntPtr GetCurrentProcess();
|
||||||
|
|
||||||
|
[DllImport("advapi32.dll", SetLastError = true)]
|
||||||
|
public static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);
|
||||||
|
|
||||||
|
[DllImport("advapi32.dll", SetLastError = true)]
|
||||||
|
public static extern bool CreateRestrictedToken(IntPtr ExistingTokenHandle, uint Flags, uint DisableSidCount, IntPtr SidsToDisable, uint DeletePrivilegeCount, IntPtr PrivilegesToDelete, uint RestrictedSidCount, IntPtr SidsToRestrict, out IntPtr NewTokenHandle);
|
||||||
|
|
||||||
|
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
|
public static extern bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
|
public static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string lpName);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoClass JobObjectInfoClass, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern uint ResumeThread(IntPtr hThread);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern bool CloseHandle(IntPtr hObject);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
public static extern IntPtr GetStdHandle(int nStdHandle);
|
||||||
|
|
||||||
|
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
|
public static extern bool ConvertStringSidToSid(string StringSid, out IntPtr Sid);
|
||||||
|
|
||||||
|
[DllImport("advapi32.dll", SetLastError = true)]
|
||||||
|
public static extern bool SetTokenInformation(IntPtr TokenHandle, int TokenInformationClass, IntPtr TokenInformation, uint TokenInformationLength);
|
||||||
|
|
||||||
|
public const uint TOKEN_DUPLICATE = 0x0002;
|
||||||
|
public const uint TOKEN_QUERY = 0x0008;
|
||||||
|
public const uint TOKEN_ASSIGN_PRIMARY = 0x0001;
|
||||||
|
public const uint TOKEN_ADJUST_DEFAULT = 0x0080;
|
||||||
|
public const uint DISABLE_MAX_PRIVILEGE = 0x1;
|
||||||
|
public const uint CREATE_SUSPENDED = 0x00000004;
|
||||||
|
public const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
|
||||||
|
public const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
||||||
|
public const uint STARTF_USESTDHANDLES = 0x00000100;
|
||||||
|
public const int TokenIntegrityLevel = 25;
|
||||||
|
public const uint SE_GROUP_INTEGRITY = 0x00000020;
|
||||||
|
public const uint INFINITE = 0xFFFFFFFF;
|
||||||
|
|
||||||
|
static int Main(string[] args) {
|
||||||
|
if (args.Length < 3) {
|
||||||
|
Console.WriteLine("Usage: GeminiSandbox.exe <network:0|1> <cwd> <command> [args...]");
|
||||||
|
Console.WriteLine("Internal commands: __read <path>, __write <path>");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool networkAccess = args[0] == "1";
|
||||||
|
string cwd = args[1];
|
||||||
|
string command = args[2];
|
||||||
|
|
||||||
|
// 1. Setup Token
|
||||||
|
IntPtr hCurrentProcess = GetCurrentProcess();
|
||||||
|
IntPtr hToken;
|
||||||
|
if (!OpenProcessToken(hCurrentProcess, TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY | TOKEN_ADJUST_DEFAULT, out hToken)) {
|
||||||
|
Console.Error.WriteLine("Failed to open process token");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
IntPtr hRestrictedToken;
|
||||||
|
IntPtr pSidsToDisable = IntPtr.Zero;
|
||||||
|
uint sidCount = 0;
|
||||||
|
|
||||||
|
if (!networkAccess) {
|
||||||
|
IntPtr networkSid;
|
||||||
|
if (ConvertStringSidToSid("S-1-5-2", out networkSid)) {
|
||||||
|
sidCount = 1;
|
||||||
|
int saaSize = Marshal.SizeOf(typeof(SID_AND_ATTRIBUTES));
|
||||||
|
pSidsToDisable = Marshal.AllocHGlobal(saaSize);
|
||||||
|
SID_AND_ATTRIBUTES saa = new SID_AND_ATTRIBUTES();
|
||||||
|
saa.Sid = networkSid;
|
||||||
|
saa.Attributes = 0;
|
||||||
|
Marshal.StructureToPtr(saa, pSidsToDisable, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IntPtr pSidsToRestrict = IntPtr.Zero;
|
||||||
|
uint restrictCount = 0;
|
||||||
|
IntPtr restrictedSid;
|
||||||
|
if (ConvertStringSidToSid("S-1-5-12", out restrictedSid)) {
|
||||||
|
restrictCount = 1;
|
||||||
|
int saaSize = Marshal.SizeOf(typeof(SID_AND_ATTRIBUTES));
|
||||||
|
pSidsToRestrict = Marshal.AllocHGlobal(saaSize);
|
||||||
|
SID_AND_ATTRIBUTES saa = new SID_AND_ATTRIBUTES();
|
||||||
|
saa.Sid = restrictedSid;
|
||||||
|
saa.Attributes = 0;
|
||||||
|
Marshal.StructureToPtr(saa, pSidsToRestrict, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CreateRestrictedToken(hToken, DISABLE_MAX_PRIVILEGE, sidCount, pSidsToDisable, 0, IntPtr.Zero, restrictCount, pSidsToRestrict, out hRestrictedToken)) {
|
||||||
|
Console.Error.WriteLine("Failed to create restricted token");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Set Integrity Level to Low
|
||||||
|
IntPtr lowIntegritySid;
|
||||||
|
if (ConvertStringSidToSid("S-1-16-4096", out lowIntegritySid)) {
|
||||||
|
TOKEN_MANDATORY_LABEL tml = new TOKEN_MANDATORY_LABEL();
|
||||||
|
tml.Label.Sid = lowIntegritySid;
|
||||||
|
tml.Label.Attributes = SE_GROUP_INTEGRITY;
|
||||||
|
int tmlSize = Marshal.SizeOf(tml);
|
||||||
|
IntPtr pTml = Marshal.AllocHGlobal(tmlSize);
|
||||||
|
Marshal.StructureToPtr(tml, pTml, false);
|
||||||
|
SetTokenInformation(hRestrictedToken, TokenIntegrityLevel, pTml, (uint)tmlSize);
|
||||||
|
Marshal.FreeHGlobal(pTml);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Handle Internal Commands or External Process
|
||||||
|
if (command == "__read") {
|
||||||
|
string path = args[3];
|
||||||
|
return RunInImpersonation(hRestrictedToken, () => {
|
||||||
|
try {
|
||||||
|
Console.Write(File.ReadAllText(path));
|
||||||
|
return 0;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Console.Error.WriteLine(e.Message);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (command == "__write") {
|
||||||
|
string path = args[3];
|
||||||
|
return RunInImpersonation(hRestrictedToken, () => {
|
||||||
|
try {
|
||||||
|
using (StreamReader reader = new StreamReader(Console.OpenStandardInput()))
|
||||||
|
using (StreamWriter writer = new StreamWriter(File.Create(path))) {
|
||||||
|
writer.Write(reader.ReadToEnd());
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Console.Error.WriteLine(e.Message);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Setup Job Object for external process
|
||||||
|
IntPtr hJob = CreateJobObject(IntPtr.Zero, null);
|
||||||
|
if (hJob != IntPtr.Zero) {
|
||||||
|
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limitInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
|
||||||
|
limitInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||||
|
int limitSize = Marshal.SizeOf(limitInfo);
|
||||||
|
IntPtr pLimit = Marshal.AllocHGlobal(limitSize);
|
||||||
|
Marshal.StructureToPtr(limitInfo, pLimit, false);
|
||||||
|
SetInformationJobObject(hJob, JobObjectInfoClass.ExtendedLimitInformation, pLimit, (uint)limitSize);
|
||||||
|
Marshal.FreeHGlobal(pLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Launch Process
|
||||||
|
STARTUPINFO si = new STARTUPINFO();
|
||||||
|
si.cb = (uint)Marshal.SizeOf(si);
|
||||||
|
si.dwFlags = STARTF_USESTDHANDLES;
|
||||||
|
si.hStdInput = GetStdHandle(-10);
|
||||||
|
si.hStdOutput = GetStdHandle(-11);
|
||||||
|
si.hStdError = GetStdHandle(-12);
|
||||||
|
|
||||||
|
string commandLine = string.Join(" ", args, 2, args.Length - 2);
|
||||||
|
PROCESS_INFORMATION pi;
|
||||||
|
if (!CreateProcessAsUser(hRestrictedToken, null, commandLine, IntPtr.Zero, IntPtr.Zero, true, CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT, IntPtr.Zero, cwd, ref si, out pi)) {
|
||||||
|
Console.Error.WriteLine("Failed to create process. Error: " + Marshal.GetLastWin32Error());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hJob != IntPtr.Zero) {
|
||||||
|
AssignProcessToJobObject(hJob, pi.hProcess);
|
||||||
|
}
|
||||||
|
|
||||||
|
ResumeThread(pi.hThread);
|
||||||
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||||
|
|
||||||
|
uint exitCode = 0;
|
||||||
|
GetExitCodeProcess(pi.hProcess, out exitCode);
|
||||||
|
|
||||||
|
CloseHandle(pi.hProcess);
|
||||||
|
CloseHandle(pi.hThread);
|
||||||
|
CloseHandle(hRestrictedToken);
|
||||||
|
CloseHandle(hToken);
|
||||||
|
if (hJob != IntPtr.Zero) CloseHandle(hJob);
|
||||||
|
|
||||||
|
return (int)exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int RunInImpersonation(IntPtr hToken, Func<int> action) {
|
||||||
|
WindowsIdentity.Impersonate(hToken);
|
||||||
|
int result = action();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
} from '../utils/terminalSerializer.js';
|
} from '../utils/terminalSerializer.js';
|
||||||
import { type EnvironmentSanitizationConfig } from './environmentSanitization.js';
|
import { type EnvironmentSanitizationConfig } from './environmentSanitization.js';
|
||||||
import { type SandboxManager } from './sandboxManager.js';
|
import { type SandboxManager } from './sandboxManager.js';
|
||||||
|
import type { SandboxConfig } from '../config/config.js';
|
||||||
import { killProcessGroup } from '../utils/process-utils.js';
|
import { killProcessGroup } from '../utils/process-utils.js';
|
||||||
const { Terminal } = pkg;
|
const { Terminal } = pkg;
|
||||||
|
|
||||||
@@ -101,6 +102,7 @@ export interface ShellExecutionConfig {
|
|||||||
defaultBg?: string;
|
defaultBg?: string;
|
||||||
sanitizationConfig: EnvironmentSanitizationConfig;
|
sanitizationConfig: EnvironmentSanitizationConfig;
|
||||||
sandboxManager: SandboxManager;
|
sandboxManager: SandboxManager;
|
||||||
|
sandboxConfig?: SandboxConfig;
|
||||||
// Used for testing
|
// Used for testing
|
||||||
disableDynamicLineTrimming?: boolean;
|
disableDynamicLineTrimming?: boolean;
|
||||||
scrollback?: number;
|
scrollback?: number;
|
||||||
@@ -308,6 +310,8 @@ export class ShellExecutionService {
|
|||||||
env,
|
env,
|
||||||
config: {
|
config: {
|
||||||
sanitizationConfig: shellExecutionConfig.sanitizationConfig,
|
sanitizationConfig: shellExecutionConfig.sanitizationConfig,
|
||||||
|
allowedPaths: shellExecutionConfig.sandboxConfig?.allowedPaths,
|
||||||
|
networkAccess: shellExecutionConfig.sandboxConfig?.networkAccess,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { WindowsSandboxManager } from './windowsSandboxManager.js';
|
||||||
|
import { SandboxRequest } from './sandboxManager.js';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
|
||||||
|
describe('WindowsSandboxManager', () => {
|
||||||
|
const manager = new WindowsSandboxManager();
|
||||||
|
|
||||||
|
it.skipIf(os.platform() !== 'win32')('should prepare a GeminiSandbox.exe command', async () => {
|
||||||
|
const req: SandboxRequest = {
|
||||||
|
command: 'whoami',
|
||||||
|
args: ['/groups'],
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: { TEST_VAR: 'test_value' },
|
||||||
|
config: {
|
||||||
|
networkAccess: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await manager.prepareCommand(req);
|
||||||
|
|
||||||
|
expect(result.program).toContain('GeminiSandbox.exe');
|
||||||
|
expect(result.args).toEqual(expect.arrayContaining(['0', process.cwd(), 'whoami', '/groups']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skipIf(os.platform() !== 'win32')('should handle networkAccess from config', async () => {
|
||||||
|
const req: SandboxRequest = {
|
||||||
|
command: 'whoami',
|
||||||
|
args: [],
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: {},
|
||||||
|
config: {
|
||||||
|
networkAccess: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await manager.prepareCommand(req);
|
||||||
|
expect(result.args[0]).toBe('1');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import os from 'node:os';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import {
|
||||||
|
type SandboxManager,
|
||||||
|
type SandboxRequest,
|
||||||
|
type SandboxedCommand,
|
||||||
|
} from './sandboxManager.js';
|
||||||
|
import {
|
||||||
|
sanitizeEnvironment,
|
||||||
|
type EnvironmentSanitizationConfig,
|
||||||
|
} from './environmentSanitization.js';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A SandboxManager implementation for Windows that uses Restricted Tokens,
|
||||||
|
* Job Objects, and Low Integrity levels for process isolation.
|
||||||
|
* Uses a native C# helper to bypass PowerShell restrictions.
|
||||||
|
*/
|
||||||
|
export class WindowsSandboxManager implements SandboxManager {
|
||||||
|
private readonly helperPath: string;
|
||||||
|
private initialized = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.helperPath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'scripts',
|
||||||
|
'GeminiSandbox.exe',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ensureInitialized(): void {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
if (!fs.existsSync(this.helperPath)) {
|
||||||
|
// If the exe doesn't exist, we try to compile it from the .cs file
|
||||||
|
const sourcePath = this.helperPath.replace(/\.exe$/, '.cs');
|
||||||
|
if (fs.existsSync(sourcePath)) {
|
||||||
|
const csc = path.join(
|
||||||
|
process.env['SystemRoot'] || 'C:\\Windows',
|
||||||
|
'Microsoft.NET',
|
||||||
|
'Framework64',
|
||||||
|
'v4.0.30319',
|
||||||
|
'csc.exe',
|
||||||
|
);
|
||||||
|
if (fs.existsSync(csc)) {
|
||||||
|
spawnSync(csc, ['/out:' + this.helperPath, sourcePath], { stdio: 'ignore' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares a command for sandboxed execution on Windows.
|
||||||
|
*/
|
||||||
|
async prepareCommand(req: SandboxRequest): Promise<SandboxedCommand> {
|
||||||
|
this.ensureInitialized();
|
||||||
|
|
||||||
|
const sanitizationConfig: EnvironmentSanitizationConfig = {
|
||||||
|
allowedEnvironmentVariables:
|
||||||
|
req.config?.sanitizationConfig?.allowedEnvironmentVariables ?? [],
|
||||||
|
blockedEnvironmentVariables:
|
||||||
|
req.config?.sanitizationConfig?.blockedEnvironmentVariables ?? [],
|
||||||
|
enableEnvironmentVariableRedaction:
|
||||||
|
req.config?.sanitizationConfig?.enableEnvironmentVariableRedaction ??
|
||||||
|
true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizedEnv = sanitizeEnvironment(req.env, sanitizationConfig);
|
||||||
|
|
||||||
|
// 1. Handle filesystem permissions for Low Integrity
|
||||||
|
// Grant "Low Mandatory Level" write access to the CWD.
|
||||||
|
this.grantLowIntegrityAccess(req.cwd);
|
||||||
|
|
||||||
|
// Grant "Low Mandatory Level" read access to allowedPaths.
|
||||||
|
if (req.config?.allowedPaths) {
|
||||||
|
for (const allowedPath of req.config.allowedPaths) {
|
||||||
|
this.grantLowIntegrityAccess(allowedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Construct the helper command
|
||||||
|
// GeminiSandbox.exe <network:0|1> <cwd> <command> [args...]
|
||||||
|
const program = this.helperPath;
|
||||||
|
|
||||||
|
// If the command starts with __, it's an internal command for the sandbox helper itself.
|
||||||
|
const args = [
|
||||||
|
req.config?.networkAccess ? '1' : '0',
|
||||||
|
req.cwd,
|
||||||
|
req.command,
|
||||||
|
...req.args,
|
||||||
|
];
|
||||||
|
|
||||||
|
return {
|
||||||
|
program,
|
||||||
|
args,
|
||||||
|
env: sanitizedEnv,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grants "Low Mandatory Level" access to a path using icacls.
|
||||||
|
*/
|
||||||
|
private grantLowIntegrityAccess(targetPath: string): void {
|
||||||
|
try {
|
||||||
|
spawnSync('icacls', [targetPath, '/setintegritylevel', '(OI)(CI)Low'], {
|
||||||
|
stdio: 'ignore',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Best effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user