Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion docs/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ production configs and the dev schema when working on experimental features:
"commandLine": "python app.py", // Required: command to execute
"cwd": "C:\\workspace", // Working directory
"env": ["MY_VAR=value"], // Environment variables as KEY=VALUE
"timeout": 30000 // Timeout in ms (0 = no timeout)
"timeout": 30000, // Timeout in ms (0 = no timeout)
"resourceLimits": { // Optional resource caps (Job Object on Windows)
"memoryMb": 512, // Memory cap in MiB (0 = no limit)
"maxProcesses": 16, // Max active processes (0 = no limit)
"cpuRatePercent": 50, // CPU cap as percentage 0..100 (0 = no limit)
"allowChildProcesses": true // Allow spawning children (default true)
}
},

"filesystem": {
Expand Down Expand Up @@ -91,6 +97,47 @@ The `filesystem` section defines path access policy shared across backends:
| `readonlyPaths` | string[] | `[]` | Paths the process can read but not write. |
| `deniedPaths` | string[] | `[]` | Paths the process cannot access at all. |

### Resource Limits

The optional `process.resourceLimits` section caps the resources consumed by
the sandboxed process and **all of its descendants**. On the Windows
processcontainer / AppContainer backends this is enforced via a Windows Job
Object.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `memoryMb` | integer | `0` | Maximum committed memory in MiB across all processes in the job. `0` = no limit. |
| `maxProcesses` | integer | `0` | Maximum number of concurrent active processes (fork-bomb protection). `0` = no limit. |
| `cpuRatePercent` | integer | `0` | CPU rate cap as a percentage `0..=100`. `0` = no limit. Values outside this range are rejected at parse time. |
| `allowChildProcesses` | boolean | `true` | When `false`, the OS blocks the sandboxed process from spawning child processes (`PROC_THREAD_ATTRIBUTE_CHILD_PROCESS_POLICY = PROCESS_CREATION_CHILD_PROCESS_RESTRICTED`). |

#### Process tree cleanup

Even when `resourceLimits` is omitted entirely, the AppContainer runner still
creates a Job Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`. This guarantees
that any descendant processes spawned by the sandboxed command β€” including
detached children created via `start /b`, `DETACHED_PROCESS`, or
`CREATE_NEW_CONSOLE` β€” are terminated when execution ends. Without this
behavior, an orphaned child could outlive the sandbox indefinitely while still
constrained by the AppContainer token.

#### Backend support

| Backend | `memoryMb` / `maxProcesses` / `cpuRatePercent` | `allowChildProcesses` | Orphan cleanup |
|---------|------------------------------------------------|------------------------|----------------|
| `processcontainer` (AppContainer, legacy) | enforced via Job Object | enforced via `PROC_THREAD_ATTRIBUTE_CHILD_PROCESS_POLICY` | always on |
| `processcontainer` (BaseContainer) | not yet enforced (see note below) | not yet enforced | inherits from OS sandbox |
| `lxc` / `wslc` | not yet enforced (future: cgroups) | not yet enforced | not yet enforced |
| `windows_sandbox` / `microvm` / `isolation_session` | governed by the VM/session, not Job Object | n/a | governed by the VM/session |
| `seatbelt` | not yet enforced (future: `RLIMIT_*`) | not yet enforced | not yet enforced |

**BaseContainer gap.** The BaseContainer `SandboxSpec` FlatBuffer
(`external/windows-sdk/BaseContainerSpecification.fbs`) does not currently
expose resource-limit fields. As a follow-up, the BaseContainer runner can
externally wrap the process handle returned by
`Experimental_CreateProcessInSandbox` in its own Job Object (Windows 8+
supports nested jobs) to apply the same caps.

### Fallback Policy

The `fallback` section gates the runner's host-impacting fallbacks. Each flag is an explicit operator consent for a specific mechanism the runner may otherwise pick when the preferred primitive is unavailable. Defaults preserve the pre-fallback-section behavior (all permitted).
Expand Down
30 changes: 30 additions & 0 deletions schemas/dev/mxc-config.schema.0.6.0-dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,36 @@
"minimum": 0,
"default": 0,
"description": "Execution timeout in milliseconds. 0 = no timeout (infinite)."
},
"resourceLimits": {
"type": "object",
"description": "Resource caps enforced on the sandboxed process and all of its descendants. Implemented via a Windows Job Object on the AppContainer / processcontainer backends. When this section is omitted the runner still creates a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so that descendant processes are terminated when execution ends β€” preventing orphan processes from outliving the sandbox.",
"properties": {
"memoryMb": {
"type": "integer",
"minimum": 0,
"default": 0,
"description": "Maximum committed memory in MiB across all processes in the job. 0 = no limit. Maps to JOB_OBJECT_LIMIT_JOB_MEMORY."
},
"maxProcesses": {
"type": "integer",
"minimum": 0,
"default": 0,
"description": "Maximum number of concurrent active processes in the job (fork-bomb protection). 0 = no limit. Maps to JOB_OBJECT_LIMIT_ACTIVE_PROCESS."
},
"cpuRatePercent": {
"type": "integer",
"minimum": 0,
"maximum": 100,
"default": 0,
"description": "CPU rate cap as a percentage of total CPU across all logical processors. 0 = no limit. Maps to JOBOBJECT_CPU_RATE_CONTROL_INFORMATION."
},
"allowChildProcesses": {
"type": "boolean",
"default": true,
"description": "Whether the sandboxed process may spawn child processes. When false, sets PROC_THREAD_ATTRIBUTE_CHILD_PROCESS_POLICY = PROCESS_CREATION_CHILD_PROCESS_RESTRICTED. Note: certain Windows subsystems (e.g. Windows Error Reporting) may still be able to spawn helpers via documented OS overrides; this flag is a hardening control, not a hard boundary."
}
}
}
}
},
Expand Down
49 changes: 49 additions & 0 deletions sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,55 @@ Open the schema file matching your `policy.version` (e.g. `mxc-config.schema.0.5

</details>

## Policy

`SandboxPolicy` describes cross-platform sandbox intent. Key fields include:

```typescript
type SandboxPolicy = {
version: string;
filesystem?: {
readwritePaths?: string[];
readonlyPaths?: string[];
deniedPaths?: string[];
clearPolicyOnExit?: boolean;
};
network?: {
allowOutbound?: boolean;
allowLocalNetwork?: boolean;
allowedHosts?: string[];
blockedHosts?: string[];
proxy?: { builtinTestServer: true } | { localhost: number } | { url: string };
};
ui?: {
allowWindows?: boolean;
clipboard?: 'none' | 'read' | 'write' | 'all';
allowInputInjection?: boolean;
};
timeoutMs?: number;
resourceLimits?: {
memoryMb?: number;
maxProcesses?: number;
cpuRatePercent?: number;
allowChildProcesses?: boolean;
};
};
```

### Resource Limits

Use `resourceLimits` to cap process-level resources for the sandboxed process and its descendants. Numeric caps use `0` or omission for no limit; `allowChildProcesses` defaults to `true`.

```typescript
const config = createConfigFromPolicy({
version: '0.6.0-dev',
resourceLimits: {
memoryMb: 512,
allowChildProcesses: false,
},
});
```

## State-Aware Sandboxes

<details>
Expand Down
1 change: 1 addition & 0 deletions sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export {
ExperimentalBackends,
ContainerConfig,
PlatformSupport,
ResourceLimits,
} from './types.js';

// Export platform detection functions
Expand Down
30 changes: 30 additions & 0 deletions sdk/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,34 @@ function validatePolicyVersion(version: string): void {
}
}

