How MXC Recovers Orphaned DACL State: Crash-Safe ACL Restoration

MXC recovers orphaned DACL state by scanning crash-safe JSON state files at startup, validating process liveness via PID and image name verification, and automatically reverting access control entries for terminated processes before any new sandbox operations begin.

When the Microsoft MXC (Microsoft eXperience Container) sandbox modifies host filesystem permissions, a system crash can leave Discretionary Access Control Lists (DACLs) in a partially modified state. To prevent permanent security drift, the microsoft/mxc repository implements a deterministic orphaned-state recovery mechanism that runs automatically on every startup. This article examines the crash-safe state persistence, process liveness validation, and atomic cleanup logic that ensures host integrity even after unexpected termination.

The Orphaned State Problem

MXC protects the host filesystem by augmenting directory DACLs for AppContainer sandboxes. Because these modifications are applied after state persistence, a crash between writing the state file and completing the ACL changes—or between applying ACLs and cleaning up—leaves orphaned modifications on the host. These orphaned entries could permanently alter access permissions if not automatically detected and reverted.

Crash-Safe State File Architecture

Atomic Write Mechanism

Before each Access Control Entry (ACE) is applied, the DaclManager writes a complete description of the pending change to a JSON state file. The implementation in src/core/wxc_common/src/filesystem_dacl.rs uses crash-safe atomic writes: data is first written to a temporary <run-id>.json.tmp file, explicitly fsynced, then renamed to <run-id>.json. This ensures that a valid state file exists only when the write is complete, as implemented in the write_state_file function (lines 33-42).

State File Location and Format

State files are stored in %LOCALAPPDATA%\Microsoft\MXC\dacl-restore by default, or under the path specified by the MXC_DACL_STATE_DIR environment variable. Each file is named with a unique run-id and contains the full description of pending DACL modifications, enabling precise restoration of the original access control state.

Startup Recovery Hook

At the very beginning of wxc::main, MXC calls recover_orphaned_state() (defined in src/core/wxc/src/main.rs, lines 40-47). This execution occurs before any COM initialization or sandbox probing, guaranteeing that leftover state is cleaned up before new work begins. If recovery fails, the process logs the error and continues, ensuring that startup is not blocked by transient filesystem issues.

The Recovery Process

Scanning and Parsing State Files

The recover_orphaned_state function opens the state directory and iterates over every .json entry (lines 5-25 in filesystem_dacl.rs). For each file:

  • It attempts to parse the JSON into a StateFile structure
  • If parsing fails, the file is renamed to .json.corrupt for operator inspection (lines 26-35), preventing crash loops from malformed data

Process Liveness Validation

Before restoring any ACEs, MXC verifies that the owning process is actually dead to avoid interfering with active sandboxes. The process_alive_with_image function (lines 95-114) performs three validation checks:

  1. PID validation: Ensures the stored Process ID is non-zero
  2. Image name matching: Compares the stored image_name against the actual process image
  3. Creation timestamp verification: Validates the recorded FILETIME creation time against the live process, preventing false positives from PID reuse

This multi-factor check ensures that MXC never restores DACLs for a process that has legitimately recycled a previously used PID.

ACE Restoration and Cleanup

For processes confirmed dead, the manager calls restore_one to revert each previously applied ACE (lines 64-78). The function tracks:

  • aces_restored: Successfully reverted entries
  • report.errors: Human-readable diagnostics for failures

If restoration succeeds for all ACEs in a state file, the file is deleted. If some ACEs fail due to transient errors like file-handle contention, a new state file is written containing only the still-pending entries, ensuring idempotent retry on the next startup (lines 79-86).

Implementation Example

The following Rust code demonstrates how to invoke the recovery mechanism programmatically:

use wxc_common::filesystem_dacl::{recover_orphaned_state, RecoveryReport};

fn main() {
    match recover_orphaned_state() {
        Ok(report) => {
            println!(
                "DACL recovery: {} file(s) processed, {} ACE(s) restored",
                report.files_processed, report.aces_restored
            );
            for err in report.errors {
                eprintln!("Recovery error: {err}");
            }
        }
        Err(e) => eprintln!("Fatal recovery failure: {e}"),
    }
}

For testing scenarios, you can verify cleanup behavior after deliberately killing a sandbox process:

#[test]
fn orphan_cleanup() {
    // … spawn a sandbox, apply some DACLs, then kill the process …
    let report = recover_orphaned_state().expect("recovery should not panic");
    assert!(report.aces_restored > 0);
}

Summary

  • MXC writes crash-safe JSON state files before applying DACL modifications, using atomic rename operations in write_state_file to guarantee file integrity
  • The recover_orphaned_state function runs unconditionally at startup in wxc::main, processing all pending state files before any sandbox operations begin
  • Process liveness validation uses PID, image name, and creation timestamp to prevent accidental restoration against recycled process identifiers
  • Failed restorations generate new state files for idempotent retry, while successful cleanups delete the original files and report statistics via RecoveryReport

Frequently Asked Questions

What happens if the JSON state file is corrupted?

If recover_orphaned_state encounters a file that cannot be parsed as valid JSON, it renames the file to .json.corrupt and continues processing other files. This prevents the recovery mechanism from crashing while preserving the corrupted data for operator inspection.

How does MXC prevent restoring DACLs for a process that reused a PID?

The process_alive_with_image function validates three criteria: non-zero PID, matching process image name, and identical creation FILETIME. By comparing the stored creation timestamp against the live process's actual creation time, MXC distinguishes between the original process and a new process that happens to reuse the same PID.

Where are the orphaned state files stored?

State files are stored in %LOCALAPPDATA%\Microsoft\MXC\dacl-restore by default. Administrators can override this location by setting the MXC_DACL_STATE_DIR environment variable before launching the MXC process.

Is the recovery process idempotent?

Yes. If restoration of specific ACEs fails due to transient errors like file-handle contention, MXC writes a new state file containing only the pending entries. On the next startup, the recovery process will attempt to restore these remaining ACEs again, continuing until all orphaned modifications are successfully reverted or the files are manually cleared.

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 →