Skip to content

Tier 3 (AppContainer + DACL): repeated full-subtree ACE apply/restore makes large read-only paths unusable on downlevel builds #572

Description

@xgdyp

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_session0x80040154 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:

  1. 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.

  2. 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

  1. 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?
  2. Is there any intended downlevel path to "provision once / exec many" short of isolation_session, which is gated to 26300.8553+?

Metadata

Metadata

Assignees

No one assigned

    Fields

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions