fix(sandbox): implement Windows Mandatory Integrity Control for GeminiSandbox (#24057)

This commit is contained in:
Gal Zahavi
2026-03-27 17:14:35 -07:00
committed by GitHub
parent c2705e8332
commit ae123c547c
8 changed files with 75 additions and 23 deletions

View File

@@ -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]

View File

@@ -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)

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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',
]);
},
);

View File

@@ -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(

View File

@@ -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);
});
});
});

View File

@@ -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') {