Skip to content
Merged
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
7 changes: 7 additions & 0 deletions sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to `@microsoft/mxc-sdk` will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- `getPlatformSupport()` now reports `uiCapabilities` on Windows when the
native probe can determine which UI restrictions the host can enforce.

## [0.3.0]

### ⚠️ Breaking changes
Expand Down
5 changes: 5 additions & 0 deletions sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ The default `processcontainer`, `bubblewrap`, and `lxc` backends work out of the

> **Hyperlight** is an opt-in build flavor (Linux x64 and Windows x64) gated by the `--with-hyperlight` cargo feature. Default shipped binaries do not include it; build from source with `build.bat --with-hyperlight` (Windows) or the equivalent cargo invocation on Linux.

`getPlatformSupport()` reports backend availability and, when the native probe can determine it, `uiCapabilities`: a platform-neutral view of which UI restrictions the host can enforce. This is currently populated only by the Windows native probe, where it is derived from `JOB_OBJECT_UILIMIT_*` support; Linux and macOS omit the field until their probes expose equivalent data.

**Node.js:** ≥ 18.

---
Expand Down Expand Up @@ -364,6 +366,9 @@ getAvailableToolsPolicy(env?, options?) → FilesystemPolicyResult
getUserProfilePolicy() → FilesystemPolicyResult
getTemporaryFilesPolicy(env?) → FilesystemPolicyResult

// Capability types
UiCapabilitySupport

