Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/api-reference/node/v1/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,7 @@ Options for getAvailableToolsPolicy.
```typescript
export interface ToolsPolicyOptions {
containerType?: 'processcontainer';
allowPowerShellDriveRootRead?: boolean;
}
```

Expand Down
8 changes: 8 additions & 0 deletions docs/api-reference/rust/v1/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ Discover tool and SDK directories from environment (defaults to the process envi
pub fn available_tools_policy(environment: Option<&[(String, String)]>, options: ToolsPolicyOptions) -> FilesystemPolicyResult;
```

## `mxc_sdk::v1::policy::filesystem::available_tools_policy_with_powershell_drive_root_read`

Discover tool and SDK directories while opting into the legacy PowerShell system-drive compatibility grant.

```rust
pub fn available_tools_policy_with_powershell_drive_root_read(environment: Option<&[(String, String)]>, options: ToolsPolicyOptions) -> FilesystemPolicyResult;
```


## `mxc_sdk::v1::container::deprovision_container`

Expand Down
26 changes: 23 additions & 3 deletions sdk/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,26 @@ Filesystem discovery helpers and their result/options types are grouped under
`ToolsPolicyOptions`; set `containerType: 'processcontainer'` to exclude
directories with ALL APPLICATION PACKAGES access on Windows. ACL inspection
is bounded to five seconds per directory; failures retain the directory and
emit a diagnostic warning. `getUserProfilePolicy` uses the supplied environment,
and `getTemporaryFilesPolicy` returns existing temporary storage without creating
directories.
emit a diagnostic warning. When `pwsh.exe` is on `PATH`, discovery adds the
PSReadLine history directory to `readwritePaths` and leaves the system-drive
root out of `readonlyPaths`. Set `allowPowerShellDriveRootRead: true` to
restore that root for callers who accept the compatibility grant. On
BaseContainer, a drive-root `readonlyPaths` entry permits recursive reads of
the whole drive, including unrelated user files. This option controls only the
synthetic root added for PowerShell compatibility; a drive root explicitly
supplied through `PATH` or another discovered environment variable is still
returned as a tool path. `getUserProfilePolicy` uses the supplied environment,
and `getTemporaryFilesPolicy` returns existing temporary storage without
creating directories.

PowerShell startup may still need to stat the system-drive root. Use explicit
host preparation or tier-supported configuration for that check.
[`wxc-host-prep prepare-system-drive`](https://github.com/microsoft/mxc/blob/main/docs/backends/process-container/host-prep.md)
adds persistent, non-inheriting metadata ACEs on the drive root for the
AppContainer package SIDs: `FILE_READ_ATTRIBUTES`, `FILE_READ_EA`,
`READ_CONTROL`, and `SYNCHRONIZE`. The ACEs apply only to the root itself.
They include no directory-enumeration right, no file-content read, and no
write. Where the isolation tier supports a narrower query grant, set
`enumeratePaths` on ProcessContainer containment configuration.
`FilesystemPolicyResult` stays limited to `readonlyPaths` and `readwritePaths`,
so this helper cannot carry `enumeratePaths` for every tier.
52 changes: 41 additions & 11 deletions sdk/node/src/v1/policy/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ export interface ToolsPolicyOptions {
* because AppContainer processes can already see them implicitly.
*/
containerType?: 'processcontainer';

/**
* When `true`, and `pwsh.exe` is found on PATH, add the system-drive root
* (`SystemDrive`, falling back to `C:`) to `readonlyPaths`.
*
* Defaults to `false`. Opting in permits recursive whole-drive reads on
* BaseContainer. PowerShell startup that only needs to stat the drive root
* should use host preparation or tier-supported configuration instead. A
* drive root explicitly supplied through PATH or another discovered
* environment variable remains in `readonlyPaths`.
*/
allowPowerShellDriveRootRead?: boolean;
Comment on lines +44 to +50
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -205,19 +217,25 @@ function deduplicatePaths(paths: string[]): string[] {
* the supplied PATH directories for a `pwsh.exe` binary.
*
* When PowerShell is found, return a policy fragment with:
* - `C:\` in `readonlyPaths` β€” pwsh.exe enumerates the drive root on startup.
* - The PSReadLine history directory in `readwritePaths` so the PSReadLine
* module can persist command history.
* - The system-drive root in `readonlyPaths` only when
* `allowPowerShellDriveRootRead` is true. The root comes from `SystemDrive`,
* falling back to `C:`. That opt-in permits recursive whole-drive reads on
* BaseContainer. The default omits the root; PowerShell startup may need
* explicit host preparation or tier-supported configuration instead.
*
* On non-Windows platforms or when pwsh.exe is not found on PATH, returns an
* empty policy.
* empty policy. The opt-in does not add a drive root in either case.
*
* @param pathDirs - The list of PATH directories already collected by the caller.
* @param environment - Environment variable map.
* @param allowPowerShellDriveRootRead - Whether to add the synthetic system-drive root.
*/
function getPowerShellPolicy(
pathDirs: string[],
environment: { [key: string]: string | undefined },
allowPowerShellDriveRootRead: boolean,
): FilesystemPolicyResult {
if (os.platform() !== 'win32') {
return { readonlyPaths: [], readwritePaths: [] };
Expand All @@ -235,9 +253,11 @@ function getPowerShellPolicy(
return { readonlyPaths: [], readwritePaths: [] };
}

const systemDrive = environmentValue(environment, 'SystemDrive') || 'C:';
const systemRoot = systemDrive + "\\";
const readonlyPaths: string[] = [systemRoot];
const readonlyPaths: string[] = [];
if (allowPowerShellDriveRootRead) {
const systemDrive = environmentValue(environment, 'SystemDrive') || 'C:';
readonlyPaths.push(systemDrive + "\\");
}
const readwritePaths: string[] = [];

const userProfile = environmentValue(environment, 'USERPROFILE');
Expand Down Expand Up @@ -268,10 +288,15 @@ function getPowerShellPolicy(
* already grant access to `ALL_APPLICATION_PACKAGES` are removed because
* AppContainer processes can see them without explicit brokering.
*
* Additionally, if PowerShell (`pwsh.exe`) is found on PATH, the drive root
* (`C:\`) is added to `readonlyPaths` and the PSReadLine history directory
* is added to `readwritePaths` so that interactive PowerShell sessions work
* correctly inside the container.
* Additionally, if PowerShell (`pwsh.exe`) is found on PATH, the PSReadLine
* history directory is added to `readwritePaths`. The system-drive root is
* added to `readonlyPaths` only when `options.allowPowerShellDriveRootRead`
* is true (the default is `false`). That opt-in permits recursive whole-drive
* reads on BaseContainer. PowerShell startup that stats the drive root may
* need explicit host preparation or tier-supported configuration; the opt-in
* is the compatibility path for callers who accept the broad read. This option
* controls only the synthetic PowerShell grant; an explicitly discovered drive
* root remains in the result.
*
* ACL inspection failures retain the directory and emit a diagnostic warning.
*
Expand Down Expand Up @@ -316,8 +341,13 @@ export function getAvailableToolsPolicy(
return true;
});

// Merge PowerShell-specific paths when pwsh.exe is available
const pwshPolicy = getPowerShellPolicy(pathDirs, environment);
// Merge PowerShell-specific paths when pwsh.exe is available.
// The drive-root read is opt-in; tool directories and PSReadLine are not.
const pwshPolicy = getPowerShellPolicy(
pathDirs,
environment,
options?.allowPowerShellDriveRootRead === true,
Comment on lines +346 to +349
);

return {
readonlyPaths: deduplicatePaths([...filtered, ...pwshPolicy.readonlyPaths]),
Expand Down
152 changes: 108 additions & 44 deletions sdk/node/tests/unit/policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ describe('filesystem helper environment and options', () => {

it('does not substitute the host environment for an empty map', () => {
assert.deepStrictEqual(getAvailableToolsPolicy({}), { readonlyPaths: [], readwritePaths: [] });
assert.deepStrictEqual(
getAvailableToolsPolicy({}, { allowPowerShellDriveRootRead: true }),
{ readonlyPaths: [], readwritePaths: [] },
);
assert.deepStrictEqual(getUserProfilePolicy({}), { readonlyPaths: [], readwritePaths: [] });
});

Expand Down Expand Up @@ -68,15 +72,55 @@ describe('filesystem helper environment and options', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mxc-tools & paths-'));
try {
execFileSync('icacls.exe', [root, '/grant', '*S-1-15-2-1:(OI)(CI)(RX)'], { windowsHide: true });
fs.writeFileSync(path.join(root, 'pwsh.exe'), '');
const environment = { path: root };
const driveRoot = path.resolve('C:\\');
assert.deepStrictEqual(getAvailableToolsPolicy(environment).readonlyPaths, [root]);
assert.deepStrictEqual(
getAvailableToolsPolicy(environment, { containerType: 'processcontainer' }).readonlyPaths, [],
);
// containerType filters ACLs and does not opt in to the drive-root read.
assert.deepStrictEqual(
getAvailableToolsPolicy(environment, {
containerType: 'processcontainer',
allowPowerShellDriveRootRead: false,
}).readonlyPaths,
[],
);
assert.deepStrictEqual(
getAvailableToolsPolicy(environment, {
containerType: 'processcontainer',
allowPowerShellDriveRootRead: true,
}).readonlyPaths,
[driveRoot],
);
} finally {
fs.rmSync(root, { recursive: true });
}
});

it('preserves a drive root explicitly supplied on PATH', { skip: process.platform !== 'win32' }, () => {
const pwshDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mxc-test-'));
try {
fs.writeFileSync(path.join(pwshDir, 'pwsh.exe'), '');
const driveRoot = path.resolve((process.env['SystemDrive'] || 'C:') + '\\');
assert.strictEqual(fs.statSync(driveRoot).isDirectory(), true);
const environment = { PATH: `${driveRoot};${pwshDir}`, USERPROFILE: 'C:\\Users\\TestUser' };
const history = path.resolve(path.join(
'C:\\Users\\TestUser', 'AppData', 'Roaming', 'Microsoft', 'Windows', 'PowerShell', 'PSReadLine',
));
const result = getAvailableToolsPolicy(environment);
const toolDir = path.resolve(pwshDir);
assert.deepStrictEqual(result.readonlyPaths, [driveRoot, toolDir]);
assert.deepStrictEqual(result.readwritePaths, [history]);
assert.deepStrictEqual(
getAvailableToolsPolicy({ PATH: pwshDir, USERPROFILE: 'C:\\Users\\TestUser' }).readonlyPaths,
[toolDir],
);
} finally {
fs.rmSync(pwshDir, { recursive: true, force: true });
}
});
});

describe('getAvailableToolsPolicy - PowerShell discovery', () => {
Expand All @@ -100,6 +144,12 @@ describe('getAvailableToolsPolicy - PowerShell discovery', () => {
return tmpDir;
};

const psReadLine = (profile: string): string => path.resolve(path.join(
profile, 'AppData', 'Roaming', 'Microsoft', 'Windows', 'PowerShell', 'PSReadLine',
));

const driveRoot = (drive: string): string => path.resolve(drive + '\\');

afterEach(() => {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform);
Expand All @@ -111,69 +161,83 @@ describe('getAvailableToolsPolicy - PowerShell discovery', () => {
}
});

it('should add system root to readonlyPaths when pwsh.exe is on PATH', { skip: isLinux }, () => {
it('should keep the tool directory and PSReadLine history without a drive root', { skip: isLinux }, () => {
mockWindows();
const pwshDir = createFakePwshDir();
const env = { PATH: pwshDir, USERPROFILE: 'C:\\Users\\TestUser' };
const result = getAvailableToolsPolicy(env);
assert.ok(
result.readonlyPaths.some(p => /^[a-z]:\\$/i.test(p)),
'System root (e.g. C:\\) should be in readonlyPaths when pwsh.exe is on PATH',
);
const toolDir = path.resolve(pwshDir);
const history = psReadLine('C:\\Users\\TestUser');
const optionSets = [undefined, {}, { allowPowerShellDriveRootRead: false }] as const;
for (const options of optionSets) {
const result = getAvailableToolsPolicy(env, options);
assert.deepStrictEqual(result.readonlyPaths, [toolDir]);
assert.deepStrictEqual(result.readwritePaths, [history]);
}
});

it('should add PSReadLine dir to readwritePaths when pwsh.exe is on PATH', { skip: isLinux }, () => {
it('should restore the drive root only when allowPowerShellDriveRootRead is true', { skip: isLinux }, () => {
mockWindows();
const pwshDir = createFakePwshDir();
const env = { PATH: pwshDir, USERPROFILE: 'C:\\Users\\TestUser' };
const result = getAvailableToolsPolicy(env);
const expected = path.join(
'C:\\Users\\TestUser', 'AppData', 'Roaming', 'Microsoft', 'Windows', 'PowerShell', 'PSReadLine',
const toolDir = path.resolve(pwshDir);
const history = psReadLine('C:\\Users\\TestUser');

const nonDefaultDrive = getAvailableToolsPolicy(
{ PATH: pwshDir, SystemDrive: 'D:', USERPROFILE: 'C:\\Users\\TestUser' },
{ allowPowerShellDriveRootRead: true },
);
assert.ok(
result.readwritePaths.some(p => p.toLowerCase() === expected.toLowerCase()),
'PSReadLine directory should be in readwritePaths',
assert.deepStrictEqual(nonDefaultDrive.readonlyPaths, [toolDir, driveRoot('D:')]);
assert.deepStrictEqual(nonDefaultDrive.readwritePaths, [history]);

for (const systemDrive of [undefined, '']) {
const environment: { [key: string]: string | undefined } = {
PATH: pwshDir,
USERPROFILE: 'C:\\Users\\TestUser',
};
if (systemDrive !== undefined) {
environment['SystemDrive'] = systemDrive;
}
const fallback = getAvailableToolsPolicy(environment, { allowPowerShellDriveRootRead: true });
assert.deepStrictEqual(fallback.readonlyPaths, [toolDir, driveRoot('C:')]);
assert.deepStrictEqual(fallback.readwritePaths, [history]);
}

const caseInsensitive = getAvailableToolsPolicy(
{ path: pwshDir, systemdrive: 'E:', userprofile: 'C:\\Users\\Other' },
{ allowPowerShellDriveRootRead: true },
);
assert.deepStrictEqual(caseInsensitive.readonlyPaths, [toolDir, driveRoot('E:')]);
assert.deepStrictEqual(caseInsensitive.readwritePaths, [psReadLine('C:\\Users\\Other')]);
});

it('should not add PowerShell paths when pwsh.exe is not on PATH', { skip: isLinux }, () => {
it('should not add a drive root without pwsh.exe even when opted in', { skip: isLinux }, () => {
mockWindows();
const env = { PATH: 'C:\\Windows\\System32', USERPROFILE: 'C:\\Users\\TestUser' };
const result = getAvailableToolsPolicy(env);
assert.ok(
!result.readonlyPaths.some(p => /^[a-z]:\\$/i.test(p)),
'System root should not be in readonlyPaths when pwsh.exe is not on PATH',
);
assert.strictEqual(result.readwritePaths.length, 0,
'readwritePaths should be empty when pwsh.exe is not on PATH',
);
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mxc-test-'));
const env = { PATH: tmpDir, USERPROFILE: 'C:\\Users\\TestUser', SystemDrive: 'D:' };
const result = getAvailableToolsPolicy(env, { allowPowerShellDriveRootRead: true });
assert.deepStrictEqual(result.readonlyPaths, [path.resolve(tmpDir)]);
assert.deepStrictEqual(result.readwritePaths, []);
});

it('should return empty policy on non-Windows even when pwsh.exe is on PATH', { skip: isLinux }, () => {
it('should return no PowerShell grant on non-Windows even when opted in', { skip: isLinux }, () => {
mockLinux();
const pwshDir = createFakePwshDir();
const env = { PATH: pwshDir, USERPROFILE: 'C:\\Users\\TestUser' };
const result = getAvailableToolsPolicy(env);
assert.ok(
!result.readonlyPaths.some(p => /^[a-z]:\\$/i.test(p)),
'System root (e.g. C:\\) should not be in readonlyPaths on Linux',
);
assert.strictEqual(result.readwritePaths.length, 0,
'readwritePaths should be empty on Linux',
);
const env = { PATH: pwshDir, USERPROFILE: 'C:\\Users\\TestUser', SystemDrive: 'D:' };
const result = getAvailableToolsPolicy(env, { allowPowerShellDriveRootRead: true });
assert.ok(!result.readonlyPaths.includes(driveRoot('D:')));
assert.ok(!result.readonlyPaths.includes(driveRoot('C:')));
assert.deepStrictEqual(result.readwritePaths, []);
});

it('should not add PSReadLine path when USERPROFILE is not set', { skip: isLinux }, () => {
it('should not add PSReadLine history when USERPROFILE is not set', { skip: isLinux }, () => {
mockWindows();
const pwshDir = createFakePwshDir();
const env = { PATH: pwshDir };
const result = getAvailableToolsPolicy(env);
assert.ok(
result.readonlyPaths.some(p => /^[a-z]:\\$/i.test(p)),
'System root should still be in readonlyPaths',
);
assert.strictEqual(result.readwritePaths.length, 0,
'readwritePaths should be empty without USERPROFILE',
);
const env = { PATH: pwshDir, SystemDrive: 'D:' };
const toolDir = path.resolve(pwshDir);
const withoutRoot = getAvailableToolsPolicy(env, { allowPowerShellDriveRootRead: false });
assert.deepStrictEqual(withoutRoot.readonlyPaths, [toolDir]);
assert.deepStrictEqual(withoutRoot.readwritePaths, []);
const withRoot = getAvailableToolsPolicy(env, { allowPowerShellDriveRootRead: true });
assert.deepStrictEqual(withRoot.readonlyPaths, [toolDir, driveRoot('D:')]);
assert.deepStrictEqual(withRoot.readwritePaths, []);
});
});
11 changes: 8 additions & 3 deletions src/mxc-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,14 @@ Filesystem discovery helpers and their result/options types live under
`container_type: Some(ToolsPolicyContainerType::ProcessContainer)` to exclude
directories with ALL APPLICATION PACKAGES access on Windows. ACL inspection is
bounded to five seconds per directory; failures retain the directory and use
the configured diagnostic sink. `user_profile_policy` discovers standard
per-user tool locations, and `temporary_files_policy` returns existing
temporary storage without creating directories.
the configured diagnostic sink. PowerShell discovery omits the synthetic
system-drive root by default; use
`available_tools_policy_with_powershell_drive_root_read` only when recursive
whole-drive reads on BaseContainer are acceptable. A drive root explicitly
supplied through `PATH` or another discovered environment variable is still
returned as a tool path. `user_profile_policy` discovers standard per-user tool
locations, and `temporary_files_policy` returns existing temporary storage
without creating directories.

## Internal modules and executor binaries

Expand Down
5 changes: 3 additions & 2 deletions src/mxc-sdk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,9 @@ pub mod v1 {
/// Host filesystem-policy discovery helpers and their options.
pub mod filesystem {
pub use crate::policy::{
available_tools_policy, temporary_files_policy, user_profile_policy,
FilesystemPolicyResult, ToolsPolicyContainerType, ToolsPolicyOptions,
available_tools_policy, available_tools_policy_with_powershell_drive_root_read,
temporary_files_policy, user_profile_policy, FilesystemPolicyResult,
ToolsPolicyContainerType, ToolsPolicyOptions,
};
}

Expand Down
Loading
Loading