function validateResourceLimits(policy: SandboxPolicy): void {
const limits = policy.resourceLimits;
if (!limits) {
return;
}

const numericCaps: Array<[string, number | undefined]> = [
['memoryMb', limits.memoryMb],
['maxProcesses', limits.maxProcesses],
['cpuRatePercent', limits.cpuRatePercent],
];

for (const [name, value] of numericCaps) {
if (value === undefined) {
continue;
}
if (!Number.isInteger(value)) {
throw new Error(`resourceLimits.${name} must be an integer`);
}
if (value < 0) {
throw new Error(`resourceLimits.${name} must be greater than or equal to 0`);
}
}

if (limits.cpuRatePercent !== undefined && limits.cpuRatePercent > 100) {
throw new Error('resourceLimits.cpuRatePercent must be between 0 and 100');
}
}

/**
* Builds the WSLC (WSL Container) portion of a ContainerConfig.
Expand Down Expand Up @@ -228,6 +256,7 @@ export function createConfigFromPolicy(
): ContainerConfig {
diagLogVersion();
validatePolicyVersion(policy.version);
validateResourceLimits(policy);

const platform = os.platform();
const containerId = containerName ?? generateRandomContainerName();
Expand All @@ -243,6 +272,7 @@ export function createConfigFromPolicy(
process: {
commandLine: '',
timeout: policy.timeoutMs ?? 0,
...(policy.resourceLimits ? { resourceLimits: policy.resourceLimits } : {}),
},
};

Expand Down
23 changes: 23 additions & 0 deletions sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,27 @@ export interface ProcessConfig {
env?: string[];
/** Execution timeout in milliseconds (default: 0 = no timeout) */
timeout?: number;
/** Process-level resource caps. Enforced via Job Object on Windows backends. */
resourceLimits?: ResourceLimits;
}

