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
StateFilestructure - If parsing fails, the file is renamed to
.json.corruptfor 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:
- PID validation: Ensures the stored Process ID is non-zero
- Image name matching: Compares the stored
image_nameagainst the actual process image - Creation timestamp verification: Validates the recorded
FILETIMEcreation 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 entriesreport.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_fileto guarantee file integrity - The
recover_orphaned_statefunction runs unconditionally at startup inwxc::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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →