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

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

// 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, 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, 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:

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:

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:

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: Implements DaclManager, crash-safe state files, ACE application logic, and orphaned-state recovery via recover_orphaned_state().

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

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →