# How MXC Handles DACL Cleanup on Windows: Crash-Safe DACL Restoration

> Learn how MXC ensures crash-safe DACL cleanup on Windows. Discover its multi-layered strategy for removing temporary ACEs during normal exits, panics, signals, and TerminateProcess.

- Repository: [Microsoft/mxc](https://github.com/microsoft/mxc)
- Tags: deep-dive
- Published: 2026-06-07

---

**MXC guarantees removal of temporary ACEs from Windows DACLs through a multi-layered cleanup strategy that covers normal exits, panics, console signals, and even TerminateProcess escapes.**

The Microsoft MXC (Windows eXtended Compatibility) sandbox temporarily augments host file-system security by adding Access Control Entries (ACEs) to DACLs (Discretionary Access Control Lists) required for AppContainer exposure. Understanding how MXC handles DACL cleanup on Windows reveals a defense-in-depth approach that ensures no lingering permissions survive process termination.

## The Crash-Safe DaclManager

At the core of MXC's DACL cleanup strategy lies the **`DaclManager`** struct, implemented in [`src/core/wxc_common/src/filesystem_dacl.rs`](https://github.com/microsoft/mxc/blob/main/src/core/wxc_common/src/filesystem_dacl.rs). This manager records every applied ACE in a per-process state file before invoking the Win32 API, creating a durable audit trail of changes.

When the `DaclManager` instance drops, it automatically restores the original DACL using the recorded state. If restoration fails, the manager logs a warning but continues cleanup, ensuring the process doesn't hang during termination. This crash-safe design guarantees that the host system reverts to its original security posture even if the sandbox terminates abnormally.

## Global Parking for Cross-Exit Accessibility

Because `DaclManager` lives inside the sandbox's main process, a standard `process::exit` would bypass its destructor. MXC solves this by parking the manager in a global **`DACL_CLEANUP_SLOT`** defined at the top of [`src/core/wxc/src/main.rs`](https://github.com/microsoft/mxc/blob/main/src/core/wxc/src/main.rs):

```rust
// Global OnceLock<Mutex<Option<DaclManager>>>
static DACL_CLEANUP_SLOT: OnceLock<Mutex<Option<DaclManager>>> = OnceLock::new();

```

This global slot, initialized as a `OnceLock<Mutex<Option<DaclManager>>>`, allows the cleanup mechanism to survive stack unwinding and remain accessible from signal handlers and recovery routines throughout the process lifecycle.

## Three Exit Path Coverage

MXC implements specific safeguards for three distinct termination scenarios to ensure DACL cleanup occurs regardless of how the process ends.

### Normal Exit Path

Before any explicit call to `process::exit`, MXC invokes `drop(take_parked_dacl())` to extract the manager from the global slot and trigger its `Drop` implementation. This explicit drop ensures the restoration logic runs before the process terminates cleanly.

### Panic Unwind Protection

To handle Rust panics, MXC instantiates a **`ParkedDaclGuard`** on the stack within `main`. This guard's `Drop` implementation also calls `take_parked_dacl()`, guaranteeing that a panic-induced stack unwind still triggers the DACL restore operation before the program aborts.

### Console Control Handler (Ctrl-C and System Events)

For user-initiated termination and system events, MXC registers a Windows console-control handler via `SetConsoleCtrlHandler`. The **`dacl_ctrl_handler`** function, defined in [`src/core/wxc/src/main.rs`](https://github.com/microsoft/mxc/blob/main/src/core/wxc/src/main.rs), captures Ctrl-C, console close, logoff, and shutdown events.

When signaled, the handler extracts the parked manager from `DACL_CLEANUP_SLOT` and drops it, running the restoration logic before the default handler calls `ExitProcess`. The handler implements a bounded wait of **≤ 5 seconds** to avoid deadlocking with concurrent `Drop` operations that might still be executing `SetNamedSecurityInfoW` calls.

## Orphaned-State Recovery

Even with comprehensive handler coverage, Windows `TerminateProcess` bypasses all cleanup mechanisms. MXC addresses this through **`recover_orphaned_state()`**, which runs automatically on the next MXC startup.

This function, implemented in [`src/core/wxc_common/src/filesystem_dacl.rs`](https://github.com/microsoft/mxc/blob/main/src/core/wxc_common/src/filesystem_dacl.rs), scans the per-process state directory for leftover state files from previous runs. It re-applies the recorded restores to any orphaned DACL modifications and reports successes or errors, ensuring that even forcibly killed processes leave no permanent security changes.

## Implementation Examples

### Direct DaclManager Usage

While SDK users rarely interact directly with the manager, internal components use patterns like this:

```rust
use wxc_common::filesystem_dacl::{DaclManager, DaclError};

fn augment_and_cleanup(path: &std::path::Path) -> Result<(), DaclError> {
    // Create a manager for this process.
    let mut mgr = DaclManager::new()?;

    // Apply a single explicit ACE (grant read/write/execute).
    mgr.apply_one(path, /*allow=*/ true, /*inherit=*/ true)?;

    // When `mgr` goes out of scope its Drop will restore the original DACL.
    Ok(())
}

```

### Parking for Global Access

The main binary parks the manager to make it accessible across exit paths:

```rust
fn main() {
    // ... MXC init ...

    // If the sandbox needs DACL augmentation:
    let dacl_mgr = wxc_common::filesystem_dacl::DaclManager::new()?;
    // …apply ACEs via the manager…

    // Park it so the Ctrl‑C handler can drop it later.
    park_dacl_for_cleanup(dacl_mgr);

    // Register the handler (idempotent).
    install_dacl_ctrl_handler();

    // Normal execution continues…
    // At the end of `main` we explicitly drop any parked manager.
    drop(take_parked_dacl());
}

```

### Startup Recovery

Fresh MXC instances clean up after potentially crashed predecessors:

```rust
fn main() {
    // Run recovery before any other work.
    if let Err(e) = wxc_common::filesystem_dacl::recover_orphaned_state() {
        eprintln!("DACL recovery failed: {e}");
    }

    // Continue with normal sandbox startup…
}

```

## Key Source Files

The DACL cleanup implementation spans these critical files:

- **[`src/core/wxc_common/src/filesystem_dacl.rs`](https://github.com/microsoft/mxc/blob/main/src/core/wxc_common/src/filesystem_dacl.rs)**: Implements `DaclManager`, crash-safe state files, ACE application logic, and orphaned-state recovery via `recover_orphaned_state()`.

- **[`src/core/wxc/src/main.rs`](https://github.com/microsoft/mxc/blob/main/src/core/wxc/src/main.rs)**: Defines the global `DACL_CLEANUP_SLOT`, coordinates the three exit paths, implements `ParkedDaclGuard`, and registers the `dacl_ctrl_handler` Windows console handler.

- **`tests/scripts/run_dacl_tests.ps1`**: PowerShell validation harness that verifies DACL augmentation and cleanup completeness on Windows systems.

## Summary

MXC's approach to DACL cleanup on Windows employs multiple redundant mechanisms to guarantee security:

- **Crash-safe state files** ensure every ACE change is recorded before application and restored on drop.
- **Global parking slots** prevent bypass of destructors during `process::exit` calls.
- **Stack guards** catch panic unwinds and force cleanup before abort.
- **Console control handlers** intercept Ctrl-C and system shutdown signals to restore DACLs before process termination.
- **Orphaned-state recovery** cleans up leftover modifications from `TerminateProcess` kills on the next startup.

## Frequently Asked Questions

### What happens if MXC crashes during DACL modification?

The `DaclManager` writes state files to disk before applying any ACE changes. If the process crashes, the next MXC startup runs `recover_orphaned_state()`, which scans for these files and restores the original DACLs, leaving no lingering permissions.

### How does MXC handle Ctrl+C or console window closure?

MXC registers a Windows console-control handler (`dacl_ctrl_handler`) via `SetConsoleCtrlHandler`. When Ctrl+C, console close, logoff, or shutdown events occur, the handler extracts the parked `DaclManager` from the global slot and drops it, triggering DACL restoration before the process exits.

### Why does MXC use a global parking slot instead of stack-based cleanup?

Standard `process::exit` bypasses stack destructors. By parking the `DaclManager` in a `OnceLock<Mutex<Option<DaclManager>>>` called `DACL_CLEANUP_SLOT`, MXC ensures the cleanup resource remains accessible from signal handlers and explicit cleanup calls regardless of stack state.

### What prevents deadlocks during console signal handling?

The console control handler uses a bounded wait of ≤ 5 seconds when acquiring the mutex on the parked DACL manager. This prevents deadlocks if a concurrent `Drop` operation is currently executing `SetNamedSecurityInfoW`, ensuring the handler completes before Windows forcibly terminates the process.