Description of the new feature / enhancement
Summary
On a stock release binary running on a build that falls back to Tier 3 (AppContainer + DACL), granting a read-only path that points at a large directory tree (e.g. a Python install with Lib\site-packages) takes ~35 seconds per run, and the cost is paid on every single invocation because the ACE is applied on entry and reverted on exit.
The slow part is not MXC's own code — it is the Windows SetNamedSecurityInfoW call propagating an OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE ACE across the entire subtree (tens of thousands of files), and then the symmetric revert on teardown via DaclManager's Drop.
For a one-shot-per-command consumer (each command spawns a fresh wxc-exec), this makes Tier 3 effectively unusable for any policy that must grant a large interpreter/library tree.
Environment
- OS: Windows 11, 25H2, build 26200.7840
- Selected tier:
appcontainer-dacl (Tier 3)
- BaseContainer API (Tier 1) not present on this build
- AppContainer + BFS (Tier 2) not compiled into the release binary, so the dispatcher falls straight from Tier 1 to Tier 3
- Higher-isolation backends are not reachable on this build:
isolation_session → 0x80040154 REGDB_E_CLASSNOTREG (needs 26300.8553+)
windows_sandbox → reports feature not enabled; also subject to the documented 26100+ boot regression
This combination (downlevel build + stock binary) is exactly the case that lands on Tier 3 with no faster path available.
Reproduction
config.json:
{
"version": "0.6.0-alpha",
"process": {
"commandLine": "python -c \"import sys; print(sys.version)\""
},
"filesystem": {
"readonlyPaths": ["C:\\Users\\<user>\\AppData\\Local\\Programs\\Python\\Python312"]
}
}
Measure-Command { .\wxc-exec.exe config.json }
Measured timings (same machine)
| Config |
Wall time |
readonlyPaths = full Python312 (incl. Lib\site-packages) |
~35 s, every run |
| same config, run 3x back-to-back |
35 s / 39 s / 42 s (no decay — not a one-time cold-start) |
readonlyPaths = a small directory (a few files) |
sub-second |
no filesystem section at all |
~290 ms (Runner completed in ~290ms) |
full Python312 after physically moving site-packages out |
drops to a few seconds |
The last two rows isolate the cause: the cost scales with the number of files under the granted path, consistent with per-descendant security-descriptor rewrites inside SetNamedSecurityInfoW, not with anything specific to the interpreter.
Note: Runner completed in <n>ms (the internal timer) stays small throughout; the dominant cost is outside that timer — in the apply step before the child runs and the revert after it exits.
Why pre-granting doesn't help today
A natural workaround is to pre-stamp a permanent ACE for the AppContainer SID on the large tree once (via icacls), so MXC wouldn't need to touch it. This does not help with the current code: apply_one in core/wxc_common/src/filesystem_dacl.rs calls scan_explicit_aces_for_sid (which already reads the target's own DACL cheaply, no recursion) but only uses the result for restore bookkeeping — it always re-applies the grant regardless of whether an equivalent explicit ACE is already present. So the expensive SetNamedSecurityInfoW propagation runs every time even when the grant is redundant.
Separately, lifecycle.preserve_policy does not cover the Tier 3 DACL path: in appcontainer_runner.rs::teardown it only gates network and BFS (Tier 2) cleanup. The Tier 3 ACE revert is driven unconditionally by DaclManager's Drop (parked via park_dacl_for_cleanup in core/wxc/src/main.rs), so there is no supported way to keep a grant across runs.
Proposed technical implementation details
Two small, opt-in additions that together let an operator pay the DACL cost once instead of per-run, without weakening the default behavior:
-
Skip-if-present (apply side). In apply_one, after scan_explicit_aces_for_sid, if an existing explicit Allow ACE for the same SID already covers the requested mask with matching inheritance flags, skip the apply (and, having recorded nothing, skip the corresponding restore). Gate behind an env var (e.g. MXC_SKIP_IF_PRESENT=1) so default behavior is unchanged. Only superset Allow grants qualify; deny ACEs are never skipped.
-
Honor preserve_policy for Tier 3 (teardown side). When lifecycle.preserve_policy = true, do not revert the Tier 3 grants on exit (i.e. don't let DaclManager Drop strip them). Combined with (1), a subsequent run detects the still-present ACE and pays nothing.
Operator workflow with both in place:
# once: permanent inheritable read ACE for the AppContainer SID
icacls "<...>\Lib\site-packages" /grant "*S-1-15-2-...:(OI)(CI)(RX)" /T
# then every run is fast; new packages added later inherit the parent ACE automatically
set MXC_SKIP_IF_PRESENT=1
wxc-exec.exe config.json
This stays within Tier 3, requires no OS upgrade, and is purely additive (both paths are off unless explicitly requested).
Offer to contribute
I have a working draft of change (1) against core/wxc_common/src/filesystem_dacl.rs and have traced the teardown path for (2). I'm happy to open a PR for both if the approach sounds acceptable — in particular I'd like maintainer guidance on:
- whether to gate via env var vs. a config field (e.g.
filesystem.skipIfPresent),
- the exact "covers the requested mask" comparison you'd want (superset vs. exact), and
- whether honoring
preserve_policy for Tier 3 grants is desirable or if you'd prefer a separate, more explicit flag to avoid surprising existing callers.
Questions for maintainers
- Is the per-run full-subtree ACE propagation considered expected for Tier 3, or is reducing it (e.g. shallower inheritance, or skip-if-present) something you'd accept?
- Is there any intended downlevel path to "provision once / exec many" short of
isolation_session, which is gated to 26300.8553+?
Description of the new feature / enhancement
Summary
On a stock release binary running on a build that falls back to Tier 3 (AppContainer + DACL), granting a read-only path that points at a large directory tree (e.g. a Python install with
Lib\site-packages) takes ~35 seconds per run, and the cost is paid on every single invocation because the ACE is applied on entry and reverted on exit.The slow part is not MXC's own code — it is the Windows
SetNamedSecurityInfoWcall propagating anOBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACEACE across the entire subtree (tens of thousands of files), and then the symmetric revert on teardown viaDaclManager'sDrop.For a one-shot-per-command consumer (each command spawns a fresh
wxc-exec), this makes Tier 3 effectively unusable for any policy that must grant a large interpreter/library tree.Environment
appcontainer-dacl(Tier 3)isolation_session→0x80040154 REGDB_E_CLASSNOTREG(needs 26300.8553+)windows_sandbox→ reports feature not enabled; also subject to the documented 26100+ boot regressionThis combination (downlevel build + stock binary) is exactly the case that lands on Tier 3 with no faster path available.
Reproduction
config.json:{ "version": "0.6.0-alpha", "process": { "commandLine": "python -c \"import sys; print(sys.version)\"" }, "filesystem": { "readonlyPaths": ["C:\\Users\\<user>\\AppData\\Local\\Programs\\Python\\Python312"] } }Measured timings (same machine)
readonlyPaths= fullPython312(incl.Lib\site-packages)readonlyPaths= a small directory (a few files)filesystemsection at allRunner completed in ~290ms)Python312after physically movingsite-packagesoutThe last two rows isolate the cause: the cost scales with the number of files under the granted path, consistent with per-descendant security-descriptor rewrites inside
SetNamedSecurityInfoW, not with anything specific to the interpreter.Note:
Runner completed in <n>ms(the internal timer) stays small throughout; the dominant cost is outside that timer — in the apply step before the child runs and the revert after it exits.Why pre-granting doesn't help today
A natural workaround is to pre-stamp a permanent ACE for the AppContainer SID on the large tree once (via
icacls), so MXC wouldn't need to touch it. This does not help with the current code:apply_oneincore/wxc_common/src/filesystem_dacl.rscallsscan_explicit_aces_for_sid(which already reads the target's own DACL cheaply, no recursion) but only uses the result for restore bookkeeping — it always re-applies the grant regardless of whether an equivalent explicit ACE is already present. So the expensiveSetNamedSecurityInfoWpropagation runs every time even when the grant is redundant.Separately,
lifecycle.preserve_policydoes not cover the Tier 3 DACL path: inappcontainer_runner.rs::teardownit only gates network and BFS (Tier 2) cleanup. The Tier 3 ACE revert is driven unconditionally byDaclManager'sDrop(parked viapark_dacl_for_cleanupincore/wxc/src/main.rs), so there is no supported way to keep a grant across runs.Proposed technical implementation details
Two small, opt-in additions that together let an operator pay the DACL cost once instead of per-run, without weakening the default behavior:
Skip-if-present (apply side). In
apply_one, afterscan_explicit_aces_for_sid, if an existing explicit Allow ACE for the same SID already covers the requested mask with matching inheritance flags, skip the apply (and, having recorded nothing, skip the corresponding restore). Gate behind an env var (e.g.MXC_SKIP_IF_PRESENT=1) so default behavior is unchanged. Only superset Allow grants qualify; deny ACEs are never skipped.Honor
preserve_policyfor Tier 3 (teardown side). Whenlifecycle.preserve_policy = true, do not revert the Tier 3 grants on exit (i.e. don't letDaclManagerDrop strip them). Combined with (1), a subsequent run detects the still-present ACE and pays nothing.Operator workflow with both in place:
This stays within Tier 3, requires no OS upgrade, and is purely additive (both paths are off unless explicitly requested).
Offer to contribute
I have a working draft of change (1) against
core/wxc_common/src/filesystem_dacl.rsand have traced the teardown path for (2). I'm happy to open a PR for both if the approach sounds acceptable — in particular I'd like maintainer guidance on:filesystem.skipIfPresent),preserve_policyfor Tier 3 grants is desirable or if you'd prefer a separate, more explicit flag to avoid surprising existing callers.Questions for maintainers
isolation_session, which is gated to 26300.8553+?