Wire captureDenials to Windows Learning Mode runtime - #696
Conversation
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Connects captureDenials to Windows Learning Mode for BaseContainer execution.
Changes:
- Injects mode-specific Learning Mode capabilities.
- Adds ETL capture lifecycle around BaseContainer processes.
- Rejects unsupported AppContainer fallback.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/core/wxc_common/src/config_parser.rs |
Maps capture modes to capabilities and adds tests. |
src/Cargo.lock |
Records the Learning Mode dependency. |
src/backends/appcontainer/common/src/base_container_runner.rs |
Implements capture launch and ETL teardown. |
src/backends/appcontainer/common/src/appcontainer_runner.rs |
Rejects capture on fallback tiers. |
src/backends/appcontainer/common/Cargo.toml |
Adds the Learning Mode crate dependency. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 5 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (4)
src/backends/appcontainer/common/src/base_container_runner.rs:1089
- Failures from this new call flow into the shared error block, which still reports
Experimental_CreateProcessInSandbox failedin both the log andextended_error. That API was not invoked on this branch, so diagnostics point developers at the wrong operation. Track which launch API was used and reportCreateProcessAsUserInsideSecurityEnvironmentfor this path.
let ok = unsafe {
launch(
HANDLE(ptr::null_mut()), // userToken: caller context
ptr::null(), // applicationName (from command line)
cmd_wide.as_mut_ptr(), // commandLine
src/backends/appcontainer/common/src/base_container_runner.rs:1062
- A host can expose these symbols while the feature is disabled, in which case
CaptureSession::beginreturnsApiCallwithERROR_CALL_NOT_IMPLEMENTED/E_NOTIMPL. This path hardcodesLaunchFailed, unlike the existing processmodel launch path that classifies those codes asBackendUnavailable, so callers receive the wrong typed failure for an unsupported host. Preserve the API error code and apply the same unavailable classification here.
return Err(ScriptResponse {
exit_code: -1,
error_message: msg.clone(),
standard_err: msg,
failure_phase: FailurePhase::LaunchFailed,
src/backends/appcontainer/common/src/base_container_runner.rs:681
- This availability check runs only during
spawn_base, butScriptRunner::runskips spawning fordry_runafter callingBaseContainerRunner::validate. Consequently,--dry-runreports “validation passed” on a host where BaseContainer exists but these capture exports are unavailable, even though the same request immediately fails at runtime. Include the capture API availability check in backend validation (ideally through a shared helper to avoid drifting checks).
This issue also appears in the following locations of the same file:
- line 1058
- line 1085
if capture_denials.is_some() {
let _ = writeln!(logger, "{EMOJI_SECTION} SECTION: captureDenials");
match (SecurityEnvironmentApi::load(), LearningModeApi::load()) {
src/core/wxc_common/src/config_parser.rs:834
- This does not preserve
mode: "block"after CLI processing.main.rs:1069later applies--auditby removinglearningModeLoggingand injectingpermissiveLearningMode, whilecapture_denials.moderemainsBlock; the capture therefore runs allow-all despite its deny-and-record mode. Reject the--audit+captureDenialscombination or synchronize the capture mode during the post-parse CLI mutation.
CaptureDenialsMode::Block => {
policy.capabilities.retain(|capability| {
!capability.eq_ignore_ascii_case("permissiveLearningMode")
});
"learningModeLogging"
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
dccad10 to
0fcce52
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 5 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/backends/appcontainer/common/src/base_container_runner.rs:1096
- This branch launches with
CreateProcessAsUserInsideSecurityEnvironment, but the shared failure path at line 1202 still reportsExperimental_CreateProcessInSandbox failed. Any capture-specific launch failure is therefore attributed to the wrong API, obscuring diagnosis of the new path. Track which launch API was used and format the failure with its actual name.
let ok = unsafe {
launch(
HANDLE(ptr::null_mut()), // userToken: caller context
ptr::null(), // applicationName (from command line)
cmd_wide.as_mut_ptr(), // commandLine
current_creation_flags, // creationFlags
current_env_ptr, // environment
cwd_ptr, // currentDirectory
&si, // startupInfo
session.environment(), // process security environment
&mut pi, // processInformation
)
0fcce52 to
45a8451
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 5 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
src/backends/appcontainer/common/src/base_container_runner.rs:681
- This availability check runs only inside
spawn_base, butScriptRunner::runreturns success fordry_runimmediately aftervalidate_runner. SinceBaseContainerRunner::validatechecks only the one-shot BaseContainer API, dry-run incorrectly succeeds on hosts that have BaseContainer but lack these newer capture APIs. Include both Learning Mode API probes in runner validation whencapture_denialsis set, then reuse or reload them during spawn.
match (SecurityEnvironmentApi::load(), LearningModeApi::load()) {
src/backends/appcontainer/common/src/base_container_runner.rs:1641
- A trace-seal error returned here becomes the generic non-timeout
waiterror insandbox_process.rs:434, which constructsScriptResponse::errorwith the defaultFailurePhase::None. Consequently a post-launch capture failure is serialized and telemetered as having no failure phase. Preserve a typed post-launch error or update the runner mapping so this path reportsFailurePhase::PostLaunchFailed.
let teardown_result = self.run_teardown();
combine_process_and_teardown_results(result, teardown_result)
src/backends/appcontainer/common/src/base_container_runner.rs:1086
- Failures from this new launch call still enter the shared error block below, which labels the extended error as
Experimental_CreateProcessInSandbox failed. For capture runs that API was never called, so diagnostics point developers at the wrong export. Track which launch function was used and reportCreateProcessAsUserInsideSecurityEnvironmenthere.
launch(
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
src/backends/appcontainer/common/src/base_container_runner.rs:1516
- A terminal
try_wait()does not call this teardown path. Public streaming callers (Sandbox::try_waitandmxc_sandbox_try_wait) can therefore observe that the child exited while the ETL is still unsealed and the trace/security-environment remain live until a laterwait()or handle drop. Finalize through the same tree-kill/reap/teardown path whentry_waitobservesWAIT_OBJECT_0, so completion means the requested output is ready.
// Seal the learning-mode ETL trace now that the child has exited and
// been reaped (both `wait` and `Drop` kill + reap before calling this).
// `finish` stops the trace to the resolved output path, then closes the
// security environment. The output-file path is surfaced on stderr so a
// caller can locate the denials (the full NDJSON contract is finalized
// in the output/consume stage).
src/backends/appcontainer/common/src/base_container_runner.rs:1077
- If this new launch call returns
FALSE, the shared failure branch still reportsExperimental_CreateProcessInSandbox failed, even though that API was not called. Carry the selected launch API name into the failure path and reportCreateProcessAsUserInsideSecurityEnvironmenthere; otherwise captureDenials failures point operators at the wrong API.
// The launch yields (api_return_code, last_win32_error_on_failure).
let (success, last_error) = if let (Some(session), Some(se_api)) =
(capture_session.as_ref(), capture_se_api.as_ref())
{
// Single-attempt in-environment launch. The learning-mode security
// environment only exists on builds that support the `environment`
// parameter, so the CreateProcessInSandbox env-not-supported retry
// does not apply here.
pi = unsafe { std::mem::zeroed() };
src/backends/appcontainer/common/src/base_container_runner.rs:1065
- This early return occurs after the tracking entry and Ctrl-C cleanup registration are installed, and possibly after the builtin proxy starts, but it does not undo any of them.
CaptureSession::begincleans its own partial environment, but the runner state remains registered/leaked in a long-lived streaming host. Mirror the later launch-failure cleanup before returning.
Err(e) => {
let msg = format!("captureDenials: failed to start learning-mode capture: {e}");
let _ = writeln!(logger, "Error: {msg}");
return Err(ScriptResponse {
exit_code: -1,
error_message: msg.clone(),
standard_err: msg,
failure_phase: FailurePhase::LaunchFailed,
..Default::default()
7a7c8ef to
07da288
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
src/backends/appcontainer/common/src/base_container_runner.rs:1602
try_wait()now tears down every BaseContainer run, even whencaptureDenialsis not configured. Once the foreground process signals,terminate_and_reap()terminates the job and therefore kills background descendants; the AppContainer implementation still only reports the exit code, and callers may rely on the existing streaming/grace-window behavior. Restrict this eager teardown to capture sessions (or a previously cached capture result) so ordinary BaseContainer polling remains non-destructive.
self.terminate_and_reap();
let teardown_result = self.run_teardown();
combine_process_and_teardown_results(Ok(code as i32), teardown_result).map(Some)
src/backends/appcontainer/common/src/base_container_runner.rs:681
- The required security-environment and trace exports are checked only inside
spawn_base().ScriptRunner::run()returns success for a dry run immediately afterBaseContainerRunner::validate(), so--dry-runreports “validation passed” on hosts wherecaptureDenialswill always fail asBackendUnavailable. Reuse this API availability check fromvalidate()whenever capture is requested, while keeping the loaded handles for the real spawn path if desired.
This issue also appears on line 1600 of the same file.
if capture_denials.is_some() {
let _ = writeln!(logger, "{EMOJI_SECTION} SECTION: captureDenials");
match (SecurityEnvironmentApi::load(), LearningModeApi::load()) {
9b27698 to
c7a335c
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
src/backends/appcontainer/common/src/base_container_runner.rs:1546
CaptureSession::finishalso returns errors fromCloseProcessSecurityEnvironment. If stopping the trace succeeds but closing the environment fails, the ETL has been sealed, so this message incorrectly reports a sealing failure. Use teardown wording here (or distinguish stop and close failures) so callers know whether the output may be valid.
Err(error) => Err(std::io::Error::other(format!(
"captureDenials failed to seal the denial trace: {error}"
))),
Remove unlaunched profiles and tracking after safe capture-init failures, retain deferred recovery state when environment or profile cleanup cannot be proven complete, and stop registered cleanup state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dfea4d42-e8a5-42ef-978a-6434e8bf89ec
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 9 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
src/backends/appcontainer/common/src/base_container_runner.rs:1055
- The capture session stays alive on the new launch path until
spawn_basereturns. Both the launch-failure handler (lines 1215–1270) and job-setup failure handler (lines 1311–1351) callrun_sandbox_cleanupbefore that drop, so profile deletion runs while the process security environment is still open.cleanup_sandboxthen removes the tracking key even if profile deletion fails, leaving an orphan with no recovery record. Explicitly discard/close the capture session before profile cleanup, and retain/mark the tracking entry deferred if closing fails.
// normal path. On any early return below, `capture_session` drops and its
// Drop discards the trace and closes the environment (no broker leak).
let mut capture_session: Option<CaptureSession> = None;
src/backends/appcontainer/common/src/base_container_runner.rs:1115
- Capture launches still use the shared failure handler below, which hard-codes
Experimental_CreateProcessInSandbox failedinextended_error. When this branch fails, the actual API wasCreateProcessAsUserInsideSecurityEnvironment, so diagnostics identify the wrong failing operation. Track the selected launch API and use its name in the failure message.
// The launch yields (api_return_code, last_win32_error_on_failure).
let (success, last_error) = if let (Some(session), Some(se_api)) =
(capture_session.as_ref(), capture_se_api.as_ref())
src/backends/appcontainer/common/src/appcontainer_runner.rs:1314
- This rejection occurs in backend validation, but fallback selection has already run by then.
select_backend_with_fallbackapplies Tier 2/3 DACL changes beforeScriptRunner::runcalls this method (and before streamingspawncalls it), so an unsupportedcaptureDenialsrequest with filesystem rules can mutate host ACLs before failing. Reject capture during tier selection before constructing/applying any fallback DACL manager.
if request.policy.capture_denials.is_some() {
let msg = "captureDenials requires the BaseContainer learning-mode APIs and is not \
supported by the AppContainer fallback tier";
return Err(ScriptResponse {
failure_phase: FailurePhase::BackendUnavailable,
..ScriptResponse::error(msg)
});
Select an unstamped AppContainer backend for unsupported captureDenials fallback so normal validation emits the structured backend-unavailable response without modifying host ACLs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dfea4d42-e8a5-42ef-978a-6434e8bf89ec
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 10 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
src/backends/appcontainer/common/src/base_container_runner.rs:1097
CaptureSession::beginhas not been givenidentity—the security-environment API receives onlyspec_bytes—so this failure path cannot have created an AppContainer profile with that name.cleanup_unlaunched_sandboxtherefore attempts to delete a nonexistent profile and retains/marks the tracking entry deferred when deletion fails, leaving a stale registry entry after each capture-initialization failure. Remove the tracking entry directly here (or treat profile-not-found as successful cleanup) rather than requiring deletion of an unrelated named profile.
sandbox_tracking::cleanup_unlaunched_sandbox(
&identity,
&sid_string,
logger,
);
docs/learning-mode/capabilities.md:91
- This now directs users to
captureDenials.mode: "allow", but the runtime added by this PR rejects AppContainer fallback and requires feature-enabled BaseContainer security-environment/Learning Mode APIs. The user-facing docs still describecaptureDenialsonly as “Windows-only,” so many Windows hosts will unexpectedly receiveBackendUnavailable. Document this host/backend requirement alongside the recommendation.
It is also mutually exclusive with `captureDenials`; use
`captureDenials.mode: "allow"` for permissive application-driven capture.
Reuse diagnostic constants, clarify capture API naming and policy matching, extract the environment fallback launch loop, and document the explicit pre-launch cleanup lifecycle. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dfea4d42-e8a5-42ef-978a-6434e8bf89ec
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 10 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
src/backends/appcontainer/common/src/base_container_runner.rs:282
- This failure branch also receives an empty
sid_stringwhen SID derivation failed.mark_cleanup_deferredthen opensTRACKING_BASE\\and writesCleanupDeferredon the shared mappings key, not a per-sandbox entry. Skip the marker when no tracking SID exists.
if learning_mode_cleanup_failed(error) {
sandbox_tracking::mark_cleanup_deferred(
sid_string,
CAPTURE_SECURITY_ENVIRONMENT_CLEANUP_DEFERRED_REASON,
logger,
);
src/backends/appcontainer/common/src/base_container_runner.rs:1229
- When this new in-environment launch returns
FALSE, the shared failure path still reportsExperimental_CreateProcessInSandbox failed. That API was not called for capture requests—the failing call isCreateProcessAsUserInsideSecurityEnvironment—so diagnostics point operators at the wrong host surface. Carry the selected API name into the common error construction or emit a branch-specific extended error.
if ok != 0 {
(ok, None)
} else {
(0, Some(unsafe { GetLastError() }))
Guard tracking operations without a SID, remove only safe pre-launch tracking state, report the actual capture launch API, and document the feature-enabled BaseContainer requirement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dfea4d42-e8a5-42ef-978a-6434e8bf89ec
61aebbc to
28b4572
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 10 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
src/backends/appcontainer/common/src/base_container_runner.rs:852
- This changes the merged #663 contract and contradicts this PR’s description:
outputPathwas documented as the exact caller-controlled ETL destination, but it is now a JSON filename template whose actual path is rewritten. Existing callers waiting on the configured path will never find it and must instead parse stderr. Either preserve the exact-path contract or explicitly version/rename the new contract and update the PR description and validation notes.
})
.transpose()?;
let _ = writeln!(logger, "{EMOJI_SECTION} SECTION: Load API");
// 2. Dynamically load the API from processmodel.dll.
let create_process_in_sandbox = match Self::load_api() {
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 10 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
src/backends/appcontainer/common/src/base_container_runner.rs:1685
try_waitis documented as a non-blocking observation, but this call invokeskill_process_tree()throughterminate_and_reap(). If the root has exited while a background descendant is still running, merely polling withtry_waitnow terminates that descendant whenever capture is enabled; the same API without capture only reports the root exit. Finalization needs to avoid adding a tree-kill side effect totry_wait(or the API contract must be redesigned explicitly).
self.terminate_and_reap();
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
📖 Description
Wires the merged
processContainer.captureDenialscontract to the Windows Learning Mode runtime for the BaseContainer tier.mode: "block"tolearningModeLoggingandmode: "allow"topermissiveLearningModeafter rejecting user-supplied reserved capability names.allowremoves deny-and-record before injecting permissive mode.CreateProcessAsUserInsideSecurityEnvironment, and seals the ETL after exit.outputPathor a managed per-run temporary ETL path.captureDenials.🔗 References
🔍 Validation
cargo check --workspace --all-targetscargo clippy --workspace --all-targets -- -D warningswxc_commoncapture-denials andappcontainer_commontestsblock: exit 0, 512-byte ETLallow: exit 0, 2,036-byte ETL, always-visible permissive security warning✅ Checklist
Cargo.lock, thedependency-feed-checkcheck passes (see docs/pull-requests.md)📋 Issue Type
GitHub Actions runs the PR validation build automatically. The ADO pipeline
(
MXC-PR-Build) is the Azure version of the PR pipeline, kept in parity with the GitHubActions build; it runs on merge to
main, and Microsoft reviewers with write access can trigger iton a PR with
/azp run. See docs/pull-requests.md.If the
dependency-feed-checkcheck fails on a new dependency, the crate must be added tothe feed before the PR can pass. See docs/pull-requests.md
for the steps.
Microsoft Reviewers: Open in CodeFlow