From 71c15627a023a705ee7c985e9cca1a8dac52e17e Mon Sep 17 00:00:00 2001 From: Ross Nichols Date: Mon, 17 Aug 2026 11:16:25 -0700 Subject: [PATCH 1/4] Improve error diagnostics for IT-blocked sandbox launch --- .../common/src/launch_diagnostics.rs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/backends/appcontainer/common/src/launch_diagnostics.rs b/src/backends/appcontainer/common/src/launch_diagnostics.rs index 3ca301eb0..7d98db77d 100644 --- a/src/backends/appcontainer/common/src/launch_diagnostics.rs +++ b/src/backends/appcontainer/common/src/launch_diagnostics.rs @@ -39,6 +39,16 @@ pub fn diagnose_create_process_failure( command_line: &str, readonly_paths: &[String], ) -> LaunchDiagnostic { + if win32_error == ERROR_ACCESS_DISABLED_BY_POLICY.0 { + return LaunchDiagnostic { + kind: "launch_blocked_by_policy", + message: "Windows blocked the sandboxed process launch because of an IT-managed policy rule \ + (ERROR_ACCESS_DISABLED_BY_POLICY, 1260). Contact your system administrator \ + to allow the target executable to run in an MXC sandbox." + .to_string(), + }; + } + // Check for feature-not-enabled (velocity keys). if win32_error == ERROR_CALL_NOT_IMPLEMENTED.0 || win32_error == E_NOTIMPL.0 as u32 { return diagnose_api_not_implemented(); @@ -108,7 +118,8 @@ const REQUIRED_VELOCITY_KEYS: &[(u32, &str)] = &[ // flow through `u32`, which matches the existing public surface of // this module (`diagnose_create_process_failure` takes `u32`). use windows::Win32::Foundation::{ - ERROR_CALL_NOT_IMPLEMENTED, ERROR_NOT_SUPPORTED, E_NOTIMPL, STATUS_DLL_INIT_FAILED, + ERROR_ACCESS_DISABLED_BY_POLICY, ERROR_CALL_NOT_IMPLEMENTED, ERROR_NOT_SUPPORTED, E_NOTIMPL, + STATUS_DLL_INIT_FAILED, }; // -- Internal heuristics ----------------------------------------------------- @@ -331,6 +342,20 @@ mod tests { assert_eq!(diag.kind, "feature_not_enabled"); } + #[test] + fn policy_block_takes_priority_over_executable_heuristics() { + let diag = diagnose_create_process_failure( + ERROR_ACCESS_DISABLED_BY_POLICY.0, + r#""C:\Program Files\PowerShell\7\pwsh.exe" -NoProfile"#, + &[], + ); + assert_eq!(diag.kind, "launch_blocked_by_policy"); + assert!(diag.message.contains("IT-managed")); + assert!(diag.message.contains("1260")); + assert!(diag.message.contains("system administrator")); + assert!(!diag.message.contains("readonlyPaths")); + } + #[test] fn packaged_app_detected_from_command_line() { let cmd = From 4539c2696b9ad131fababcc61e07432428fee580 Mon Sep 17 00:00:00 2001 From: Ross Nichols Date: Mon, 17 Aug 2026 11:46:10 -0700 Subject: [PATCH 2/4] Apply rustfmt to policy launch diagnostic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/backends/appcontainer/common/src/launch_diagnostics.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/backends/appcontainer/common/src/launch_diagnostics.rs b/src/backends/appcontainer/common/src/launch_diagnostics.rs index 7d98db77d..d9a64dd14 100644 --- a/src/backends/appcontainer/common/src/launch_diagnostics.rs +++ b/src/backends/appcontainer/common/src/launch_diagnostics.rs @@ -42,10 +42,11 @@ pub fn diagnose_create_process_failure( if win32_error == ERROR_ACCESS_DISABLED_BY_POLICY.0 { return LaunchDiagnostic { kind: "launch_blocked_by_policy", - message: "Windows blocked the sandboxed process launch because of an IT-managed policy rule \ + message: + "Windows blocked the sandboxed process launch because of an IT-managed policy rule \ (ERROR_ACCESS_DISABLED_BY_POLICY, 1260). Contact your system administrator \ to allow the target executable to run in an MXC sandbox." - .to_string(), + .to_string(), }; } From 6d704748cb93a1de0919226107b9f096b6e02da3 Mon Sep 17 00:00:00 2001 From: Ross Nichols Date: Mon, 17 Aug 2026 11:54:07 -0700 Subject: [PATCH 3/4] Diagnose AppContainer policy-blocked launches Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../common/src/appcontainer_runner.rs | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/src/backends/appcontainer/common/src/appcontainer_runner.rs b/src/backends/appcontainer/common/src/appcontainer_runner.rs index d2c435674..cbbc3066c 100644 --- a/src/backends/appcontainer/common/src/appcontainer_runner.rs +++ b/src/backends/appcontainer/common/src/appcontainer_runner.rs @@ -34,6 +34,7 @@ use windows_core::{PCWSTR, PWSTR}; use crate::capture_output; use crate::guarded_capture::{GuardedCaptureFactory, GuardedCaptureSession}; use crate::job_object::UiJobObject; +use crate::launch_diagnostics::diagnose_create_process_failure; use crate::process_mitigation; use wxc_common::error::WxcError; use wxc_common::logger::Logger; @@ -67,6 +68,24 @@ const PROCESS_CREATION_ALL_APPLICATION_PACKAGES_OPT_OUT: u32 = 1; /// Proxy-related env var names to strip/override when building the child env block. const PROXY_VAR_NAMES: &[&str] = &["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "ALL_PROXY"]; +fn create_process_failure( + err: &windows_core::Error, + command_line: &str, + readonly_paths: &[String], + working_directory: &str, +) -> WxcError { + let hresult = err.code().0 as u32; + let message = if (hresult >> 16) & 0x1FFF == 7 { + diagnose_create_process_failure(hresult & 0xFFFF, command_line, readonly_paths).message + } else { + format!("CreateProcessW failed: {err}") + }; + + WxcError::Process(format!( + "{message} (working directory: {working_directory})" + )) +} + /// Serialize `KEY=VALUE` pairs into a double-null-terminated UTF-16 environment block. /// /// Entries are sorted case-insensitively by key as required by `CreateProcessW`. @@ -1063,11 +1082,12 @@ impl AppContainerScriptRunner { ) } .map_err(|err| { - WxcError::Process(format!( - "CreateProcessW failed: {} (working directory: {})", - err, - working_directory.describe() - )) + create_process_failure( + &err, + &request.script_code, + &request.policy.readonly_paths, + &working_directory.describe(), + ) })?; logger.log_line(&format!( @@ -2144,11 +2164,13 @@ mod tests { // ---- validate_runner: unsupported policy fields surface as errors. ---- use super::{ - AppContainerScriptRunner, FilesystemMode, CAPTURE_DENIALS_FALLBACK_UNSUPPORTED_MSG, + create_process_failure, AppContainerScriptRunner, FilesystemMode, + CAPTURE_DENIALS_FALLBACK_UNSUPPORTED_MSG, }; use crate::guarded_capture::{GuardedCaptureFactory, GuardedCaptureSession}; use learning_mode_core::AnalysisResult; use std::sync::Arc; + use windows::Win32::Foundation::ERROR_ACCESS_DISABLED_BY_POLICY; use wxc_common::models::{ExecutionRequest, FailurePhase}; use wxc_common::sandbox_process::SandboxBackend; @@ -2180,6 +2202,24 @@ mod tests { } } + #[test] + fn appcontainer_policy_block_uses_launch_diagnostic() { + let err = windows_core::Error::from_hresult(ERROR_ACCESS_DISABLED_BY_POLICY.to_hresult()); + let mapped = create_process_failure( + &err, + r#""C:\Program Files\PowerShell\7\pwsh.exe" -NoProfile"#, + &[], + r"C:\work", + ); + let message = mapped.to_string(); + + assert!(message.contains("IT-managed policy rule")); + assert!(message.contains("1260")); + assert!(message.contains("system administrator")); + assert!(message.contains(r"working directory: C:\work")); + assert!(!message.contains("readonlyPaths")); + } + #[test] fn validate_runner_rejects_denied_paths_in_bfs_mode() { let runner = AppContainerScriptRunner::with_filesystem_mode(FilesystemMode::Bfs); From 12e1c07fbce459d85c3f72ff157ce244625cf0e4 Mon Sep 17 00:00:00 2001 From: Ross Nichols Date: Mon, 17 Aug 2026 13:31:38 -0700 Subject: [PATCH 4/4] Limit AppContainer policy diagnostic to error 1260 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../common/src/appcontainer_runner.rs | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/backends/appcontainer/common/src/appcontainer_runner.rs b/src/backends/appcontainer/common/src/appcontainer_runner.rs index cbbc3066c..508607876 100644 --- a/src/backends/appcontainer/common/src/appcontainer_runner.rs +++ b/src/backends/appcontainer/common/src/appcontainer_runner.rs @@ -6,8 +6,8 @@ use std::ptr; use std::sync::Arc; use windows::Win32::Foundation::{ - CloseHandle, GetLastError, LocalFree, SetHandleInformation, ERROR_ALREADY_EXISTS, HANDLE, - HANDLE_FLAG_INHERIT, HLOCAL, WAIT_OBJECT_0, WAIT_TIMEOUT, + CloseHandle, GetLastError, LocalFree, SetHandleInformation, ERROR_ACCESS_DISABLED_BY_POLICY, + ERROR_ALREADY_EXISTS, HANDLE, HANDLE_FLAG_INHERIT, HLOCAL, WAIT_OBJECT_0, WAIT_TIMEOUT, }; use windows::Win32::Security::Authorization::ConvertSidToStringSidW; use windows::Win32::Security::Isolation::{ @@ -74,9 +74,13 @@ fn create_process_failure( readonly_paths: &[String], working_directory: &str, ) -> WxcError { - let hresult = err.code().0 as u32; - let message = if (hresult >> 16) & 0x1FFF == 7 { - diagnose_create_process_failure(hresult & 0xFFFF, command_line, readonly_paths).message + let message = if err.code() == ERROR_ACCESS_DISABLED_BY_POLICY.to_hresult() { + diagnose_create_process_failure( + ERROR_ACCESS_DISABLED_BY_POLICY.0, + command_line, + readonly_paths, + ) + .message } else { format!("CreateProcessW failed: {err}") }; @@ -2170,7 +2174,7 @@ mod tests { use crate::guarded_capture::{GuardedCaptureFactory, GuardedCaptureSession}; use learning_mode_core::AnalysisResult; use std::sync::Arc; - use windows::Win32::Foundation::ERROR_ACCESS_DISABLED_BY_POLICY; + use windows::Win32::Foundation::{ERROR_ACCESS_DISABLED_BY_POLICY, ERROR_CALL_NOT_IMPLEMENTED}; use wxc_common::models::{ExecutionRequest, FailurePhase}; use wxc_common::sandbox_process::SandboxBackend; @@ -2220,6 +2224,18 @@ mod tests { assert!(!message.contains("readonlyPaths")); } + #[test] + fn appcontainer_other_win32_error_preserves_create_process_message() { + let err = windows_core::Error::from_hresult(ERROR_CALL_NOT_IMPLEMENTED.to_hresult()); + let mapped = create_process_failure(&err, "cmd.exe", &[], r"C:\work"); + let message = mapped.to_string(); + + assert!(message.contains("CreateProcessW failed")); + assert!(message.contains(r"working directory: C:\work")); + assert!(!message.contains("BaseContainer")); + assert!(!message.contains("Experimental_CreateProcessInSandbox")); + } + #[test] fn validate_runner_rejects_denied_paths_in_bfs_mode() { let runner = AppContainerScriptRunner::with_filesystem_mode(FilesystemMode::Bfs);