Add guarded WPR captureDenials fallback - #813
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52e48fee-23e1-4b05-803c-522812fcdda2
Forward audit workload exit codes, avoid policy previews from truncated analysis, and retain the legacy current-directory exclusion in the temporary adjusted-config adapter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52e48fee-23e1-4b05-803c-522812fcdda2
Store newly added PLM files with repository-standard LF endings so whitespace validation reports the actual content cleanly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52e48fee-23e1-4b05-803c-522812fcdda2
Normalize non-verbatim Win32 output aliases during collision checks and keep metadata-only READ_CONTROL denials from producing content-access grants. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52e48fee-23e1-4b05-803c-522812fcdda2
Compare existing outputs by filesystem identity, fail closed when identity cannot be examined, normalize default-stream aliases, and preserve canonical workload self-access filtering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52e48fee-23e1-4b05-803c-522812fcdda2
Use Windows ordinal Unicode case comparison for output collision checks and discard partial capability matches from malformed DACL records. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52e48fee-23e1-4b05-803c-522812fcdda2
Compare complete paths when both self-access operands use the same namespace, and only use volume-relative matching for mixed DOS and device paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52e48fee-23e1-4b05-803c-522812fcdda2
Inspect missing leaves without following reparse points so dangling links remain unexaminable, and make unknown identity dominate an absent counterpart during collision checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52e48fee-23e1-4b05-803c-522812fcdda2
Keep the PLM collision hardening in the Windows resolver without changing Unix filesystem-object semantics or tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52e48fee-23e1-4b05-803c-522812fcdda2
Pass the workload exit code using Clap's equals syntax so timeout and error values such as -1 reach plm stop without being parsed as options. Add regression coverage for positive and negative exit codes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52e48fee-23e1-4b05-803c-522812fcdda2
Run the public PLM process under the caller token and elevate only authenticated fixed WPR control operations. Transfer ETL data over bounded local pipes, use protected ProgramData scratch storage, and retain a guarded elevated start child for owner-death cleanup and explicit post-stop disarm. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52e48fee-23e1-4b05-803c-522812fcdda2
Record the asInvoker public helper, restricted self-elevation, authenticated pipe transfer, protected scratch storage, and guarded lifecycle in the repository Copilot instructions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52e48fee-23e1-4b05-803c-522812fcdda2
Preserve abandoned mutex ownership, disarm guarded capture immediately after ETL transfer, and normalize case-insensitive verbatim UNC paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52e48fee-23e1-4b05-803c-522812fcdda2
Keep live WPR control in-process while wxc-exec owns the singleton, then run public PLM only for existing-trace analysis. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52e48fee-23e1-4b05-803c-522812fcdda2
Add a dedicated stopped protocol frame so guarded cleanup is disabled before ETL handling, including failure-safe guardian termination. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52e48fee-23e1-4b05-803c-522812fcdda2
Retain one elevated guardian for singleton, WPR start, stop, and cleanup. Fail closed on stale or uncertain recovery without cancelling unverified WPR sessions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52e48fee-23e1-4b05-803c-522812fcdda2
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4eff3ee2-323c-494d-99b9-f7b54e495216
Use a post-authentication readiness handshake and PeekNamedPipe polling so empty connected pipes are not mistaken for disconnects. Store default audit artifacts under the caller's local app-data directory so protected installations remain writable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4eff3ee2-323c-494d-99b9-f7b54e495216
Use protected working directories for elevated PLM and WPR processes, and restore a bounded audit-analysis watchdog with concurrent output draining and process termination. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4eff3ee2-323c-494d-99b9-f7b54e495216
Fail closed on uncertain WPR teardown, bound control output draining, restore path validation, and document the final recovery model. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4eff3ee2-323c-494d-99b9-f7b54e495216
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4eff3ee2-323c-494d-99b9-f7b54e495216
Track sandbox process lifetimes and add a bounded elevated protocol that returns process-scoped denial analysis without exposing host-wide ETL. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4eff3ee2-323c-494d-99b9-f7b54e495216
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4eff3ee2-323c-494d-99b9-f7b54e495216
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4eff3ee2-323c-494d-99b9-f7b54e495216
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4eff3ee2-323c-494d-99b9-f7b54e495216
There was a problem hiding this comment.
Pull request overview
Adds a guarded WPR fallback for captureDenials when native PSEC/V2 capture is unavailable, while hardening PLM privilege separation and unifying denial analysis/output.
Changes:
- Adds process-scoped guarded WPR capture across legacy containment tiers.
- Refactors PLM into an unelevated client with a retained elevated guardian.
- Unifies canonical ETL analysis, output generation, lifecycle tracking, and documentation.
Reviewed changes
Copilot reviewed 50 out of 51 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
.github/copilot-instructions.md |
Documents PLM architecture. |
README.md |
Updates audit-mode behavior. |
docs/process-container/os-version-support.md |
Documents fallback support. |
docs/schema.md |
Clarifies capture compatibility. |
src/Cargo.lock |
Updates dependencies. |
src/Cargo.toml |
Registers PLM workspace dependency. |
src/backends/appcontainer/common/src/appcontainer_runner.rs |
Adds guarded capture lifecycle. |
src/backends/appcontainer/common/src/base_container_runner.rs |
Integrates guarded SBOX capture. |
src/backends/appcontainer/common/src/capture_output.rs |
Centralizes denial output. |
src/backends/appcontainer/common/src/dispatcher.rs |
Selects capture-aware fallback tiers. |
src/backends/appcontainer/common/src/guarded_capture.rs |
Defines capture DI traits. |
src/backends/appcontainer/common/src/job_object.rs |
Tracks process lifetimes. |
src/backends/appcontainer/common/src/lib.rs |
Exports new modules. |
src/backends/appcontainer/common/src/test_env.rs |
Adds capture capability overrides. |
src/backends/learning_mode/windows/Cargo.toml |
Adjusts shared dependencies. |
src/backends/learning_mode/windows/src/capability_dacl.rs |
Makes ACE extraction fail closed. |
src/backends/learning_mode/windows/src/etl_decode.rs |
Adds process-lifetime filtering. |
src/backends/learning_mode/windows/src/extractors.rs |
Refines denial extraction. |
src/backends/learning_mode/windows/src/guarded_wpr_protocol.rs |
Adds bounded protocol framing. |
src/backends/learning_mode/windows/src/lib.rs |
Exports guarded protocol. |
src/core/learning_mode_core/src/analyze.rs |
Adds serializable process lifetimes. |
src/core/learning_mode_core/src/lib.rs |
Re-exports lifetime type. |
src/core/mxc_engine/Cargo.toml |
Adds guarded capture dependencies. |
src/core/mxc_engine/src/dispatch.rs |
Enables streaming fallback capture. |
src/core/mxc_engine/src/guarded_capture.rs |
Adapts PLM to backend traits. |
src/core/mxc_engine/src/lib.rs |
Registers guarded capture module. |
src/core/mxc_engine/src/run.rs |
Enables run-to-completion fallback. |
src/core/wxc/Cargo.toml |
Updates PLM dependency usage. |
src/core/wxc/src/audit.rs |
Implements retained audit guardian. |
src/core/wxc/src/main.rs |
Reworks audit orchestration. |
src/core/wxc/src/plm_launch.rs |
Removes legacy elevation launcher. |
src/core/wxc_common/src/filesystem_object.rs |
Exposes object comparison support. |
src/host/plm/Cargo.toml |
Updates PLM dependencies and manifest behavior. |
src/host/plm/build.rs |
Makes public PLM asInvoker. |
src/host/plm/readme.md |
Documents guarded PLM design. |
src/host/plm/src/access_failure.rs |
Removes legacy decoder. |
src/host/plm/src/analysis.rs |
Adds canonical analysis adapter. |
src/host/plm/src/config.rs |
Updates analyzer references. |
src/host/plm/src/coordination.rs |
Hardens singleton coordination. |
src/host/plm/src/elevated.rs |
Implements guarded elevated lifecycle. |
src/host/plm/src/elevated_protocol.rs |
Re-exports shared protocol. |
src/host/plm/src/event_parser.rs |
Removes legacy ETL parser. |
src/host/plm/src/extract_caps.rs |
Updates diagnostic documentation. |
src/host/plm/src/lib.rs |
Revises PLM module surface. |
src/host/plm/src/log.rs |
Uses guarded interactive capture. |
src/host/plm/src/main.rs |
Revises public and hidden CLI modes. |
src/host/plm/src/profile_gen.rs |
Retains only embedded profile data. |
src/host/plm/src/secure_scratch.rs |
Adds protected scratch and recovery state. |
src/host/plm/src/start.rs |
Adds bounded WPR control. |
src/host/plm/src/stop.rs |
Uses canonical unelevated analysis. |
src/host/plm/src/wpr_path.rs |
Hardens WPR working directory. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if self.process_lifetimes.is_some_and(|lifetimes| { | ||
| !lifetimes | ||
| .iter() | ||
| .any(|lifetime| lifetime.contains(raw.pid, raw.filetime)) |
| let guarded_capture_required = request.policy.capture_denials.is_some() | ||
| && (decision.tier != IsolationTier::BaseContainer || !uses_native_capture); |
| fn plm_exe_path() -> Result<std::path::PathBuf, String> { | ||
| let exe = std::env::current_exe() | ||
| .map_err(|e| format!("failed to resolve the current executable's path: {e}"))?; | ||
| let dir = exe | ||
| .parent() | ||
| .ok_or_else(|| "the current executable has no parent directory".to_string())?; | ||
| Ok(dir.join("plm.exe")) |
| let write_result = { | ||
| let mut writer = std::io::BufWriter::new(file); | ||
| write(&mut writer) | ||
| }; |
|
|
||
| fn process_started(&mut self, pid: u32) { | ||
| self.active_process_zero = false; | ||
| if self.active.len() >= MAX_TRACKED_PROCESSES { |
| (Some(_), Some(_)) => { | ||
| match ( | ||
| volume_relative_path(object_name), | ||
| volume_relative_path(app_path), | ||
| ) { | ||
| (Some(object_relative), Some(app_relative)) => { | ||
| !object_relative.is_empty() | ||
| && object_relative.eq_ignore_ascii_case(app_relative) | ||
| } |
| - Each Windows containment backend lives in its own `backends/*/common` crate (e.g. `appcontainer_common`, `windows_sandbox_common`, `isolation_session_common`, `hyperlight_common`, `nanvix_runner`). Backend crates depend on `wxc_common`; there are no cross-edges between backend crates. Windows Sandbox additionally has `windows_sandbox_lifecycle`, which owns the one-shot and state-aware runners and depends on `windows_sandbox_common` for the wire protocol, plus separate daemon and guest binaries. | ||
| - `learning_mode_core` is the cross-platform learning-mode denial model and output layer. It owns denial types, summaries, analyzer abstractions, plain-JSON document emission, and the serializable output-pointer type, and must not depend on any `backends/*` crate. | ||
| - `learning_mode_windows` (`backends/learning_mode/windows`) is a Windows-only backend support crate for the AppInfo-brokered Learning Mode APIs in `processmodel.dll`. It runtime-resolves the Learning Mode trace and process security-environment exports, owns their typed handle/lifecycle wrappers, decodes sealed ETL traces through `learning_mode_core`, and depends on `wxc_common` plus `learning_mode_core`; runner integration consumes it from the AppContainer backend layer. The trace contract is `HRESULT Start` + retryable `HRESULT Stop` + infallible `Close`: `Stop` never consumes the trace handle, and every started trace must be closed exactly once (closing without stopping is the early-exit discard path). The process security-environment contract is `HRESULT Create` + infallible by-value `Close` and consumes a PSEC 1.0 FlatBuffer, not the legacy SBOX buffer; generated PSEC bindings live in `core/generated/process_security_environment_specification`. | ||
| - `plm` (`host/plm`) is the Windows-only legacy WPR Learning Mode helper. Public `plm.exe` is `asInvoker`: ETL analysis and every caller-selected file path stay under the caller token. It self-elevates only hidden fixed WPR start/stop/cancel operations, authenticates the elevated child over unique local PID-checked named pipes, uses the compiled-in profile from protected fixed-volume ProgramData scratch, and streams bounded ETL bytes back to the unelevated parent. Guarded starts retain the elevated child through the workload; owner death or pipe break cancels the trace, while successful stop explicitly disarms the child before releasing the PLM singleton. |
| pipe.write_all(&[CONTROL_STOP_AND_ANALYZE]) | ||
| .context("failed to send guarded PLM analyzed STOP")?; | ||
| write_process_lifetimes(&mut pipe, process_lifetimes) | ||
| .context("failed to send guarded WPR process-lifetime scope")?; |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4eff3ee2-323c-494d-99b9-f7b54e495216
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 50 out of 51 changed files in this pull request and generated 1 comment.
Suppressed comments (7)
src/backends/appcontainer/common/src/job_object.rs:405
- This limits only concurrently active processes, while every exited process remains in
completed. A workload that launches more than 4096 short-lived processes sequentially therefore accumulates an oversized vector;write_process_lifetimeslater rejects it after the STOP control byte has already been sent, causing capture failure and leaving WPR in uncertain recovery state. Bound the total active plus completed lifetime count to the protocol limit.
if self.active.len() >= MAX_TRACKED_PROCESSES {
src/backends/learning_mode/windows/src/extractors.rs:229
- For different namespace types, matching only the volume-relative suffix does not establish that the paths refer to the same volume. For example,
C:\Tools\app.exeis considered equal to\Device\HarddiskVolume4\Tools\app.exeeven whenC:maps to another device, so a real denial can be silently removed. Resolve the DOS drive to its device volume before suppressing cross-namespace matches, or retain the denial when volume identity cannot be proven.
(Some(_), Some(_)) => {
match (
volume_relative_path(object_name),
volume_relative_path(app_path),
) {
(Some(object_relative), Some(app_relative)) => {
!object_relative.is_empty()
&& object_relative.eq_ignore_ascii_case(app_relative)
}
src/core/mxc_engine/src/guarded_capture.rs:28
mxc_enginealso runs in-process through the Rust SDK andmxc_ffi; in those casescurrent_exe()is the consumer application, not an MXC binary. Cargo does not deployplm.exebeside Rust consumers, and the .NET package currently stages onlymxc_ffi.dll, so guarded capture deterministically reportsplm.exe not foundfor those supported SDK surfaces. Resolve the helper from an explicit/install-aware path and package it with every Windows SDK that exposescaptureDenials.
fn plm_exe_path() -> Result<std::path::PathBuf, String> {
let exe = std::env::current_exe()
.map_err(|e| format!("failed to resolve the current executable's path: {e}"))?;
let dir = exe
.parent()
.ok_or_else(|| "the current executable has no parent directory".to_string())?;
Ok(dir.join("plm.exe"))
.github/copilot-instructions.md:205
- This lifecycle description contradicts the implementation and the PLM README:
GuardLifecycle::on_owner_exit/on_pipe_breakpreserve the recovery marker and intentionally leave WPR untouched to avoid cancelling a replacement recording. Document the manual-recovery behavior instead of saying the trace is cancelled.
- `plm` (`host/plm`) is the Windows-only legacy WPR Learning Mode helper. Public `plm.exe` is `asInvoker`: ETL analysis and every caller-selected file path stay under the caller token. It self-elevates only hidden fixed WPR start/stop/cancel operations, authenticates the elevated child over unique local PID-checked named pipes, uses the compiled-in profile from protected fixed-volume ProgramData scratch, and streams bounded ETL bytes back to the unelevated parent. Guarded starts retain the elevated child through the workload; owner death or pipe break cancels the trace, while successful stop explicitly disarms the child before releasing the PLM singleton.
src/backends/appcontainer/common/src/base_container_runner.rs:899
- Native-capture eligibility ignores
leastPrivilegeandnetwork.proxy, so a host with the complete PSEC/V2 API set selects native capture here and thenvalidaterejects the same request at lines 2199–2211. This bypasses the guarded legacy fallback even though SBOX/AppContainer can honor these policies, contradicting the fallback behavior documented indocs/process-container/os-version-support.md:75-82. Include all PSEC policy incompatibilities in native eligibility and ensure the instance-level selection uses the same predicate.
request.policy.capture_denials.is_some()
&& Self::schema_prefers_process_security_environment(request)
&& Self::is_process_security_environment_usable()
&& RealCapturePlatformSupport.check_apis(true).is_ok()
&& (request.policy.denied_paths.is_empty()
|| RealCapturePlatformSupport
.supports_deny_paths()
.unwrap_or(false))
src/core/wxc/src/audit.rs:12
- The child does not cancel on owner death or pipe break; it preserves the recovery marker and deliberately leaves WPR untouched. This module-level contract is therefore misleading for future cleanup changes and should match the implemented uncertain-state behavior.
//! The child opens and validates the wxc-exec owner before starting WPR, stays
//! alive through the workload, and cancels on owner death or pipe break.
src/backends/learning_mode/windows/src/etl_decode.rs:130
- Each retained denial scans all process lifetimes linearly. With the protocol bounds this permits roughly 1,000,000 events × 4,096 windows (over four billion comparisons), making guarded stop analysis likely to exceed its timeout for process-churning workloads. Index windows by PID once (and search only that PID's intervals) before processing the trace.
if self.process_lifetimes.is_some_and(|lifetimes| {
!lifetimes
.iter()
.any(|lifetime| lifetime.contains(raw.pid, raw.filetime))
| Ok(GuardControl::StopAndAnalyze) => { | ||
| let lifetimes = match read_process_lifetimes(pipe) { | ||
| Ok(lifetimes) => lifetimes, | ||
| Err(error) => { | ||
| self.preserve_after_pipe_break(); | ||
| return Err(error).context( | ||
| "failed to receive guarded WPR process-lifetime scope", | ||
| ); | ||
| } | ||
| }; | ||
| return run_guarded_stop(pipe, self, Some(&lifetimes)); |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4eff3ee2-323c-494d-99b9-f7b54e495216
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 52 out of 53 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/core/mxc_engine/src/guarded_capture.rs:28
mxc_engineis also used in-process by the Rust SDK andmxc_ffi, so the current executable is the embedding application (for exampledotnet.exe), notwxc-exec.exe. Those callers therefore look forplm.exebeside the host executable and the guarded fallback always fails even if PLM is packaged beside the native library. Resolve/inject the guardian location per frontend and stage it with the C#/Rust SDK assets rather than deriving it fromcurrent_exe().
let exe = std::env::current_exe()
.map_err(|e| format!("failed to resolve the current executable's path: {e}"))?;
let dir = exe
.parent()
.ok_or_else(|| "the current executable has no parent directory".to_string())?;
Ok(dir.join("plm.exe"))
src/backends/learning_mode/windows/src/extractors.rs:229
- This cross-namespace comparison drops a denial solely because the paths after the volume prefix match.
C:\Tools\app.exeand\Device\HarddiskVolume3\Tools\app.exeare not necessarily the same file—C:may map to another volume—so a real denial can be suppressed. Only filter cross-namespace paths after proving the DOS drive maps to that device volume (or comparing filesystem identity).
(Some(_), Some(_)) => {
match (
volume_relative_path(object_name),
volume_relative_path(app_path),
) {
(Some(object_relative), Some(app_relative)) => {
!object_relative.is_empty()
&& object_relative.eq_ignore_ascii_case(app_relative)
}
src/backends/appcontainer/common/src/job_object.rs:408
- The limit currently bounds only concurrently active processes, while every completed lifetime remains in
completed. A workload can spawn an unbounded number of short-lived children, growing memory until teardown, and then exceed the protocol's 4096-lifetime limit anyway. Bound the total retained lifetimes, not just the active map.
if self.active.len() >= MAX_TRACKED_PROCESSES {
self.fail(format!(
"sandbox job exceeded the {MAX_TRACKED_PROCESSES}-process tracking limit"
));
.github/copilot-instructions.md:205
- This architecture note contradicts the implemented guarded lifecycle and the updated PLM README: owner death or a pipe break preserves the recovery marker and deliberately leaves WPR untouched; there is also no standalone elevated cancel operation. Documenting automatic cancellation could lead future cleanup code to rely on behavior that does not exist.
- `plm` (`host/plm`) is the Windows-only legacy WPR Learning Mode helper. Public `plm.exe` is `asInvoker`: ETL analysis and every caller-selected file path stay under the caller token. It self-elevates only hidden fixed WPR start/stop/cancel operations, authenticates the elevated child over unique local PID-checked named pipes, uses the compiled-in profile from protected fixed-volume ProgramData scratch, and streams bounded ETL bytes back to the unelevated parent. Guarded starts retain the elevated child through the workload; owner death or pipe break cancels the trace, while successful stop explicitly disarms the child before releasing the PLM singleton.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4eff3ee2-323c-494d-99b9-f7b54e495216
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 52 out of 53 changed files in this pull request and generated no new comments.
Suppressed comments (5)
src/core/mxc_engine/src/guarded_capture.rs:27
current_exe()is the consuming application formxc-sdk/mxc_ffi, not a trusted MXC installation directory. This causes the NuGet path to fail because onlymxc_ffi.dllis staged, and—more importantly—an application in a user-writable directory can have a planted siblingplm.exelaunched viarunas. Resolve the helper from a protected/package-controlled location and validate its identity before elevation; package it for every in-process SDK surface.
let exe = std::env::current_exe()
.map_err(|e| format!("failed to resolve the current executable's path: {e}"))?;
let dir = exe
.parent()
.ok_or_else(|| "the current executable has no parent directory".to_string())?;
src/backends/appcontainer/common/src/capture_output.rs:75
- The
BufWriteris dropped without an explicit flush, andBufWriter::dropdiscards flush errors. A buffered disk-full or device error can therefore return success and publish metadata for a truncated denials document. Flush insidewrite_resultso the existing failure cleanup path handles the error.
let write_result = {
let mut writer = std::io::BufWriter::new(file);
write(&mut writer)
};
src/backends/learning_mode/windows/src/extractors.rs:225
- This treats matching volume-relative suffixes as the same file whenever one path is DOS-form and the other is device-form, without proving that the DOS drive maps to that device volume. For example,
C:\Tools\app.exeand\Device\HarddiskVolume4\Tools\app.exeare classified as self-access even when C: is volume 3, silently dropping a real denial. Resolve the DOS-device mapping before comparing, or conservatively retain cross-namespace events.
(Some(_), Some(_)) => {
match (
volume_relative_path(object_name),
volume_relative_path(app_path),
) {
src/backends/appcontainer/common/src/job_object.rs:409
- The bound is applied only to concurrently active processes, while every completed lifetime is retained and the protocol rejects more than 4096 total lifetimes. A workload that starts 4097 short-lived processes sequentially passes this check, then
write_process_lifetimesfails during teardown; the guardian preserves the active WPR session and blocks later captures pending manual recovery. Enforce the total-lifetime bound and provide an explicit stop-without-analysis cleanup path when scope collection fails.
if self.active.len() >= MAX_TRACKED_PROCESSES {
self.fail(format!(
"sandbox job exceeded the {MAX_TRACKED_PROCESSES}-process tracking limit"
));
return;
.github/copilot-instructions.md:205
- This lifecycle description contradicts the implementation and the updated PLM README: owner death or pipe break preserves the recovery marker and deliberately leaves WPR untouched; there is also no standalone elevated cancel operation. Keeping the repository instructions accurate is important because future cleanup changes based on this statement could reintroduce host-wide cancellation races.
- `plm` (`host/plm`) is the Windows-only legacy WPR Learning Mode helper. Public `plm.exe` is `asInvoker`: ETL analysis and every caller-selected file path stay under the caller token. It self-elevates only hidden fixed WPR start/stop/cancel operations, authenticates the elevated child over unique local PID-checked named pipes, uses the compiled-in profile from protected fixed-volume ProgramData scratch, and streams bounded ETL bytes back to the unelevated parent. Guarded starts retain the elevated child through the workload; owner death or pipe break cancels the trace, while successful stop explicitly disarms the child before releasing the PLM singleton.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4eff3ee2-323c-494d-99b9-f7b54e495216
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 53 out of 54 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/backends/learning_mode/windows/src/extractors.rs:229
- The cross-namespace branch equates paths solely by their volume-relative suffix without proving that the DOS drive maps to the device volume. For example, an app at
D:\Tools\app.exewould cause a denial for\Device\HarddiskVolume3\Tools\app.exeto be discarded even when volume 3 isC:. Resolve both paths to the same volume/file identity before suppressing the event; otherwise retain the denial.
(Some(_), Some(_)) => {
match (
volume_relative_path(object_name),
volume_relative_path(app_path),
) {
(Some(object_relative), Some(app_relative)) => {
!object_relative.is_empty()
&& object_relative.eq_ignore_ascii_case(app_relative)
}
.github/copilot-instructions.md:205
- This architecture note contradicts the implemented guarded lifecycle:
GuardedOwner::preserve_after_pipe_breakpreserves the recovery marker and leaves WPR untouched, as the updated PLM readme also documents. Recording cancellation here would mislead future maintenance of this security-sensitive path.
- `plm` (`host/plm`) is the Windows-only legacy WPR Learning Mode helper. Public `plm.exe` is `asInvoker`: ETL analysis and every caller-selected file path stay under the caller token. It self-elevates only hidden fixed WPR start/stop/cancel operations, authenticates the elevated child over unique local PID-checked named pipes, uses the compiled-in profile from protected fixed-volume ProgramData scratch, and streams bounded ETL bytes back to the unelevated parent. Guarded starts retain the elevated child through the workload; owner death or pipe break cancels the trace, while successful stop explicitly disarms the child before releasing the PLM singleton.
src/backends/appcontainer/common/src/job_object.rs:423
- This asynchronously opens a process from the PID in
JOB_OBJECT_MSG_NEW_PROCESS. A short-lived descendant can exit—and its PID can be released or reused—before this worker dequeues the packet, soOpenProcesscan fail or observe an unrelated process. The tracker then discards the whole capture or can build an incorrect lifetime window. Lifetime collection needs a race-free process identity source rather than reopening notification PIDs.
let handle = match unsafe {
OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE,
false,
pid,
)
} {
src/core/mxc_engine/src/guarded_capture.rs:110
- This missing-helper error is returned as an untyped
Stringfromfactory.start, and both runner call sites classify start failures as generic launch/backend failures. Therefore a deployment withoutplm.exedoes not produce the documentedbackend_unavailableerror. Add a typed unavailable result or preflight helper availability before dispatch so this case maps toFailurePhase::BackendUnavailable.
if !plm_path.exists() {
return Err(format!(
"plm.exe not found at {} (required for the guarded WPR captureDenials fallback)",
plm_path.display()
));
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4eff3ee2-323c-494d-99b9-f7b54e495216
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 53 out of 54 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/backends/learning_mode/windows/src/extractors.rs:229
- This cross-namespace branch compares only the volume-relative suffix, without proving that the DOS drive maps to the reported device volume. For example, if
C:maps toHarddiskVolume4, an object onHarddiskVolume3\Tools\app.exeis incorrectly dropped as self-access. Resolve the DOS/device volume identity before filtering; if identity cannot be proven, preserve the denial.
(Some(_), Some(_)) => {
match (
volume_relative_path(object_name),
volume_relative_path(app_path),
) {
(Some(object_relative), Some(app_relative)) => {
!object_relative.is_empty()
&& object_relative.eq_ignore_ascii_case(app_relative)
}
.github/copilot-instructions.md:205
- This says owner death or pipe break cancels the trace, but
GuardedSession/GuardedOwnerdeliberately preserve the recovery marker and leave WPR untouched on those paths (src/host/plm/src/elevated.rs:258-285, 388-430). Keep this architecture guidance aligned with the implemented fail-closed recovery behavior.
- `plm` (`host/plm`) is the Windows-only legacy WPR Learning Mode helper. Public `plm.exe` is `asInvoker`: ETL analysis and every caller-selected file path stay under the caller token. It self-elevates only hidden fixed WPR start/stop/cancel operations, authenticates the elevated child over unique local PID-checked named pipes, uses the compiled-in profile from protected fixed-volume ProgramData scratch, and streams bounded ETL bytes back to the unelevated parent. Guarded starts retain the elevated child through the workload; owner death or pipe break cancels the trace, while successful stop explicitly disarms the child before releasing the PLM singleton.
src/core/wxc/src/main.rs:1263
- When guarded stop/transfer fails, this branch writes only to the diagnostic logger. The command can then exit with the workload's successful status while producing no audit artifacts and no stderr explanation. Surface the capture failure to the caller instead of silently losing the audit result.
src/host/plm/readme.md:111 - This workspace has no
default-memberslist, sohost/plmis a default member (seesrc/Cargo.toml:1-43) and ordinary workspace builds already include it. The build guidance should not claim that PLM is excluded.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4eff3ee2-323c-494d-99b9-f7b54e495216
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 53 out of 54 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/backends/learning_mode/windows/src/extractors.rs:229
- When the object and app paths use different namespaces, this compares only the suffix after the volume and ignores volume identity. For example,
\Device\HarddiskVolume3\Tools\app.exeis treated as the same file asD:\Tools\app.exeeven ifD:maps to another volume, so a real denial can be silently removed. Resolve the DOS drive to its device volume (or compare filesystem identity) before applying the self-access filter.
) {
(Some(object_relative), Some(app_relative)) => {
!object_relative.is_empty()
&& object_relative.eq_ignore_ascii_case(app_relative)
}
.github/copilot-instructions.md:205
- This newly documented lifecycle contradicts the implementation: owner death and pipe break preserve the recovery marker and deliberately leave WPR untouched (
elevated.rs:138-147,177-182,349-360); there is also no separately elevated stop/cancel operation—stop/discard use the retained START child. Keeping this architecture guidance inaccurate can lead future changes to reintroduce unsafe host-wide cancellation.
- `plm` (`host/plm`) is the Windows-only legacy WPR Learning Mode helper. Public `plm.exe` is `asInvoker`: ETL analysis and every caller-selected file path stay under the caller token. It self-elevates only hidden fixed WPR start/stop/cancel operations, authenticates the elevated child over unique local PID-checked named pipes, uses the compiled-in profile from protected fixed-volume ProgramData scratch, and streams bounded ETL bytes back to the unelevated parent. Guarded starts retain the elevated child through the workload; owner death or pipe break cancels the trace, while successful stop explicitly disarms the child before releasing the PLM singleton.
src/backends/appcontainer/common/src/appcontainer_runner.rs:1618
- A failed guarded-capture teardown is forgotten after this call because
teardown_doneis set before the work and subsequent calls returnOk(()). Callingwait()again therefore reports the child exit code and hides the first capture failure after the session has already been consumed. Cache and replay the teardownResult, asBaseContainerSandboxProcessdoes.
fn run_teardown(&mut self) -> std::io::Result<()> {
if self.teardown_done {
return Ok(());
}
self.teardown_done = true;
docs/schema.md:77
- This contract change leaves the primary Learning Mode guide stale:
docs/learning-mode/capabilities.md:120-145still says the complete PSEC/V2 set is mandatory, AppContainer fallback is unsupported,leastPrivilege/network.proxyare always rejected, and missing PSEC deny support fails. Update that guide alongside this schema note so users do not receive contradictory host and policy requirements.
// Native PSEC/V2 capture cannot combine with leastPrivilege
// or network.proxy. Hosts without that complete native set
// retain an eligible legacy containment tier and use guarded WPR.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4eff3ee2-323c-494d-99b9-f7b54e495216
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 54 out of 55 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/backends/learning_mode/windows/src/extractors.rs:229
- This cross-namespace branch can drop a real denial. For example,
ObjectName = D:\Tools\app.exeandAppPath = \Device\HarddiskVolume3\Tools\app.exehave the same volume-relative suffix, but nothing here proves thatD:maps toHarddiskVolume3; the event is therefore incorrectly classified as self-access and omitted from canonical capture output. Fail open for differing namespace forms unless the DOS device mapping is resolved and compared.
(Some(_), Some(_)) => {
match (
volume_relative_path(object_name),
volume_relative_path(app_path),
) {
(Some(object_relative), Some(app_relative)) => {
!object_relative.is_empty()
&& object_relative.eq_ignore_ascii_case(app_relative)
}
.github/copilot-instructions.md:205
- This architecture note contradicts the implementation: owner exit and pipe break call the preserve paths in
elevated.rsand deliberately leave WPR untouched with a recovery marker to avoid cancelling a replacement host recording. Saying they cancel will misguide future changes toward the unsafe behavior this guard is designed to prevent.
- `plm` (`host/plm`) is the Windows-only legacy WPR Learning Mode helper. Public `plm.exe` is `asInvoker`: ETL analysis and every caller-selected file path stay under the caller token. It self-elevates only hidden fixed WPR start/stop/cancel operations, authenticates the elevated child over unique local PID-checked named pipes, uses the compiled-in profile from protected fixed-volume ProgramData scratch, and streams bounded ETL bytes back to the unelevated parent. Guarded starts retain the elevated child through the workload; owner death or pipe break cancels the trace, while successful stop explicitly disarms the child before releasing the PLM singleton.
📖 Description
Adds a secure
processContainer.captureDenialsfallback for Windows hosts that do not expose the complete PSEC plus V2 Learning Mode API set.The native path remains PSEC plus V2 capture when the complete capability set is available. Otherwise MXC selects the highest legacy containment tier that fully honors the request (SBOX, AppContainer+BFS, or AppContainer+DACL) and pairs it with the retained elevated PLM guardian. The guardian filters WPR events to OS-observed process lifetime windows and returns bounded canonical denial data; raw host-wide ETL does not cross into the SDK result.
This draft is stacked on #760.
🔗 References
🔍 Validation
✅ Checklist
Cargo.lock, thedependency-feed-checkcheck passes (see docs/pull-requests.md)📋 Issue Type
Microsoft Reviewers: Open in CodeFlow