/**
* Process-level resource caps enforced on the sandboxed process and all of its
* descendants. On Windows backends these are implemented via a Job Object.
*
* A value of 0 (the default for each numeric cap) means "no limit". When
* resourceLimits is omitted entirely, the executor still creates a Job Object
* with KILL_ON_JOB_CLOSE so descendant processes cannot outlive the sandbox.
*/
export interface ResourceLimits {
/** Maximum committed memory in MiB across all processes in the job. 0 = no limit. */
memoryMb?: number;
/** Maximum concurrent active processes (fork-bomb protection). 0 = no limit. */
maxProcesses?: number;
/** CPU rate cap as a percentage 0..=100. 0 = no limit. */
cpuRatePercent?: number;
/** Whether the sandboxed process may spawn child processes. Defaults to true. */
allowChildProcesses?: boolean;
}

/**
Expand Down Expand Up @@ -302,6 +323,8 @@ export type SandboxPolicy = {
};
/** Execution timeout in milliseconds. Omitted = no timeout. */
timeoutMs?: number;
/** Process-level resource caps (memory, process count, CPU, child-process policy). */
resourceLimits?: ResourceLimits;
}

/**
Expand Down
67 changes: 67 additions & 0 deletions sdk/tests/unit/sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,73 @@ describe('createConfigFromPolicy', () => {
assert.strictEqual(config.process!.timeout, 30000);
});

describe('resourceLimits', () => {
it('should omit process.resourceLimits when policy omits resourceLimits', () => {
const config = createConfigFromPolicy(defaultPolicy);
assert.strictEqual(config.process!.resourceLimits, undefined);
});

it('should propagate all resourceLimits fields faithfully', () => {
const resourceLimits = {
memoryMb: 512,
maxProcesses: 16,
cpuRatePercent: 50,
allowChildProcesses: true,
};
const config = createConfigFromPolicy({
version: '0.6.0-alpha',
resourceLimits,
});
assert.deepStrictEqual(config.process!.resourceLimits, resourceLimits);
});

it('should reject cpuRatePercent greater than 100', () => {
assert.throws(
() => createConfigFromPolicy({
version: '0.6.0-alpha',
resourceLimits: { cpuRatePercent: 101 },
}),
{ message: /resourceLimits\.cpuRatePercent must be between 0 and 100/ },
);
});

it('should reject negative memoryMb and maxProcesses', () => {
assert.throws(
() => createConfigFromPolicy({
version: '0.6.0-alpha',
resourceLimits: { memoryMb: -1 },
}),
{ message: /resourceLimits\.memoryMb must be greater than or equal to 0/ },
);

assert.throws(
() => createConfigFromPolicy({
version: '0.6.0-alpha',
resourceLimits: { maxProcesses: -1 },
}),
{ message: /resourceLimits\.maxProcesses must be greater than or equal to 0/ },
);
});

it('should reject non-integer numeric caps', () => {
assert.throws(
() => createConfigFromPolicy({
version: '0.6.0-alpha',
resourceLimits: { memoryMb: 1.5 },
}),
{ message: /resourceLimits\.memoryMb must be an integer/ },
);
});

it('should accept allowChildProcesses false and include it in output', () => {
const config = createConfigFromPolicy({
version: '0.6.0-alpha',
resourceLimits: { allowChildProcesses: false },
});
assert.deepStrictEqual(config.process!.resourceLimits, { allowChildProcesses: false });
});
});

describe('Windows', () => {
let originalPlatform: PropertyDescriptor | undefined;

Expand Down
Loading
Loading