// Errors (typed wire-format errors from wxc-exec)
ErrorCode, MxcError, mxcErrorFromCode(code)
```
Expand Down
1 change: 1 addition & 0 deletions sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export {
ExperimentalBackends,
ContainerConfig,
PlatformSupport,
UiCapabilitySupport,
} from './types.js';

// Export platform detection functions
Expand Down
43 changes: 36 additions & 7 deletions sdk/src/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { execSync, execFileSync } from 'child_process';
import { fileURLToPath } from 'node:url';
import { ContainmentBackend, IsolationTier, PlatformSupport } from './types.js';
import { ContainmentBackend, IsolationTier, PlatformSupport, UiCapabilitySupport } from './types.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Expand Down Expand Up @@ -141,11 +141,12 @@ function isWindowsSandboxAvailable(): boolean {
/**
* Get platform support information.
*
* On Windows, when the host build is supported, this also invokes
* `wxc-exec --probe` to populate `isolationTier` and (if any) the
* `isolationWarnings` array. The result is cached for the lifetime of
* the SDK module — the underlying machine state is not expected to
* change at runtime.
* On Windows, this also invokes `wxc-exec --probe` to populate
* `isolationTier`, the `isolationWarnings` array (if any), and portable UI
* capability facts. Linux and macOS currently do not expose native probe data,
* so `uiCapabilities` is omitted on those platforms. The result is cached for
* the lifetime of the SDK module — the underlying machine state is not
* expected to change at runtime.
*
* @returns Platform support details including available sandboxing methods
*/
Expand Down Expand Up @@ -194,6 +195,27 @@ function isValidTier(s: unknown): s is IsolationTier {
return s === 'base-container' || s === 'appcontainer-bfs' || s === 'appcontainer-dacl';
}

const UI_CAPABILITY_FIELDS: readonly (keyof UiCapabilitySupport)[] = [
'canBlockClipboardRead',
'canBlockClipboardWrite',
'canBlockInputInjection',
'canBlockInputMethodChanges',
'canBlockExternalUiObjects',
'canBlockGlobalUiNamespace',
'canBlockDesktopSwitching',
'canBlockLogoffOrShutdown',
'canBlockSystemParameterChanges',
'canBlockDisplaySettingsChanges',
];

function isUiCapabilitySupport(value: unknown): value is UiCapabilitySupport {
if (!value || typeof value !== 'object') {
return false;
}
const capabilities = value as Record<keyof UiCapabilitySupport, unknown>;
return UI_CAPABILITY_FIELDS.every((field) => typeof capabilities[field] === 'boolean');
}

/**
* Run the probe binary and merge its results into `support`. On any
* failure (binary missing, timeout, malformed JSON, unknown tier), the
Expand All @@ -215,6 +237,12 @@ function populateIsolationFromProbe(support: PlatformSupport): void {
support.isolationWarnings = warnings;
}
}
const facts = probe.probes;
if (facts && typeof facts === 'object') {
if (isUiCapabilitySupport(facts.uiCapabilities)) {
support.uiCapabilities = facts.uiCapabilities;
}
}
}
} catch {
// Graceful degradation: leave isolation fields unset.
Expand All @@ -225,7 +253,8 @@ function computeSupport(): PlatformSupport {
const platform = os.platform();
const support: PlatformSupport = { isSupported: false, reason: '', availableMethods: [] };

// Non-Windows platforms
// Non-Windows platforms do not currently have native probes, so fields that
// depend on probe data (including uiCapabilities) stay omitted.
if (platform === 'darwin') {
// seatbelt is the only containment backend on macOS.
// /usr/bin/sandbox-exec ships with every release of macOS so the check
Expand Down
40 changes: 39 additions & 1 deletion sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,39 @@ export type IsolationTier =
| 'appcontainer-bfs'
| 'appcontainer-dacl';

/**
* Host support for enforcing sandbox UI restrictions.
*
* The fields describe platform-agnostic restriction intents, not the
* OS-specific primitive used to enforce them. For example, Windows derives
* these values from `JOB_OBJECT_UILIMIT_*` support. The SDK currently
* receives this object only from the Windows native probe; other platforms
* omit `PlatformSupport.uiCapabilities` until they expose equivalent probe
* data.
*/
export interface UiCapabilitySupport {
Comment thread
MGudgin marked this conversation as resolved.
Comment thread
MGudgin marked this conversation as resolved.
/** Whether the host can block reads from the clipboard. */
canBlockClipboardRead: boolean;
/** Whether the host can block writes to the clipboard. */
canBlockClipboardWrite: boolean;
/** Whether the host can block synthetic keyboard/mouse input. */
canBlockInputInjection: boolean;
/** Whether the host can block input method / IME changes. */
canBlockInputMethodChanges: boolean;
/** Whether the host can block access to external UI object handles. */
canBlockExternalUiObjects: boolean;
/** Whether the host can block access to global UI namespaces. */
canBlockGlobalUiNamespace: boolean;
/** Whether the host can block desktop switching. */
canBlockDesktopSwitching: boolean;
/** Whether the host can block logoff or shutdown requests. */
canBlockLogoffOrShutdown: boolean;
/** Whether the host can block system parameter changes. */
canBlockSystemParameterChanges: boolean;
/** Whether the host can block display settings changes. */
canBlockDisplaySettingsChanges: boolean;
}

/**
* Platform support information
*/
Expand All @@ -427,4 +460,9 @@ export interface PlatformSupport {
* Omitted on non-Windows platforms or when the probe fails.
*/
isolationWarnings?: string[];
}
/**
* Host UI-restriction capabilities. Omitted when the backend probe cannot
* determine them, including on Linux and macOS today.
*/
uiCapabilities?: UiCapabilitySupport;
}
113 changes: 113 additions & 0 deletions sdk/tests/unit/platform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ import {

const isWindows = os.platform() === 'win32';

const allUiCapabilities = {
canBlockClipboardRead: true,
canBlockClipboardWrite: true,
canBlockInputInjection: true,
canBlockInputMethodChanges: true,
canBlockExternalUiObjects: true,
canBlockGlobalUiNamespace: true,
canBlockDesktopSwitching: true,
canBlockLogoffOrShutdown: true,
canBlockSystemParameterChanges: true,
canBlockDisplaySettingsChanges: true,
};

describe('getPlatformSupport probe integration', () => {
beforeEach(() => {
_resetPlatformSupportCache();
Expand Down Expand Up @@ -97,6 +110,7 @@ describe('getPlatformSupport probe integration', () => {
const support = getPlatformSupport();
assert.strictEqual(support.isolationTier, undefined);
assert.strictEqual(support.isolationWarnings, undefined);
assert.strictEqual(support.uiCapabilities, undefined);
assert.ok(Array.isArray(support.availableMethods));
});

Expand Down Expand Up @@ -169,6 +183,105 @@ describe('getPlatformSupport probe integration', () => {
assert.strictEqual(support.isolationWarnings, undefined, `payload=${payload}`);
}
});

it('surfaces portable UI capabilities from probes', { skip: !isWindows }, () => {
_setProbeRunner(() =>
JSON.stringify({
tier: 'appcontainer-dacl',
probes: {
baseContainerApiPresent: false,
bfscfgPresent: false,
uiCapabilities: allUiCapabilities,
},
}),
);
const support = getPlatformSupport();
if (!support.isSupported) return;
assert.deepStrictEqual(support.uiCapabilities, allUiCapabilities);
});

it('reports input-injection blocking unsupported from probe capabilities', { skip: !isWindows }, () => {
_setProbeRunner(() =>
JSON.stringify({
tier: 'appcontainer-dacl',
probes: {
baseContainerApiPresent: false,
bfscfgPresent: false,
uiCapabilities: {
...allUiCapabilities,
canBlockInputInjection: false,
},
},
}),
);
const support = getPlatformSupport();
if (!support.isSupported) return;
assert.strictEqual(support.uiCapabilities?.canBlockInputInjection, false);
assert.strictEqual(support.uiCapabilities?.canBlockInputMethodChanges, true);
});

it('reports input-method and input-injection blocking unsupported from probe capabilities', { skip: !isWindows }, () => {
_setProbeRunner(() =>
JSON.stringify({
tier: 'appcontainer-dacl',
probes: {
baseContainerApiPresent: false,
bfscfgPresent: false,
uiCapabilities: {
...allUiCapabilities,
canBlockInputInjection: false,
canBlockInputMethodChanges: false,
},
},
}),
);
const support = getPlatformSupport();
if (!support.isSupported) return;
assert.strictEqual(support.uiCapabilities?.canBlockInputInjection, false);
assert.strictEqual(support.uiCapabilities?.canBlockInputMethodChanges, false);
assert.strictEqual(support.uiCapabilities?.canBlockClipboardRead, true);
assert.strictEqual(support.uiCapabilities?.canBlockDisplaySettingsChanges, true);
});

it('omits UI capabilities when probes block is absent', { skip: !isWindows }, () => {
_setProbeRunner(() => JSON.stringify({ tier: 'appcontainer-dacl' }));
const support = getPlatformSupport();
if (!support.isSupported) return;
assert.strictEqual(support.uiCapabilities, undefined);
});

it('omits UI capabilities when probe omits them', { skip: !isWindows }, () => {
_setProbeRunner(() =>
JSON.stringify({
tier: 'appcontainer-dacl',
probes: {
baseContainerApiPresent: false,
bfscfgPresent: false,
},
}),
);
const support = getPlatformSupport();
if (!support.isSupported) return;
assert.strictEqual(support.uiCapabilities, undefined);
});

it('omits UI capabilities when probe returns a partial capability object', { skip: !isWindows }, () => {
_setProbeRunner(() =>
JSON.stringify({
tier: 'appcontainer-dacl',
probes: {
baseContainerApiPresent: false,
bfscfgPresent: false,
uiCapabilities: {
canBlockClipboardRead: true,
},
},
}),
);
const support = getPlatformSupport();
if (!support.isSupported) return;
assert.strictEqual(support.uiCapabilities, undefined);
});
});

// findWxcExecutable failure-mode: the SDK's default probe runner calls
Expand Down
46 changes: 45 additions & 1 deletion src/backends/appcontainer/common/src/job_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,47 @@ fn supported_ui_limit_mask_for_build(build: u32) -> u32 {
supported
}

#[inline(always)]
fn has_ui_limit(mask: u32, flag: u32) -> bool {
Comment thread
MGudgin marked this conversation as resolved.
Comment thread
MGudgin marked this conversation as resolved.
(mask & flag) == flag
}

fn supported_ui_restrictions_for_build(build: u32) -> EffectiveUiRestrictions {
let supported = supported_ui_limit_mask_for_build(build);
EffectiveUiRestrictions {
block_clipboard_read: has_ui_limit(supported, JOB_OBJECT_UILIMIT_READCLIPBOARD.0),
block_clipboard_write: has_ui_limit(supported, JOB_OBJECT_UILIMIT_WRITECLIPBOARD.0),
block_input_injection: has_ui_limit(supported, JOB_OBJECT_UILIMIT_INJECTION),
block_input_method_changes: has_ui_limit(supported, JOB_OBJECT_UILIMIT_IME),
block_external_ui_objects: has_ui_limit(supported, JOB_OBJECT_UILIMIT_HANDLES.0),
block_global_ui_namespace: has_ui_limit(supported, JOB_OBJECT_UILIMIT_GLOBALATOMS.0),
block_desktop_switching: has_ui_limit(supported, JOB_OBJECT_UILIMIT_DESKTOP.0),
block_logoff_or_shutdown: has_ui_limit(supported, JOB_OBJECT_UILIMIT_EXITWINDOWS.0),
block_system_parameter_changes: has_ui_limit(
supported,
JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS.0,
),
block_display_settings_changes: has_ui_limit(
supported,
JOB_OBJECT_UILIMIT_DISPLAYSETTINGS.0,
),
}
}

/// Returns the subset of encoder-defined `JOB_OBJECT_UILIMIT_*` flags the
/// current OS build can enforce. The effective restriction mask applied to a
/// job is always `requested & supported`, so the kernel is never handed a
/// flag it would reject. Reported to callers via `wxc-exec --probe`.
/// flag it would reject.
pub fn supported_ui_limit_mask() -> u32 {
supported_ui_limit_mask_for_build(os_build_number())
}

/// Returns the platform-agnostic UI restrictions the current OS build can
/// enforce. Reported to callers via `wxc-exec --probe`.
pub fn supported_ui_restrictions() -> EffectiveUiRestrictions {
supported_ui_restrictions_for_build(os_build_number())
}

/// Encode platform-agnostic UI restrictions as the `JOB_OBJECT_UILIMIT_*`
/// bitmask consumed by `SetInformationJobObject(JobObjectBasicUIRestrictions)`
/// and by the BaseContainer SandboxSpec `ui_restrictions` field.
Expand Down Expand Up @@ -382,6 +415,17 @@ mod tests {
assert_eq!(mask, 0x00FF);
}

#[test]
fn supported_restrictions_match_downlevel_mask() {
let restrictions = supported_ui_restrictions_for_build(20348);
assert!(restrictions.block_clipboard_read);
assert!(restrictions.block_clipboard_write);
assert!(restrictions.block_external_ui_objects);
assert!(restrictions.block_display_settings_changes);
assert!(!restrictions.block_input_injection);
assert!(!restrictions.block_input_method_changes);
}

#[test]
fn supported_mask_keeps_ime_on_22h2() {
// Build 22621 (22H2) and later support the IME flag.
Expand Down
Loading
Loading