From 9f3c6579b15f82be4c87c07c03dd9350d46116ae Mon Sep 17 00:00:00 2001 From: Alexander Sklar <22989529+asklar@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:50:29 -0700 Subject: [PATCH 1/4] docs: add guide for debugging workloads in the process container There was no doc covering how to attach a debugger to a process running inside a Windows process container. Adds docs/process-container/debugging.md, covering: * Which process you actually want to debug (wxc-exec vs. the sandboxed workload vs. "just tell me what was blocked"). * The OS-side debug-on-launch hook: the sandboxed process is created suspended, and a debugger command line configured in HKLM\SOFTWARE\wxc (debugOnLaunch) is launched under the caller's token with the PID/TID appended, following the packaged-app on-launch convention. Documents that the hook is feature-gated off by default on every build, that WinDbg is the verified configuration and Visual Studio is not, and the caveats (stale value hangs CI, the debugger is uncontained, process.timeout keeps counting while suspended). * The inject-learning-mode hook as a way to force a learning-mode capability onto a run whose config you do not control, with a warning that the permissive variant weakens containment machine-wide. * Pointers to --audit / captureDenials / the diagnostics console, which answer most "I need a debugger" questions more cheaply. * The launch failures MXC already self-diagnoses in launch_diagnostics.rs. Cross-links the new doc from README (Debugging + Documentation table), docs/diagnostics.md, and docs/learning-mode/capabilities.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 32262a24-46de-45a0-9fc9-f18612f4491c --- .github/copilot-instructions.md | 1 + README.md | 4 + docs/diagnostics.md | 8 + docs/learning-mode/capabilities.md | 5 + docs/process-container/debugging.md | 234 ++++++++++++++++++++++++++++ 5 files changed, 252 insertions(+) create mode 100644 docs/process-container/debugging.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d2c9f143d..dfc88ebd7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -231,6 +231,7 @@ Core references: Per-backend guides: - `docs/process-container/guide.md` — process container (Windows AppContainer / BaseContainer) +- `docs/process-container/debugging.md` — debugging workloads inside the process container (OS-side debug-on-launch hook, inject-learning-mode hook, self-diagnosed launch failures) - `docs/process-container/UIPolicy_Schema.md` — UI policy schema (JOB_OBJECT_UILIMIT_* mappings) - `docs/process-container/os-version-support.md` — per-Windows-release policy-support matrix (filesystem / network / UI) - `docs/lxc-support/lxc-backend.md` — LXC container backend (Linux) diff --git a/README.md b/README.md index 22d913db7..d8ece63d8 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,8 @@ Pick the latest stable schema for new code on any supported platform. See [docs/ ## Debugging +For attaching a debugger to a workload running *inside* a Windows process container — including the debug-on-launch hook — see [docs/process-container/debugging.md](docs/process-container/debugging.md). + ### Debug Console Mode By default, native binaries run in **silent mode** — stdin/stdout/stderr is coupled directly to the container. Use `--debug` for verbose output: @@ -276,6 +278,8 @@ Privacy information can be found at https://privacy.microsoft.com and in the Mic | [docs/diagnostics.md](docs/diagnostics.md) | Diagnostic logging and ETW | | [docs/sandbox-policy/0.7.0/policy.md](docs/sandbox-policy/0.7.0/policy.md) | Sandbox policy 0.7.0 specification | | [docs/process-container/guide.md](docs/process-container/guide.md) | Windows AppContainer / BaseContainer guide | +| [docs/process-container/debugging.md](docs/process-container/debugging.md) | Debugging workloads inside the process container | +| [docs/learning-mode/capabilities.md](docs/learning-mode/capabilities.md) | Learning-mode capabilities and denial capture | | [docs/lxc-support/lxc-backend.md](docs/lxc-support/lxc-backend.md) | LXC backend (Linux) | | [docs/bwrap-support/bubblewrap-backend.md](docs/bwrap-support/bubblewrap-backend.md) | Bubblewrap backend (Linux) | | [docs/seatbelt/seatbelt-backend.md](docs/seatbelt/seatbelt-backend.md) | Seatbelt backend (macOS) | diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 514ac3e21..512ae928a 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -95,3 +95,11 @@ messages work without elevation. ## Scope Diagnostic logging currently covers the **BaseContainer runner only**. + +## See also + +- [Debugging workloads inside the process container](./process-container/debugging.md) + — attaching a debugger to the sandboxed process, including the + debug-on-launch hook +- [Learning-mode capabilities](./learning-mode/capabilities.md) — recording what + the sandbox denied diff --git a/docs/learning-mode/capabilities.md b/docs/learning-mode/capabilities.md index 98a6879f5..1beb817ab 100644 --- a/docs/learning-mode/capabilities.md +++ b/docs/learning-mode/capabilities.md @@ -82,6 +82,11 @@ When either learning-mode capability is in effect the runner emits a diagnostic describing the mode (informational logging for `learningModeLogging`, a retained security warning for `permissiveLearningMode`, readable via `warnings()`). +On builds where the OS-side debug hooks are enabled, a learning-mode capability +can also be injected into every sandboxed launch without touching the config. +That is a debugging aid, not a supported configuration path — see +[Forcing learning mode without editing the config](../process-container/debugging.md#forcing-learning-mode-without-editing-the-config). + ## Three learning-mode flows Learning-mode telemetry is consumed through three distinct flows. They differ in diff --git a/docs/process-container/debugging.md b/docs/process-container/debugging.md new file mode 100644 index 000000000..6c51fae24 --- /dev/null +++ b/docs/process-container/debugging.md @@ -0,0 +1,234 @@ +# Debugging workloads inside the process container + +Attaching a debugger to a process running inside an MXC sandbox is harder than +debugging a normal process, for reasons that are inherent to containment: + +- The sandboxed process starts with a restricted token in a container the + debugger is not part of, so a debugger running as you cannot simply open it. +- The interesting failures usually happen in the first few milliseconds — a DLL + failing to initialize, a config file that cannot be read — and are over long + before you can attach by hand. +- The workload may not fail *visibly* at all. Deny-by-default turns a missing + grant into a generic "Access is denied" deep inside someone else's library. + +This document covers the techniques available today, in the order you should +reach for them. + +> **Platform.** Everything here is **Windows-only** and applies to the +> ProcessContainer backends (classic AppContainer and BaseContainer, which share +> `backends/appcontainer/common`). For the other backends see the +> [per-backend guides](../../README.md#documentation). + +## First: which process are you debugging? + +| You want to debug | Use | +|---|---| +| **MXC itself** — config parsing, policy mapping, backend/tier selection, launch failure | Attach to `wxc-exec.exe` normally. It is an ordinary unsandboxed process; no special setup is needed. | +| **The sandboxed workload** — your app, once it is running under containment | [Debug on launch](#debug-on-launch) (below). | +| **Neither — you just want to know what the sandbox blocked** | [Learning mode and denial capture](#finding-out-what-the-sandbox-blocked). Usually the right answer, and far cheaper than a debugger. | + +A large fraction of "I need a debugger" situations are actually policy gaps. +Try [`--audit`](../learning-mode/capabilities.md#three-learning-mode-flows) +first; it will tell you what the workload tried to touch without you setting +foot in a debugger. + +## Debug on launch + +The OS-side process sandbox has a **debug-on-launch hook**, conceptually similar +to the classic +[Image File Execution Options](https://learn.microsoft.com/windows-hardware/drivers/debugger/debugging-a-uwp-app-using-windbg) +`Debugger` value, but scoped to sandboxed launches rather than to an image name. + +> **This is a mitigation, not a feature.** It exists to unblock developers until +> a designed solution ships. It is gated off by default (see +> [Enabling the hook](#enabling-the-hook)), the configuration surface is not a +> supported API, and it can change or disappear without notice. Do not build +> tooling on top of it. + +### What it does + +The **OS sandbox launch path** implements this hook — not MXC. MXC passes an +ordinary sandboxed-launch request; when the hook is enabled the OS changes how +that request is carried out. Nothing in the MXC config turns it on or off. + +When the hook is enabled: + +1. The sandboxed process is created **suspended**. Its initial thread has not + run a single instruction, so nothing in the workload's startup path has + happened yet. +2. The OS reads the debugger command line from the registry (see + [Configuring the debugger](#configuring-the-debugger)). +3. If a command line is configured, it is launched **under the caller's token** + — that is, as *you*, outside the sandbox — with the sandboxed process's + **process ID and thread ID appended**, following the same convention used for + on-launch debugging of packaged apps: + + ```text + -p -tid + ``` + +4. The debugger attaches and resumes the process, and you get control at the + very first instruction. + +If **no** command line is configured, step 3 is skipped and the process simply +**stays suspended**. That is a deliberate escape hatch: you can attach by +whatever means you like and resume manually. Be aware that manually attaching to +the suspended process is known to be finicky — prefer configuring a debugger +command line and letting the hook launch it. + +### Enabling the hook + +The debug hooks live in their own OS feature group, and the root hook is +**disabled by default on every build**, release branches included. It is only +turned on in internal Windows engineering test runs. On a normal build — +including internal flighting builds — the hook is inert and the registry value +below is never read. + +The feature flag IDs are not published here because they are OS-side and change +across branches. To check whether a given feature flag is on, MXC reads the +Windows Feature Store the same way it does for BaseContainer: + +```text +HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FeatureManagement\Overrides\\ + EnabledState = 2 (REG_DWORD; 2 means enabled) +``` + +`` is `4` or `8`. See `check_velocity_keys` in +[`src/backends/appcontainer/common/src/launch_diagnostics.rs`](../../src/backends/appcontainer/common/src/launch_diagnostics.rs) +for the implementation MXC uses to report disabled flags in launch diagnostics, +and [`tests/scripts/README.md`](../../tests/scripts/README.md) for the flags the +E2E suite already depends on. + +### Configuring the debugger + +Set the debugger command line in: + +```text +HKLM\SOFTWARE\wxc + debugOnLaunch = "" (REG_SZ) +``` + +Writing under `HKLM` requires elevation. The value is a command line, not just +an image path, so you can include switches; the PID/TID arguments are appended +to whatever you put there. + +#### WinDbg + +WinDbg is the configuration that has been verified end to end — it launches, +attaches, and resumes correctly. + +```powershell +# Run elevated. Substitute the path to your own WinDbg install. +$windbg = 'C:\Debuggers\windbgx.exe' + +New-Item -Path 'HKLM:\SOFTWARE\wxc' -Force | Out-Null +Set-ItemProperty -Path 'HKLM:\SOFTWARE\wxc' -Name 'debugOnLaunch' ` + -Value $windbg -Type String +``` + +Because the value is a command line rather than a bare image path, quote it the +way a command line would be quoted if the path contains spaces, and append any +switches you want the debugger to start with. + +To turn it back off, remove the value: + +```powershell +Remove-ItemProperty -Path 'HKLM:\SOFTWARE\wxc' -Name 'debugOnLaunch' +``` + +#### Visual Studio + +**Not verified.** Visual Studio's on-launch attach path for packaged apps +differs from WinDbg's, and pointing `debugOnLaunch` at `devenv.exe` has not been +confirmed to work. If you need Visual Studio today, the pragmatic route is to +attach it to the still-suspended process (leave `debugOnLaunch` unset) and +resume by hand, accepting the rough edges noted above. + +### Caveats + +- **Leave it off when you are done.** A stale `debugOnLaunch` value means + *every* sandboxed launch on the machine tries to spawn a debugger, which will + look like a hang to anything that runs MXC non-interactively — including test + suites and CI. +- **The debugger is not contained.** It runs under your token, outside the + sandbox, by design: a debugger inside the container could not do its job. + Consequently, what you can see and touch from the debugger does **not** + reflect the sandbox's restrictions. Do not use the debugger's own view as + evidence of what the workload is allowed to do. +- **The suspended window is real.** Anything with a timeout around sandbox + startup — including MXC's own `process.timeout` — is still counting while you + sit at the first instruction. Set `process.timeout` to `0` (no timeout) while + debugging. + +## Forcing learning mode without editing the config + +The same debug hook group includes an **inject-learning-mode hook**. When +enabled, it adds one of the two learning-mode capabilities to every sandboxed +launch: + +| Variant | Capability injected | Enforcement | +|---|---|---| +| 1 | `learningModeLogging` | **Unchanged** — accesses stay denied, denials are recorded. | +| 2 | `permissiveLearningMode` | **Relaxed** — every access check is allowed and recorded. | + +If the launch already names one of the two capabilities explicitly, the hook +leaves it alone; it never overrides an explicit choice. + +This exists for the case where you do not control the config being passed to +MXC — an app generates it, or it is baked into a harness — but you still need to +see what the workload is reaching for. When you *do* control the config, prefer +the supported entry points instead: +[`processContainer.learningMode`](../learning-mode/capabilities.md#how-to-enable-them) +for deny-and-record, or `wxc-exec --audit` for permissive audit. + +> **Variant 2 turns off deny-by-default** for every sandboxed process on the +> machine while it is enabled — not just the one you are debugging. It is a +> machine-wide weakening of containment. Turn it off as soon as you are done, +> and never enable it on a machine handling anything you care about. + +See [Learning-mode capabilities](../learning-mode/capabilities.md) for what the +two capabilities mean, how the resulting events are collected, and the shape of +the denials output. + +## Finding out what the sandbox blocked + +Before reaching for a debugger, use the machinery built for exactly this +question: + +| Tool | Use it for | +|---|---| +| [`wxc-exec --audit`](../learning-mode/capabilities.md#three-learning-mode-flows) | Bringing a new workload up. Runs permissively, records every access check, and emits `denials.json` plus an `Adjusted_*.json` config with the missing grants already filled in. | +| [`captureDenials`](../learning-mode/capabilities.md#relationship-to-denial-capture) | Programmatic capture from an app or test. `mode: "block"` keeps enforcement on; `mode: "allow"` audits. Returns a structured path to the denials document. | +| [Diagnostics console](../diagnostics.md) | Watching a run live. `MXC_DIAG_CONSOLE=1` plus `mxc-diagnostic-console.exe` streams the parsed request, sandbox spec, process lifecycle, and OS-side ETW events in one window. BaseContainer only. | + +The denials document names the resource in a form you can paste straight back +into a policy — an absolute path for files, the capability name for +capabilities — so the loop is usually "audit, copy the grant, re-run" rather +than "single-step through startup". + +## Common launch failures + +If the workload never gets far enough to debug, check these first. MXC already +detects each one and reports it as a launch diagnostic, so read `wxc-exec`'s +error output before attaching anything. + +| Symptom | Cause | Fix | +|---|---|---| +| `packaged_app` | The target is a packaged (MSIX) app. Packaged apps cannot be launched inside a container. | Install an unpackaged build. | +| `dll_init_failed_ui_required` — exit code `0xC0000142` (`STATUS_DLL_INIT_FAILED`) from PowerShell | The sandbox is blocking Win32k syscalls, which PowerShell needs to initialize. | Set `ui.allowWindows: true`. | +| `missing_filesystem_access` | `pwsh.exe` before 7.7 needs read-only access to the drive root to start. | Add the drive root to `readonlyPaths`, or upgrade to pwsh 7.7+. | +| `feature_not_enabled` — `E_NOTIMPL` from the sandbox API | The BaseContainer feature is not enabled on this build. | Enable the required feature flags, or accept the automatic AppContainer fallback. | + +The heuristics behind these live in +[`launch_diagnostics.rs`](../../src/backends/appcontainer/common/src/launch_diagnostics.rs). + +## Related documentation + +- [Learning-mode capabilities](../learning-mode/capabilities.md) — what gets + recorded, and the three consumption flows +- [MXC diagnostics](../diagnostics.md) — live cross-layer tracing and log + collection +- [Process Container: adding OS features](./guide.md) — the MXC ↔ OS FlatBuffer + contract +- [Windows OS-version policy support](./os-version-support.md) — which policy + aspects each Windows release can actually enforce From 861c53f710f5344096210dd79aa84ec0a6410e21 Mon Sep 17 00:00:00 2001 From: Alexander Sklar <22989529+asklar@users.noreply.github.com> Date: Sat, 29 Aug 2026 13:00:09 -0700 Subject: [PATCH 2/4] docs: correct why debugging a sandboxed process is hard The opening rationale claimed a debugger running as you "cannot simply open" a sandboxed process. That is wrong. Verified empirically on a non-elevated, medium-integrity shell: all 29 AppContainer processes running on the test machine opened successfully with PROCESS_ALL_ACCESS. Mandatory integrity control's no-write-up rule blocks low-to-high access, not the high-to-low direction a debugger needs, and the process DACL grants the creating user. Replaces it with the actual obstacle, which is launch control rather than access: * A debugger cannot create the target itself, because a sandboxed process must be created through the OS sandbox-creation API with a fully-formed spec. A classic IFEO Debugger value does not help either, since it substitutes the debugger for the target image and would run the debugger inside the container. * Attaching after the fact is too late for startup failures. Adds an explicit callout that access is NOT the problem, so readers do not inherit the same misconception, and reframes the hook as launching the debugger alongside the target rather than substituting for it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 32262a24-46de-45a0-9fc9-f18612f4491c --- docs/process-container/debugging.md | 40 +++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/docs/process-container/debugging.md b/docs/process-container/debugging.md index 6c51fae24..8bb57c155 100644 --- a/docs/process-container/debugging.md +++ b/docs/process-container/debugging.md @@ -3,13 +3,29 @@ Attaching a debugger to a process running inside an MXC sandbox is harder than debugging a normal process, for reasons that are inherent to containment: -- The sandboxed process starts with a restricted token in a container the - debugger is not part of, so a debugger running as you cannot simply open it. -- The interesting failures usually happen in the first few milliseconds — a DLL - failing to initialize, a config file that cannot be read — and are over long - before you can attach by hand. -- The workload may not fail *visibly* at all. Deny-by-default turns a missing - grant into a generic "Access is denied" deep inside someone else's library. +- **You cannot put the debugger in front of the process.** The usual flow — the + debugger creates the target with `DEBUG_PROCESS` and owns it from instruction + zero — is unavailable. A sandboxed process must be created by MXC through the + OS sandbox-creation API with a fully-formed sandbox spec, and a debugger has + no way to issue that call. Nor can you redirect the launch with a classic + Image File Execution Options `Debugger` value: that substitutes the debugger + for the target image, so the debugger would end up running *inside* the + container, restricted by the very policy you are trying to investigate. +- **Attaching afterwards is usually too late.** The interesting failures — a DLL + failing to initialize, a config file that cannot be read — happen in the first + few milliseconds and are over long before you can attach by hand. +- **The workload may not fail *visibly* at all.** Deny-by-default turns a + missing grant into a generic "Access is denied" deep inside someone else's + library. + +> **What is *not* the problem: access to the process.** Containment restricts +> what the sandboxed process can reach; it does not hide that process from you. +> A same-user debugger can open a running AppContainer process with +> `PROCESS_ALL_ACCESS` without elevation — mandatory integrity control's +> no-write-up rule blocks *low-to-high* access, not the high-to-low direction a +> debugger needs. So if you can catch the process while it is still alive, +> ordinary attach works. The difficulty is one of *timing and launch control*, +> which is exactly what the hook below addresses. This document covers the techniques available today, in the order you should reach for them. @@ -34,10 +50,14 @@ foot in a debugger. ## Debug on launch -The OS-side process sandbox has a **debug-on-launch hook**, conceptually similar -to the classic +The OS-side process sandbox has a **debug-on-launch hook**: a registry-configured +debugger that is started as part of a sandboxed launch. It is the spiritual +counterpart of the classic [Image File Execution Options](https://learn.microsoft.com/windows-hardware/drivers/debugger/debugging-a-uwp-app-using-windbg) -`Debugger` value, but scoped to sandboxed launches rather than to an image name. +`Debugger` value, with the one difference that matters here — instead of +*substituting* the debugger for the target image (which would put the debugger +inside the container), it launches the debugger **alongside** the target, +outside the container, and hands it the target's PID. > **This is a mitigation, not a feature.** It exists to unblock developers until > a designed solution ships. It is gated off by default (see From bf922c10e53213901397b95567f745005a0f8d45 Mon Sep 17 00:00:00 2001 From: Alexander Sklar <22989529+asklar@users.noreply.github.com> Date: Sat, 29 Aug 2026 13:03:51 -0700 Subject: [PATCH 3/4] docs: remove OS feature-gating details from the debugging guide The "Enabling the hook" section described how the OS gates the debug hooks and how to inspect that gating. That is an internal implementation detail of an unshipped capability and does not belong in a public repo. Removed: * The description of the hooks living in a dedicated OS feature group. * The characterization of which build types have it on, and the reference to internal test runs and flighting builds. * The Windows Feature Store registry path, priority values, and EnabledState semantics, plus the pointer to the velocity-key checking code as a way to inspect the debug hooks specifically. Replaced with an Availability section that states only what a reader needs: the hook is off by default, is not available on generally available builds, is not turned on by anything in MXC, and can be recognized behaviorally (the launch stops before the workload runs). Enablement is explicitly out of scope and directed to internal channels. The BaseContainer feature_not_enabled row in the launch-failures table is unchanged; that diagnostic is a shipped, already-public behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 32262a24-46de-45a0-9fc9-f18612f4491c --- docs/learning-mode/capabilities.md | 6 ++--- docs/process-container/debugging.md | 41 +++++++++++------------------ 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/docs/learning-mode/capabilities.md b/docs/learning-mode/capabilities.md index 1beb817ab..016e225e9 100644 --- a/docs/learning-mode/capabilities.md +++ b/docs/learning-mode/capabilities.md @@ -82,9 +82,9 @@ When either learning-mode capability is in effect the runner emits a diagnostic describing the mode (informational logging for `learningModeLogging`, a retained security warning for `permissiveLearningMode`, readable via `warnings()`). -On builds where the OS-side debug hooks are enabled, a learning-mode capability -can also be injected into every sandboxed launch without touching the config. -That is a debugging aid, not a supported configuration path — see +In some internal development configurations a learning-mode capability can also +be injected into every sandboxed launch without touching the config. That is a +debugging aid, not a supported configuration path — see [Forcing learning mode without editing the config](../process-container/debugging.md#forcing-learning-mode-without-editing-the-config). ## Three learning-mode flows diff --git a/docs/process-container/debugging.md b/docs/process-container/debugging.md index 8bb57c155..3718bef04 100644 --- a/docs/process-container/debugging.md +++ b/docs/process-container/debugging.md @@ -60,8 +60,8 @@ inside the container), it launches the debugger **alongside** the target, outside the container, and hands it the target's PID. > **This is a mitigation, not a feature.** It exists to unblock developers until -> a designed solution ships. It is gated off by default (see -> [Enabling the hook](#enabling-the-hook)), the configuration surface is not a +> a designed solution ships. It is off by default (see +> [Availability](#availability)), the configuration surface is not a > supported API, and it can change or disappear without notice. Do not build > tooling on top of it. @@ -96,28 +96,19 @@ whatever means you like and resume manually. Be aware that manually attaching to the suspended process is known to be finicky — prefer configuring a debugger command line and letting the hook launch it. -### Enabling the hook +### Availability -The debug hooks live in their own OS feature group, and the root hook is -**disabled by default on every build**, release branches included. It is only -turned on in internal Windows engineering test runs. On a normal build — -including internal flighting builds — the hook is inert and the registry value -below is never read. +The debug-on-launch hook is **off by default and is not available on generally +available Windows builds**. It is a developer aid that only functions in +specific internal development configurations. Nothing in the MXC config, CLI, or +SDK turns it on, and how it is enabled is out of scope for this document. -The feature flag IDs are not published here because they are OS-side and change -across branches. To check whether a given feature flag is on, MXC reads the -Windows Feature Store the same way it does for BaseContainer: - -```text -HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FeatureManagement\Overrides\\ - EnabledState = 2 (REG_DWORD; 2 means enabled) -``` - -`` is `4` or `8`. See `check_velocity_keys` in -[`src/backends/appcontainer/common/src/launch_diagnostics.rs`](../../src/backends/appcontainer/common/src/launch_diagnostics.rs) -for the implementation MXC uses to report disabled flags in launch diagnostics, -and [`tests/scripts/README.md`](../../tests/scripts/README.md) for the flags the -E2E suite already depends on. +The practical test is behavioral: when the hook is active, a sandboxed launch +visibly stops before your workload runs, because the process is created +suspended. If your workload instead runs straight through, the hook is not +active on your build and the registry value below is never consulted. If you +believe you should have access to it and do not, ask through the usual internal +channels rather than trying to enable it yourself. ### Configuring the debugger @@ -182,9 +173,9 @@ resume by hand, accepting the rough edges noted above. ## Forcing learning mode without editing the config -The same debug hook group includes an **inject-learning-mode hook**. When -enabled, it adds one of the two learning-mode capabilities to every sandboxed -launch: +There is also an **inject-learning-mode hook**, subject to the same +[availability](#availability) constraints as debug-on-launch. When active, it +adds one of the two learning-mode capabilities to every sandboxed launch: | Variant | Capability injected | Enforcement | |---|---|---| From cec20206d3a6f8f0c22e00895cedca6808bdc7b6 Mon Sep 17 00:00:00 2001 From: Alexander Sklar <22989529+asklar@users.noreply.github.com> Date: Sat, 29 Aug 2026 13:19:49 -0700 Subject: [PATCH 4/4] docs: correct the debug-on-launch registry mechanism Verified the current implementation on the OS integration branch rather than relying on the original change description, which is now stale. Three factual errors fixed: 1. Registry location. The debugger command line is not read from a product-specific key. It is an Image File Execution Options value named SecurityEnvironmentDebugger (REG_SZ) on the *target executable*, queried via LdrQueryImageFileExecutionOptions. Rewrote the configuration section and the PowerShell example accordingly, and noted the two consequences that follow: it is scoped per-image, and it is distinct from the classic Debugger value so it does not affect unsandboxed launches. 2. Behavior with no debugger configured. The doc claimed the process stays suspended as an escape hatch for manual attach. It does not: when no debugger is launched the suspend is dropped and the process runs normally. Removed the claim and the Visual Studio workaround that depended on it. 3. Learning-mode injection modes. Described as permissive / non-permissive rather than by internal variant numbering. Also sharpened the stale-value caveat: because the value is keyed by image name, setting it on a shared interpreter such as python.exe or pwsh.exe affects every sandboxed run of that interpreter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 32262a24-46de-45a0-9fc9-f18612f4491c --- docs/process-container/debugging.md | 118 +++++++++++++++------------- 1 file changed, 64 insertions(+), 54 deletions(-) diff --git a/docs/process-container/debugging.md b/docs/process-container/debugging.md index 3718bef04..a095d6475 100644 --- a/docs/process-container/debugging.md +++ b/docs/process-container/debugging.md @@ -51,13 +51,14 @@ foot in a debugger. ## Debug on launch The OS-side process sandbox has a **debug-on-launch hook**: a registry-configured -debugger that is started as part of a sandboxed launch. It is the spiritual -counterpart of the classic +debugger that is started as part of a sandboxed launch. It is configured through [Image File Execution Options](https://learn.microsoft.com/windows-hardware/drivers/debugger/debugging-a-uwp-app-using-windbg) -`Debugger` value, with the one difference that matters here — instead of -*substituting* the debugger for the target image (which would put the debugger -inside the container), it launches the debugger **alongside** the target, -outside the container, and hands it the target's PID. +(IFEO), but through its own value — **not** the classic `Debugger` value, and the +distinction is the whole point. `Debugger` *substitutes* the named program for +the target image, which here would run the debugger **inside** the container, +restricted by the very policy you are trying to investigate. This hook instead +launches the debugger **alongside** the target, outside the container, and hands +it the target's PID. > **This is a mitigation, not a feature.** It exists to unblock developers until > a designed solution ships. It is off by default (see @@ -73,11 +74,10 @@ that request is carried out. Nothing in the MXC config turns it on or off. When the hook is enabled: -1. The sandboxed process is created **suspended**. Its initial thread has not - run a single instruction, so nothing in the workload's startup path has - happened yet. -2. The OS reads the debugger command line from the registry (see - [Configuring the debugger](#configuring-the-debugger)). +1. The sandboxed process is created and its initial thread is **suspended**, so + nothing in the workload's startup path has run yet. +2. The OS looks up the debugger command line under the **target executable's** + IFEO key (see [Configuring the debugger](#configuring-the-debugger)). 3. If a command line is configured, it is launched **under the caller's token** — that is, as *you*, outside the sandbox — with the sandboxed process's **process ID and thread ID appended**, following the same convention used for @@ -87,14 +87,13 @@ When the hook is enabled: -p -tid ``` -4. The debugger attaches and resumes the process, and you get control at the - very first instruction. +4. The debugger attaches and **is responsible for resuming** the target. You get + control at the very first instruction. -If **no** command line is configured, step 3 is skipped and the process simply -**stays suspended**. That is a deliberate escape hatch: you can attach by -whatever means you like and resume manually. Be aware that manually attaching to -the suspended process is known to be finicky — prefer configuring a debugger -command line and letting the hook launch it. +If **no** debugger command line is configured for that executable, the hook +drops the suspend and the process runs normally. There is no "leave it suspended +and attach by hand" mode: without a configured debugger you get an ordinary +launch, so configuring the value is the only way to use this hook. ### Availability @@ -103,25 +102,35 @@ available Windows builds**. It is a developer aid that only functions in specific internal development configurations. Nothing in the MXC config, CLI, or SDK turns it on, and how it is enabled is out of scope for this document. -The practical test is behavioral: when the hook is active, a sandboxed launch -visibly stops before your workload runs, because the process is created -suspended. If your workload instead runs straight through, the hook is not -active on your build and the registry value below is never consulted. If you -believe you should have access to it and do not, ask through the usual internal -channels rather than trying to enable it yourself. +The practical test is behavioral: with the hook active and a debugger configured +for your executable, a sandboxed launch visibly stops and your debugger comes up +before the workload runs. If the workload instead runs straight through, either +the hook is not active on your build or the value below is not set for that +executable. If you believe you should have access to it and do not, ask through +the usual internal channels rather than trying to enable it yourself. ### Configuring the debugger -Set the debugger command line in: +The debugger command line is read from the **target executable's** Image File +Execution Options key, under a dedicated `SecurityEnvironmentDebugger` value: ```text -HKLM\SOFTWARE\wxc - debugOnLaunch = "" (REG_SZ) +HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\ + SecurityEnvironmentDebugger = "" (REG_SZ) ``` -Writing under `HKLM` requires elevation. The value is a command line, not just -an image path, so you can include switches; the PID/TID arguments are appended -to whatever you put there. +Two consequences worth internalizing: + +- **It is scoped to one executable**, keyed by image name — not to MXC, and not + to sandboxed launches in general. Set it on the program you actually want to + debug. Note that this is the *sandboxed workload's* exe, not `wxc-exec.exe`. +- **It does not collide with normal IFEO debugging.** `SecurityEnvironmentDebugger` + is a separate value from `Debugger`, so setting it does not change how the + program behaves when launched outside a sandbox. + +Writing under `HKLM` requires elevation. The value is a command line rather than +a bare image path, so you may include switches; the PID/TID arguments are +appended to whatever you put there. #### WinDbg @@ -129,38 +138,39 @@ WinDbg is the configuration that has been verified end to end — it launches, attaches, and resumes correctly. ```powershell -# Run elevated. Substitute the path to your own WinDbg install. +# Run elevated. Substitute your executable and your WinDbg install path. +$exe = 'myapp.exe' $windbg = 'C:\Debuggers\windbgx.exe' +$key = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\$exe" -New-Item -Path 'HKLM:\SOFTWARE\wxc' -Force | Out-Null -Set-ItemProperty -Path 'HKLM:\SOFTWARE\wxc' -Name 'debugOnLaunch' ` - -Value $windbg -Type String +New-Item -Path $key -Force | Out-Null +Set-ItemProperty -Path $key -Name 'SecurityEnvironmentDebugger' -Value $windbg -Type String ``` -Because the value is a command line rather than a bare image path, quote it the -way a command line would be quoted if the path contains spaces, and append any -switches you want the debugger to start with. +If the debugger path contains spaces, quote it the way a command line would be +quoted. To turn it back off, remove the value: ```powershell -Remove-ItemProperty -Path 'HKLM:\SOFTWARE\wxc' -Name 'debugOnLaunch' +Remove-ItemProperty -Path $key -Name 'SecurityEnvironmentDebugger' ``` #### Visual Studio **Not verified.** Visual Studio's on-launch attach path for packaged apps -differs from WinDbg's, and pointing `debugOnLaunch` at `devenv.exe` has not been -confirmed to work. If you need Visual Studio today, the pragmatic route is to -attach it to the still-suspended process (leave `debugOnLaunch` unset) and -resume by hand, accepting the rough edges noted above. +differs from WinDbg's, and pointing `SecurityEnvironmentDebugger` at Visual +Studio has not been confirmed to work. Use WinDbg unless you are prepared to +work out the Visual Studio invocation yourself. ### Caveats -- **Leave it off when you are done.** A stale `debugOnLaunch` value means - *every* sandboxed launch on the machine tries to spawn a debugger, which will - look like a hang to anything that runs MXC non-interactively — including test - suites and CI. +- **Remove the value when you are done.** A stale `SecurityEnvironmentDebugger` + makes every *sandboxed* launch of that executable spawn a debugger, which + looks like a hang to anything running MXC non-interactively — test suites and + CI included. Because the value is keyed by image name, this bites hardest on + common interpreters: setting it on something like `python.exe` or + `pwsh.exe` affects every sandboxed run of that interpreter, not just yours. - **The debugger is not contained.** It runs under your token, outside the sandbox, by design: a debugger inside the container could not do its job. Consequently, what you can see and touch from the debugger does **not** @@ -177,13 +187,13 @@ There is also an **inject-learning-mode hook**, subject to the same [availability](#availability) constraints as debug-on-launch. When active, it adds one of the two learning-mode capabilities to every sandboxed launch: -| Variant | Capability injected | Enforcement | +| Mode | Capability injected | Enforcement | |---|---|---| -| 1 | `learningModeLogging` | **Unchanged** — accesses stay denied, denials are recorded. | -| 2 | `permissiveLearningMode` | **Relaxed** — every access check is allowed and recorded. | +| Non-permissive | `learningModeLogging` | **Unchanged** — accesses stay denied, denials are recorded. | +| Permissive | `permissiveLearningMode` | **Relaxed** — every access check is allowed and recorded. | -If the launch already names one of the two capabilities explicitly, the hook -leaves it alone; it never overrides an explicit choice. +If the launch already names either capability explicitly, the hook leaves it +alone; it never overrides an explicit choice. This exists for the case where you do not control the config being passed to MXC — an app generates it, or it is baked into a harness — but you still need to @@ -192,9 +202,9 @@ the supported entry points instead: [`processContainer.learningMode`](../learning-mode/capabilities.md#how-to-enable-them) for deny-and-record, or `wxc-exec --audit` for permissive audit. -> **Variant 2 turns off deny-by-default** for every sandboxed process on the -> machine while it is enabled — not just the one you are debugging. It is a -> machine-wide weakening of containment. Turn it off as soon as you are done, +> **The permissive mode turns off deny-by-default** for every sandboxed process +> on the machine while it is active — not just the one you are debugging. It is +> a machine-wide weakening of containment. Turn it off as soon as you are done, > and never enable it on a machine handling anything you care about. See [Learning-mode capabilities](../learning-mode/capabilities.md) for what the