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()) {
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). |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a8860a9-bb20-48ef-91a1-3da8e34b92fb
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 15 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:1221
capture_sessionis still live here, but this helper classifies the startup-info error and deletes the recovery entry for every non-CleanupFailederror. Only after returning doesCaptureSession::Dropattempt the fallible security-environment close, so a close failure leaves broker state untracked. Explicitly abort/finish the session first and use that teardown result to decide whether to remove the entry or mark cleanup deferred.
self.cleanup_capture_prelaunch_failure(&error, &request, &sid_string, logger);
src/backends/appcontainer/common/src/base_container_runner.rs:1239
- The PR description says the child is launched through
CreateProcessAsUserInsideSecurityEnvironment, but this implementation removes that export and usesCreateProcessWwithPROC_THREAD_ATTRIBUTE_SECURITY_ENVIRONMENT. Update the description so reviewers and host-requirement documentation reflect the actual API surface.
CreateProcessW(
PCWSTR::null(),
Some(PWSTR(cmd_wide.as_mut_ptr())),
None,
None,
!inherited_handles.is_empty(),
PROCESS_CREATION_FLAGS(current_creation_flags | EXTENDED_STARTUPINFO_PRESENT.0),
src/backends/appcontainer/common/src/base_container_runner.rs:1305
- For the new security-environment launch,
ERROR_NOT_SUPPORTEDalso means the host feature is unavailable, but the classifier below usesis_api_not_implemented, which excludes it. ACreateProcessWfailure with that code is therefore reported asLaunchFailedinstead of the documentedBackendUnavailable. Include this code only for the capture launch path, or reuse the learning-mode unavailable classifier.
let extended_error = format!("{launch_api_name} failed: {err:?}");
MGudgin
left a comment
There was a problem hiding this comment.
Update the PR description to match the final launch mechanism
The PR description says the child is launched through CreateProcessAsUserInsideSecurityEnvironment, but the final implementation no longer uses that export.
The current code attaches the process security environment using PROC_THREAD_ATTRIBUTE_SECURITY_ENVIRONMENT and launches the child with CreateProcessW plus EXTENDED_STARTUPINFO_PRESENT.
Could you update the description so other reviewers and future readers are evaluating the mechanism that will actually merge?
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a8860a9-bb20-48ef-91a1-3da8e34b92fb
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3a8860a9-bb20-48ef-91a1-3da8e34b92fb
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/backends/appcontainer/common/src/base_container_runner.rs:1156
- This fallible call occurs after the tracking entry and Ctrl-C cleanup registration are installed. The
?returns withoutcleanup_capture_prelaunch_failure, leaving a stale tracking entry/active registration even though no security environment or child was created. Run the prelaunch cleanup before returning this error.
crate::appcontainer_runner::create_default_env_entries().map_err(|error| {
|
/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.PROC_THREAD_ATTRIBUTE_SECURITY_ENVIRONMENT, and launches through normalCreateProcessW.PROC_THREAD_ATTRIBUTE_HANDLE_LIST.CreateProcessW, preventingwxc-execprocess variables and secrets from being inherited.outputPathor a managed per-run temporary ETL path, then seals the trace after process-tree teardown.try_wait()non-blocking; blocking waits and managedWait/WaitAsynccomplete process-tree and capture finalization.🔗 References
🔍 Validation
cargo test -p learning_mode_windows -p appcontainer_common -- --test-threads=1cargo clippy -p learning_mode_windows -p appcontainer_common --all-targets -- -D warningscargo test -p wxc_e2e_tests --no-rundotnet build Microsoft.Mxc.Sdk/Microsoft.Mxc.Sdk.csproj --no-restore172.17.7.153(build26657.1002):CLEAN, exit 0)block: exit 0, 512-byte ETLallow: exit 0, 3,614-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.